事件

事件 (Event ) 是学习 .net 面向对象编程很重要的一部分,在学习事件之前,我们实际上已经在很多地方使用了事件,比如控件的 click 事件等,这些都是 .net 设计控件的时候已经定义好的事件。除此之外,我们同样可以自己定义事件。

事件实际上是一种消息机制,当然点击控件时, click 就通知处理他的方法去处理,实际上就是前面说的委托。因此我们可以说: 事件是一种具有特殊签名的委托。而事件 / 消息机制是 windows 的核心,因此我们必须掌握他。

为了更加容易理解事件,我们还是使用前面的动物的示例来说明,有两三只动物,猫 ( 名叫 Tom) ,还有两只老鼠 (Jerry 和 Jack) ,当猫叫的时候,触发事件 (CatShout), 然后两只老鼠开始逃跑 (MouseRun) 。在使用代码实现这个示例之前,我们先看一下事件的书面定义 .

1.什么是事件(Event)?

事件( Event )是类或对象向其他类或对象通知发生的事情的一种特殊签名的委托 .

2.事件的声明

public event 委托类型 事件名 ;

事件使用 event 关键词来声明,他的返回类值是一个委托类型。

通常事件的命名,以名字 +Event 作为他的名称,在编码中尽量使用规范命名,增加代码可读性。

3.事件示例

下面我们实现本篇开始的猫捉老鼠的示例

首先看一下 UML 图

如上 UML 类图,有猫( Cat )和老鼠 (Mouse) 两个类,里面包含其成员 . 当猫叫 (CatShout) 时 , 触发事件 (CatShoutEvent), 事件通知老鼠 , 然后老鼠跑路 (MouseRun).

两个类的代码如下:

class Cat
{
    string catName;
    string catColor { get; set; }
    public Cat(string name, string color)
    {
        this.catName = name;
        catColor = color;
    }

    public void CatShout()
    {
        Console.WriteLine(catColor+" 的猫 "+catName+" 过来了,喵!喵!喵!\n");

        //猫叫时触发事件
        //猫叫时,如果CatShoutEvent中有登记事件,则执行该事件
        if (CatShoutEvent != null)
            CatShoutEvent();
    }

    public delegate void CatShoutEventHandler();

    public event CatShoutEventHandler CatShoutEvent;

}
class Mouse
{
    string mouseName;
    string mouseColor { get; set; }
    public Mouse(string name, string color)
    {
        this.mouseName = name;
        this.mouseColor = color;
    }

    public void MouseRun()
    {
        Console.WriteLine(mouseColor + " 的老鼠 " + mouseName + " 说:\"老猫来了,快跑!\"  \n我跑!!\n我使劲跑!!\n我加速使劲跑!!!\n");
    }
}

调用如下:

Console.WriteLine("[场景说明]: 一个月明星稀的午夜,有两只老鼠在偷油吃\n");
Mouse Jerry = new Mouse("Jerry", "白色");
Mouse Jack = new Mouse("Jack", "黄色");


Console.WriteLine("[场景说明]: 一只黑猫蹑手蹑脚的走了过来\n");
Cat Tom = new Cat("Tom", "黑色");

Console.WriteLine("[场景说明]: 为了安全的偷油,登记了一个猫叫的事件\n");
Tom.CatShoutEvent += new Cat.CatShoutEventHandler(Jerry.MouseRun);
Tom.CatShoutEvent += new Cat.CatShoutEventHandler(Jack.MouseRun);

Console.WriteLine("[场景说明]: 猫叫了三声\n");
Tom.CatShout();


Console.ReadKey();

运行结果如下:

4.事件参数

上面的事件是最简单的事件,通过我们看到的事件,都会带两个参数,比如 c# winform 中的 button 点击事件的委托方法如下 :

private void button1_Click(object sender, EventArgs e)

带有两个参数,不熟悉事件参数的小伙伴肯定要问,这两个参数 sender 和 e 到底有什么用呢? 第一个参数 sender, 其中 object 类型的参数 sender 表示的是发送消息的对象,为什么要使用 object 类型呢,这是因为不同类型的对象调用时使用 object 能很好的兼容。

第二个参数 e ,他的类型为 EventArgs.EventArgs 这个类没有实际的用途,只是作为一个基类让其他对象继承。很多对象不需要传递额外的信息,例如按钮事件,只是调用一个回调方法就够了。当我们定义的事件不需要传递额外的信息时,这时调用 EventArgs.Empty 就行了,不需要重新构建一个 EventArgs 对象。

我们可以看到在 Button 事件登记时,只传了一个参数 sender

为了更好的理解带参数的事件,我们改写一下上面猫捉老鼠的示例:

先看UML图:

实现代码如下:

