勇哥的C#知识经验圈点:事件

勇哥注:

这个系列的贴子主要是为了培训用(专职自动化设备的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)由委托引出事件

(2)用委托实现Observer模式,例子:热水器烧水

(3)用事件实现Observer模式

(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;

然后出现编译错误:

image.png

错误1和错误2信息是一样的,如下:

 image.png

查阅资料,介绍到通过反编译后,看到编译器会将原来的委托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();

结果:

image.png

使用委托,很好的完成了任务。

下面使用事件再次完成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观察者集合

http://47.98.154.65/?id=1871


C# 事件总线 EventBus

http://47.98.154.65/?id=1822



示例源码下载:

支付5元或购买VIP会员后,才能查看本内容!立即支付升级会员查询订单


--------------------- 

作者:hackpig

来源:www.skcircle.com

版权声明:本文为博主原创文章,转载请附上博文链接!



本文出自勇哥的网站《少有人走的路》wwww.skcircle.com,转载请注明出处!讨论可扫码加群:

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

会员中心
搜索
«    2025年4月    »
123456
78910111213
14151617181920
21222324252627
282930
网站分类
标签列表
最新留言
    热门文章 | 热评文章 | 随机文章
文章归档
友情链接
  • 订阅本站的 RSS 2.0 新闻聚合
  • 扫描加本站机器视觉QQ群,验证答案为:halcon勇哥的机器视觉
  • 点击查阅微信群二维码
  • 扫描加勇哥的非标自动化群,验证答案:C#/C++/VB勇哥的非标自动化群
  • 扫描加站长微信:站长微信:abc496103864
  • 扫描加站长QQ:
  • 扫描赞赏本站:
  • 留言板:

Powered By Z-BlogPHP 1.7.2

Copyright Your skcircle.com Rights Reserved.

鄂ICP备18008319号


站长QQ:496103864 微信:abc496103864