勇哥注:
这个系列的贴子主要是为了培训用(专职自动化设备的C#软件工程师),因此例子的知识范围被限制在行业范围内。
C#基础知识在网上是最多的存在,不足主要下面几点:
1. 内容零碎,没有大纲来组织
2. 什么功能都有讲,就是没有按实际项目的常用程度来组织
3. PLC转上位机的人员,很难把PLC的编程思想转到C#编程上来,需要在知识点讲解上对此问题有点拔
勇哥的这套贴子会有大纲,主要特点是补足以上几点问题,另外本套贴子属于经验性质的圈点知识,不属于完全小白式的教学。
如果读者有好的意见,请留言反馈。
事件
C#的事件是一种特殊的编程机制,用于在程序中处理特定的动作。 它可以在程序运行时触发指定的代码,从而让程序更加灵活和可扩展。
简单例程:
public class Program { public static void Main() { // 创建一个Button类的实例 Button button = new Button(); // 注册一个事件处理函数 button.OnClick += new EventHandler(Button_OnClick); // 当按钮被点击时,触发Button_OnClick函数 button.Click(); } // 当按钮被点击时,会触发此函数 public static void Button_OnClick(object sender, EventArgs e) { Console.WriteLine("Button clicked!"); } }
当编辑winform面板,在一个按钮上双击的时候,VS会自动生成事件注册(+=)和触发函数(Button_OnClick)的代码了。
这是因为winform本身就是基于事件驱动的一种UI框架,VS已经把事件相关的自动化代码生成都做到代码编辑器里了。
做为对比,WPF则是另一种UI框架,它则是基于数据驱动的一种框架。
委托和事件之间的关系和区别:
委托是一种特殊的C#类,用于保存一个或多个方法的引用,可以将它们看作是函数的指针。 事件是一种特殊的委托,它保存了一个或多个方法的引用,当某个特定的动作发生时,就会触发这些方法。 因此,可以把事件看作是委托的一种特殊使用。
目录:
(1)由委托引出事件
(4) 一个练习:模拟数据驱动
(5)观察者集合
(6)延伸阅读参考
(1)由委托引出事件
1. 需求:输出一段信息,可以是英文和中文。
下面是用委托做为函数参数,实现需求。
public delegate void RunDelegate(string name); public class Class事件1 { public static void Run(string name, RunDelegate method) { method(name); } public static void SayEnglish(string name) { Console.WriteLine("Hi," +name); } public static void SayChina(string name) { Console.WriteLine("哈罗," + name); } }
调用:
Class事件1.Run("汪淼", Class事件1.SayChina); Class事件1.Run("wanmiao", Class事件1.SayEnglish);
结果:
Hi,wanmiao 哈罗,wanmiao
2. 希望一次输出全部版本的信息
下面是用多播委托解决需求:
//多播委托 RunDelegate del1; del1 = Class事件1.SayEnglish; //这是赋值 ① del1 += Class事件1.SayChina; //这是多播绑定 ② del1("wanmiao");
这里你会看到一个现象。第①句赋值的=号,不可以写成+=,否则会报“使用了未赋值的局部变量”的编译错误。
当然这个问题可以像下面这样解决:
//多播委托 RunDelegate del1=default(RunDelegate); del1 += Class事件1.SayEnglish; //这是赋值 del1 += Class事件1.SayChina; //这是多播绑定 del1("wanmiao");
至这一步我们有点事件绑定的感觉了。
3. 仿事件进行类的功能封装
public class Class事件2 { public RunDelegate del1; //① public void Run(string name, RunDelegate method) { method(name); } } public class Class事件3 { public static void MainFun() { Class事件2 fun = new Class事件2(); fun.del1 = SayEnglish; fun.del1 += SayChina; fun.Run("wanmiao", fun.del1); } public static void SayEnglish(string name) { Console.WriteLine("Hi," + name); } public static void SayChina(string name) { Console.WriteLine("哈罗," + name); } }
调用者:
Class事件3.MainFun();
输出结果和上面一样。
4. 测试关键字event
把Class事件2类的委托定义 ,添加event关键字
public event RunDelegate del1;
然后出现编译错误:
错误1和错误2信息是一样的,如下:
查阅资料,介绍到通过反编译后,看到编译器会将原来的委托del1会自动变成私有。
以上说明,事件内部实现时确实是一个委托。另外事件中+=和-=也做了一些封装。
因此,事件其实就是基于委托实现的。
(2)用委托实现Observer模式,例子:热水器烧水
需求:假设热水器由三部分组成:热水器、警报器、显示器,它们来自于不同厂商并进行了组装。那么,应该是热水器仅仅负责烧水,它不能发出警报也不能显示水温;在水烧开时由警报器发出警报、显示器显示提示和水温。
此需求可以应用Observer模式。
我们先了解一下Observer设计模式,Observer设计模式中主要包括如下两类对象:
Subject:监视对象,它往往包含着其他对象所感兴趣的内容。
在本范例中,热水器就是一个监视对象,它包含的其他对象所感兴趣的内容,就是temprature字段,
当这个字段的值快到100时,会不断把数据发给监视它的对象。
Observer:监视者,它监视Subject,当Subject中的某件事发生的时候,会告知Observer,
而Observer则会采取相应的行动。在本范例中,Observer有警报器和显示器,
它们采取的行动分别是发出警报和显示水温。
在本例中,事情发生的顺序应该是这样的:
警报器和显示器告诉热水器,它对它的温度比较感兴趣(注册)。
热水器知道后保留对警报器和显示器的引用。
热水器进行烧水这一动作,当水温超过95度时,通过对警报器和显示器的引用,
自动调用警报器的MakeAlert()方法、显示器的ShowMsg()方法。
热水器、屏幕、报警器三个对象如下:
// 热水器 public class Heater { private int temperature; public TempeDelegate temp { get; set; } = null; // 烧水 public void BoilWater() { for (int i = 0; i <= 100; i++) { temperature = i; } } } // 警报器 public class Alarm { private void MakeAlert(int param) { Console.WriteLine($"Alarm:水已经 {param}度了"); } } // 显示器 public class Display { private void ShowMsg(int param) { Console.WriteLine($"Display:水已烧开,当前温度:{param}度"); } }
实现代码:
// 热水器 public class Heater { private int temperature; public delegate void TempeDelegate(int data); public TempeDelegate temp { get; set; } = null; // 烧水 public void BoilWater() { for (int i = 0; i <= 100; i++) { temperature = i; if(i>=95 & temp!=null) { temp(i); Thread.Sleep(1000); } } } } // 警报器 public class Alarm { public void MakeAlert(int param) { Console.WriteLine($"Alarm:水已经 {param}度了"); } } // 显示器 public class Display { public void ShowMsg(int param) { Console.WriteLine($"Display:水已烧开,当前温度:{param}度"); } }
调用者:
var heater = new Heater(); heater.temp += new Alarm().MakeAlert; heater.temp += new Display().ShowMsg; heater.BoilWater();
结果:
使用委托,很好的完成了任务。
下面使用事件再次完成Oberver模式,通过对比可以看到事件方式更加灵活。
(3)用事件实现Observer模式
.Net Framework的编码规范:
委托类型的名称都应该以EventHandler结束。
委托的原型定义:有一个void返回值,并接受两个输入参数:一个Object 类型,一个 EventArgs类型(或继承自EventArgs)。
事件的命名为 委托去掉 EventHandler之后剩余的部分。
继承自EventArgs的类型应该以EventArgs结尾。
委托声明原型中的Object类型的参数代表了Subject,也就是监视对象,在本例中是 Heater(热水器)。
回调函数(比如Alarm的MakeAlert)可以通过它访问触发事件的对象(Heater)。
EventArgs 对象包含了Observer所感兴趣的数据,在本例中是temperature。
类:
// 热水器 public class Heater2 { private int temperature; public string type = "图丫丫00A"; public string area = "中国大陆"; public delegate void HeaterEventHandler(Object sender, HeaterEventArgs e); public event HeaterEventHandler Boiled; public class HeaterEventArgs : EventArgs { public readonly int temperature; public HeaterEventArgs(int temp) { this.temperature = temp; } } //可供其它继承类重写,以便继承类拒绝其它对象对它的监视 protected virtual void OnBolied(HeaterEventArgs e) { if(Boiled!=null) { Boiled(this, e); } } // 烧水 public void BoilWater() { for (int i = 0; i <= 100; i++) { temperature = i; if (i >= 95 ) { OnBolied(new HeaterEventArgs(temperature)); Thread.Sleep(1000); } } } } // 警报器 public class Alarm2 { public void MakeAlert(object sender,Heater2.HeaterEventArgs e) { Heater2 heater = (Heater2)sender; //这里就可以访问sender中的公共字段了 Console.WriteLine($"Alarm:{heater.area},{heater.type}"); Console.WriteLine($"Alarm:水已经 {e.temperature}度了"); } } // 显示器 public class Display2 { public void ShowMsg(Object sender,Heater2.HeaterEventArgs e) { Heater2 heater = (Heater2)sender; Console.WriteLine($"Display:{heater.type},{heater.area}"); Console.WriteLine($"Display:水已烧开,当前温度:{e.temperature}度"); } }
调用者:
//这里是用事件实现的烧开水例子 var heater2 = new Heater2(); heater2.Boiled += new Alarm2().MakeAlert; heater2.Boiled += new Display2().ShowMsg; heater2.BoilWater();
结果:
Alarm:中国大陆,图丫丫00A Alarm:水已经 95度了 Display:图丫丫00A,中国大陆 Display:水已烧开,当前温度:95度 Alarm:中国大陆,图丫丫00A Alarm:水已经 96度了 Display:图丫丫00A,中国大陆 Display:水已烧开,当前温度:96度 Alarm:中国大陆,图丫丫00A Alarm:水已经 97度了 Display:图丫丫00A,中国大陆 Display:水已烧开,当前温度:97度 Alarm:中国大陆,图丫丫00A Alarm:水已经 98度了 Display:图丫丫00A,中国大陆 Display:水已烧开,当前温度:98度 Alarm:中国大陆,图丫丫00A Alarm:水已经 99度了 Display:图丫丫00A,中国大陆 Display:水已烧开,当前温度:99度 Alarm:中国大陆,图丫丫00A Alarm:水已经 100度了 Display:图丫丫00A,中国大陆 Display:水已烧开,当前温度:100度
对比先前的委托版本,有什么区别了?
1、 事件可以看做是委托类型的变量
2、委托一般用于回调,而事件一般用于外部接口。
在观察者模式中,被观察者可在内部声明一个事件作为外部观察者注册的接口。
3、事件只能在方法的外部进行声明,而委托在方法的外部和内部都可以进行声明;
4、事件只能在类的内部进行触发,不能在类的外部进行触发。而委托在类的内部和外部都可触发
5、事件有自己一系列的书写规范和预定义类型。
(4)一个练习:模拟数据驱动
数据驱动是使用数据模型来推动界面UI上的动作的。这里数据模型就是ModelA。
wpf的数据驱动不是这么简单的,这里勇哥只是用事件来进行模拟最基本的概念。
类:
public class ModelA: INodityTextboxChange { private string txtValue; public string TxtValue { get { return txtValue; } set { txtValue = value; PropertyChanged?.Invoke(this, new TextboxPropertyChangedEventArgs(value)); if(value=="100") { MyColor = Color.Red; } else { MyColor = Color.Black; } } } private Color myColor; public Color MyColor { get { return myColor; } set { myColor = value; PropertyChanged?.Invoke(this, new TextboxPropertyChangedEventArgs(value.Name)); } } public event TextboxPropertyChangedEventHandler PropertyChanged; } public interface INodityTextboxChange { event TextboxPropertyChangedEventHandler PropertyChanged; } public delegate void TextboxPropertyChangedEventHandler(object sender, TextboxPropertyChangedEventArgs e); public class TextboxPropertyChangedEventArgs:EventArgs { public TextboxPropertyChangedEventArgs(string propertyName) { PropertyName = propertyName; } public virtual string PropertyName { get; private set; } }
调用者:
//练习:模拟数据驱动 private void Model_PropertyChanged(object sender, TextboxPropertyChangedEventArgs e) { //这里只是用字符串区分颜色与文本的通知 switch(e.PropertyName) { case "Red": this.textBox1.ForeColor = Color.Red; break; case "Black": this.textBox1.ForeColor = Color.Black; break; default: this.textBox1.Text = e.PropertyName; break; } } //当模型的属性改变为"100"的时候,文本框文字变红色,否则变黑色 ModelA model = new ModelA(); model.PropertyChanged += Model_PropertyChanged; model.TxtValue = "100"; MessageBox.Show("100"); model.TxtValue = "200";
调用者通过修改了数据模型的属性,实现推动界面UI的动作。
结果:
几点说明:
1、Event?.Invoke 是几个意思?
若event不为null,则Invoke,这个是C#6的新语法,?. 称为空值传播运算符。
Invoke则是调用事件对象(触发事件),委托和控件都有Invoke方法,事件是基于委托的,所以有也Invoke。
C#5 var handler=Event; if(handler!=null) { handler(source,e); } C#6 var handler=Event; handler?.Invoke(source,e);
(5)观察者集合
ObservableCollection表示一个动态数据集合,在添加项、移除项或刷新整个列表时, 此集合将提供通知
所以我们可以利用它的这一特性,制作事件池。
比如软件的面板有几十个,它们之间的消息通讯如果人工一条条来创建就玩复杂了,可以只用ObservableCollection封装一个事件池就可以了。
示例:
//(1)定义事件池的类 public enum WindowEventEnum { 急停,停止,运行 } public class WindowsEvents { public Dictionary<WindowEventEnum, ObservableCollection<bool>> Obc = new Dictionary<WindowEventEnum, ObservableCollection<bool>>() { { WindowEventEnum.急停, new ObservableCollection<bool>() { false} }, { WindowEventEnum.停止, new ObservableCollection<bool>() { false} }, { WindowEventEnum.运行, new ObservableCollection<bool>() { false} } }; } //(2)设备控制面板的停止按钮按下后触发事件 winEvents.Obc[WindowsEvents.WindowEventEnum.停止][0] = true; //(3)在其它的几十个窗口都订阅处理此事件 winEvents.Obc[WindowsEvents.WindowEventEnum.停止].CollectionChanged += MotionDeviceForm_CollectionChanged1; private void MotionDeviceForm_CollectionChanged1(object sender, NotifyCollectionChangedEventArgs e) { if (winEvents.Obc[WindowsEvents.WindowEventEnum.停止][0]) { 面板处理停止动作(); } }
说明:
事件池WindowsEvents设置为全局共享的类。
这样在任何地方,包括面板,都可以统一用事件池的事件进行通讯。
这里勇哥在事件池类WindowsEvents只定义了一个obc池,它的数据类型是bool。
如果你的消息通讯需要其它的数据类型,则需要再仿照上面创建其它的池。
(6)延伸阅读参考
勇哥谈谈ObservableCollection观察者集合
C# 事件总线 EventBus
示例源码下载:
---------------------
作者:hackpig
来源:www.skcircle.com
版权声明:本文为博主原创文章,转载请附上博文链接!

