勇哥注:
这个系列的贴子主要是为了培训用(专职自动化设备的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观察者集合
https://www.skcircle.com/?id=1871
C# 事件总线 EventBus
https://www.skcircle.com/?id=1822
示例源码下载:
---------------------
作者:hackpig
来源:www.skcircle.com
版权声明:本文为博主原创文章,转载请附上博文链接!