class Cat
  {
      string catName;
      string catColor { get; set; }
      public Cat(string name, string color)
      {
          this.catName = name;
          catColor = color;
      }

      public void CatShout()
      {
          Console.WriteLine(catColor+" 的猫 "+catName+" 过来了,喵!喵!喵!\n");

          //猫叫时触发事件
          //猫叫时,如果CatShoutEvent中有登记事件,则执行该事件
          if (CatShoutEvent != null)
              CatShoutEvent(this, new CatShoutEventArgs() {catName=this.catName, catColor=this.catColor});
      }

      public delegate void CatShoutEventHandler(object sender,CatShoutEventArgs e);

      public event CatShoutEventHandler CatShoutEvent;

  }

  /// <summary>
  /// EventArgs类的作用就是让事件传递参数用的
  /// 我们定义一个类CatShout包含两个成员属性,以方便传递
  /// </summary>
  class CatShoutEventArgs:EventArgs
  {
     public  string catColor { get; set; }
     public string catName { get; set; }
  }

  class Mouse
  {
      string mouseName;
      string mouseColor { get; set; }
      public Mouse(string name, string color)
      {
          this.mouseName = name;
          this.mouseColor = color;
      }

      public void MouseRun(object sender, CatShoutEventArgs e)
      {
          if (e.catColor == "黑色")
              Console.WriteLine(mouseColor + " 的老鼠 " + mouseName + " 说:\" " + e.catColor + " 猫 " + e.catName + " 来了,快跑!\"  \n我跑!!\n我使劲跑!!\n我加速使劲跑!!!\n");
          else
              Console.WriteLine(mouseColor + " 的老鼠 " + mouseName + " 说:\" " + e.catColor + " 猫 " + e.catName + " 来了,慢跑!\"  \n我跑!!\n我慢慢跑!!\n我慢慢悠悠跑!!!\n");

      }
  }

调用如下:

Console.WriteLine("[场景说明]: 一个月明星稀的午夜,有两只老鼠在偷油吃\n\n\n");
Mouse Jerry = new Mouse("Jerry", "白色");
Mouse Jack = new Mouse("Jack", "黄色");


Console.WriteLine("[场景说明]: 一只黑猫蹑手蹑脚的走了过来");
Cat Tom = new Cat("Tom", "黑色");
Console.WriteLine("[场景说明]: 为了安全的偷油,登记了一个猫叫的事件");
Tom.CatShoutEvent += new Cat.CatShoutEventHandler(Jerry.MouseRun);
Tom.CatShoutEvent += new Cat.CatShoutEventHandler(Jack.MouseRun);
Console.WriteLine("[场景说明]: 猫叫了三声\n");
Tom.CatShout();

Console.WriteLine("\n\n\n");

//当其他颜色的猫过来时
Console.WriteLine("[场景说明]: 一只蓝色的猫蹑手蹑脚的走了过来");
Cat BlueCat = new Cat("BlueCat", "蓝色");
Console.WriteLine("[场景说明]: 为了安全的偷油,登记了一个猫叫的事件");
BlueCat.CatShoutEvent += new Cat.CatShoutEventHandler(Jerry.MouseRun);
BlueCat.CatShoutEvent += new Cat.CatShoutEventHandler(Jack.MouseRun);
Console.WriteLine("[场景说明]: 猫叫了三声");
BlueCat.CatShout();

运行结果如下:

也可以使用前面学过的Lamda表达式来简洁的写以上的事件注册

Cat Doraemon = new Cat("哆啦A梦", "彩色");
Doraemon.CatShoutEvent += (sender, e) => Jerry.MouseRun(sender, e);
Doraemon.CatShoutEvent += (sender, e) => Jack.MouseRun(sender, e);
Doraemon.CatShout();

调用后结果一样.

5. 事件应用实例

如果上面的简单实例不够过瘾,我下面列举几个日常开发过程中应用事件解决实际问题的例子,加深对事件的理解。

示例一:我们使用一个事件来监控一个文件夹下文件的变更情况

先看一下 UML 类图

代码如下:

/// <summary>
/// 文件夹监控
/// </summary>
public class FolderWatch
{
    public class Files
    {
        string _fileName;
        public string FileName
        {
          get { return _fileName; }
          set { _fileName = value; }
        }
        DateTime _fileLastDate;
        public DateTime FileLastDate
        {
          get { return _fileLastDate; }
          set { _fileLastDate = value; }
        }

    }
    /// <summary>
    /// 文件夹路径
    /// </summary>
    public string folderPath;

    /// <summary>
    /// 文件集合
    /// </summary>
    public List<Files> fileList=new List<Files>();

    /// <summary>
    /// 文件夹监控事件
    /// </summary>
    public event FolderWatchEventHandler FolderWatchEvent;

    /// <summary>
    /// 构造函数
    /// </summary>
    public FolderWatch(string path)
    {
        folderPath = path;

        //获取目录下所有文件
        foreach (var file in  new  System.IO.DirectoryInfo(path).GetFiles())
        {
            fileList.Add(
                 new Files()
                {
                    FileName = file.Name,
                    FileLastDate=file.LastWriteTime
                }

            );
        }
    }
    public void OnFieldChange()
    {
        if (FolderWatchEvent != null)
            FolderWatchEvent(this, new MonitorFileEventArgs { Files = fileList });
    }

    /// <summary>
    /// 委托实现方法
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    public void MonitorFiles(object sender, MonitorFileEventArgs e)
    {

        while(true)
        {
            //遍历fileList文件列表,检测是否有变更(删除或修改)
            if(e.Files!=null)
            foreach(var file in this.fileList)
            {
                string fileFullName=folderPath + "\\" + file.FileName;
                //检测是否存在
                if (!System.IO.File.Exists(fileFullName))
                    Console.WriteLine("文件\"" + file.FileName + "\"已被删除或更名;\n");
                else if (file.FileLastDate != (new System.IO.FileInfo(fileFullName)).LastWriteTime)
                {
                    Console.WriteLine("文件\"" + file.FileName + "\"已被修改过(上次修改日期:" + file.FileLastDate + ",本次检测到日期为:" + (new System.IO.FileInfo(fileFullName)).LastWriteTime + ");\n");

                }

            }
                 //重新获取目录下所有文件
            List<Files> newFiles = new List<Files>();
            foreach (var newFile in new System.IO.DirectoryInfo(this.folderPath).GetFiles())
            {
                newFiles.Add(
                     new Files()
                     {
                         FileName = newFile.Name,
                         FileLastDate = newFile.LastWriteTime
                     }

                );
                if(!(fileList.Any(m=>m.FileName==newFile.Name)))
                    Console.WriteLine("新建文件\"" + newFile.Name+"\"\n");
            }
             fileList.Clear();
            this.fileList = newFiles;
        }
    }
}

/// <summary>
/// 文件夹监控委托
/// </summary>
public delegate void FolderWatchEventHandler(object sender, MonitorFileEventArgs e);

/// <summary>
/// 事件传递参数类
/// </summary>
public class MonitorFileEventArgs : EventArgs
{
    /// <summary>
    /// 文件
    /// </summary>
    public List<FolderWatch.Files> Files { get; set; }

}

调用如下:

string MyFolder = "MyFolder";
FolderWatch folder = new FolderWatch(System.IO.Directory.GetCurrentDirectory() +"\\"+ MyFolder);


folder.FolderWatchEvent += new FolderWatchEventHandler(folder.MonitorFiles);
folder.OnFieldChange();

运行结果如下:

可以看到当我们增加,修改,删除文件时,就会返回文件夹内文件更改的提示信息。

实际上对于文件更改的监控.NET提供了专门的类FileSystemWatcher来完成。上面的示例只是为了加深理解事件,在实际应用中对文件的变更还是有缺陷的,比如同一文件更名、通过时间判断文件变更也是不科学的。

下面我们就使用.net提供的FileSystemWatcher类来完成文件夹监控,代码非常简单

static void watcher_Renamed(object sender,System.IO.RenamedEventArgs e)
      {
          Console.WriteLine("文件\"" + e.OldName + "\"更名为:"+e.Name+";\n");
      }
      static void watcher_Deleted(object sender, System.IO.FileSystemEventArgs e)
      {
          Console.WriteLine("文件\"" + e.Name + "\"已被删除;\n");
      }
      static void watcher_Changed(object sender, System.IO.FileSystemEventArgs e)
      {
          Console.WriteLine("文件\"" + e.Name + "\"已被修改;\n");
      }
      static void watcher_Created(object sender, System.IO.FileSystemEventArgs e)
      {
          Console.WriteLine("新创建了文件\"" + e.Name + "\";\n");
      }

调用如下:

string MyFolder = "MyFolder";
           
           System.IO.FileSystemWatcher watcher = new System.IO.FileSystemWatcher(System.IO.Directory.GetCurrentDirectory() + "\\" + MyFolder);

           watcher.Renamed+= watcher_Renamed;
           watcher.Deleted+=watcher_Deleted;
           watcher.Changed+=watcher_Changed;
           watcher.Created +=watcher_Created;

           watcher.EnableRaisingEvents = true;

运行结果如下:

示例二:使用事件完成了一个文件下载进度条的示例,平时我们看到很多进度条程序员为了偷懒都是加载完成直接跳到 100% ,这个示例就是传说中的真进度条。

UML类图如下:

代码如下:

public partial class Form1 : Form
    {
        System.Threading.Thread thread;
        public Form1()
        {
            InitializeComponent();
        }


        private void downButton_Click(object sender, EventArgs e)
        {

            if(thread==null)
                thread = new System.Threading.Thread(new System.Threading.ThreadStart(StartDown));

            //使用子线程工作
             if (this.downButton.Text == "开始下载文件")
             {
                 this.downButton.Text = "停止下载文件";
                 if (thread.ThreadState.ToString() == "Unstarted")
                 {
                     thread.Start();
                 }
                 else if (thread.ThreadState.ToString() == "Suspended")
                     thread.Resume();
             }
             else
             {
                 this.downButton.Text = "开始下载文件";
                 thread.Suspend();
             }
        }

        //开始加载进度
        public void StartDown()
        {
            //注册事件
            DownLoad down = new DownLoad();
           down.onDownLoadProgress+=down_onDownLoadProgress;
           down.onDownLoadProgress += down_ShowResult;
           down.Start();

        }

        public void down_onDownLoadProgress(long total,long current)
        {


            if (this.InvokeRequired)
            {
                this.Invoke(new DownLoad.DownLoadProgress(down_onDownLoadProgress), new object[] { total, current });
            }
            else
            {
                this.myProgressBar.Maximum = (int)total;
                this.myProgressBar.Value = (int)current;
            }

        }

        public void down_ShowResult(long total,long current)
        {
            Action<long, long> ac = (c, t) => { this.resultShow.Text = ((double)current / total).ToString("P"); ; };
            this.Invoke(ac, new object[] { current, total });
        }



        //下载处理类
        class DownLoad
        {
            //委托
            public delegate void DownLoadProgress(long total, long current);

            //事件
            public event DownLoadProgress onDownLoadProgress;

            //事件
            public event DownLoadProgress down_ShowResult;

            public void Start()
            {
                //下载模拟
                for (int i = 0; i <= 100; i++)
                {
                    if (onDownLoadProgress != null)
                        onDownLoadProgress(100, i);
                    if (down_ShowResult != null)
                        down_ShowResult(100, i);
                    System.Threading.Thread.Sleep(100);
                }
            }


        }
    }

运行结果如下:

上面示例使用 winform 应用程序,实现了一个进度条即时计算进度的例子。在文件下载子类 (DownLoad) 中有两个事件,一个是进度条事件,一个是进度百分比显示事件,在初始化调用时,采用了线程。启用线程时,注册了两个事件。随着模拟进度的加载,触发了进度条事件和显示百分比事件。做到了即时显示。

关于线程相关知识,在后面有时间了会详细说明。

6 要点

6.1 事件:事件是对象发送的消息,发送信号通知客户发生了操作。这个操作可能是由鼠标单击引起的,也可能是由某些其他的程序逻辑触发的。事件的发送方不需要知道哪个对象或者方法接收它引发的事件,发送方只需知道它和接收方之间的中介( delegate )。

6.2 事件处理程序总是返回 void ,它不能有返回值。

6.3只要使用 EventHandler 委托,参数就应是 object 和 EventArgs 。第一个参数是引发事件的对象。第二个参数 EventArgs 是包含有关事件的其他有用信息的对象;这个参数可以是任意类型,只要它派生自 EventArgs 即可。

6.4 方法的命名也应注意,按照约定,事件处理程序应遵循 “object_event” 的命名约定。

6.5 事件具有以下特点:

( 1 )发行者确定何时引发事件,订户确定执行何种操作来响应该事件。

( 2 )一个事件可以有多个订户。 一个订户可处理来自多个发行者的多个事件。

( 3 )没有订户的事件永远也不会引发。

( 4 )事件通常用于通知用户操作,例如,图形用户界面中的按钮单击或菜单选择操作。

( 5 )如果一个事件有多个订户,当引发该事件时,会同步调用多个事件处理程序。 要异步调用事件,请参见 使用异步方式调用同步方法

( 6 )在 .NET Framework 类库中,事件是基于 EventHandler 委托和 EventArgs 基类的。

6.6 事件的 创建步骤

(1) 、定义 delegate 对象类型,它有两个参数,第一个参数是事件发送者对象,第二个参数是事件参数类对象。 (2) 、定义事件参数类,此类应当从 System.EventArgs 类派生。如果事件不带参数,这一步可以省略。 (3) 、定义 " 事件处理方法,它应当与 delegate 对象具有相同的参数和返回值类型 " 。 (4) 、用 event 关键字定义事件对象,它同时也是一个 delegate 对象。 (5) 、用 += 操作符添加事件到事件队列中( -= 操作符能够将事件从队列中删除)。 (6) 、在需要触发事件的地方用调用 delegate 的方式写事件触发方法。一般来说,此方法应为 protected 访问限制,既 不能以 public 方式调用,但可以被子类继承。名字是 OnEventName 。 (7) 、在适当的地方调用事件触发方法触发事件。