事件
事件含义
事件由对象引发,通过我们提供的代码来处理。一个事件我们必须订阅(Subscribe)他们,订阅一个事件的含义就是提供代码,在这个事件发生时执行这些代码,这些代码称为事件处理程序。
一个事件可以被多个事件处理程序订阅,在这个事件发生时,这些处理程序都会被执行。事件处理程序可以在该事件的对象所处的类中,也可以在其他类中。
事件处理程序本身就是一个普通的方法,对这个方法的唯一限制是:必须匹配事件所要求的返回类型和参数,这个限制是事件定义的一部分,由一个委托指定。
处理事件
要处理事件,需要提供一个事件处理方法来订阅事件,方法的返回类型和参数必须匹配事件指定的委托。下面使用一个简单的计时器对象来引发事件,调用一个处理方法。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Timers;
namespace eve
{
class Program
{
static int counter = 0;
static string displayString =
"This string will appear one letter at a time. ";
static void Main(string[] args)
{
Timer myTimer = new Timer(100);
myTimer.Elapsed += WriteChar;
// myTimer.Elapsed += WriteChar2;
//myTimer.Elapsed += new ElapsedEventHandler(WriteChar);
// myTimer.Elapsed += (sender, eventArgs) =>
// {
// Console.Write(displayString[counter++ % displayString.Length]);
// };
myTimer.Start();
System.Threading.Thread.Sleep(2000);
Console.ReadKey();
}
static void WriteChar(object source, ElapsedEventArgs e)
{
Console.Write(displayString[counter++ % displayString.Length]);
}
static void WriteChar2(object source, ElapsedEventArgs e)
{
Console.Write(counter);
}
}
}
这个示例运行后将会打印字符,一个一个打印。那么我来解释一下上面的栗子。
Timer 是一个定时器,每隔一段时间会触发一个事件,这个时间在构造函数里给出,这里是100ms。要引发事件,首先要把这个定时器跑起来,就是
myTimer.Start();
这个Timer呢有一个事件叫做Elapsed
,我们就要用一个方法去订阅它。条件就是必须匹配System.Timers.ElapsedEventHandler
这个委托类型的返回类型和参数。这个委托来自.Net Framework
标准委托。它指定了如下的返回类型和参数。
void
(Object source, ElapsedEventArgs e);
其中的source
参数是Timer对象本身的引用,ElapsedEventArgs
是对象的一个实例。后面继续介绍。
在上面给出的代码中有满足这个委托返回类型和参数列表的方法static void WriteChar(object source, ElapsedEventArgs e) {
Console.Write(displayString[counter++ % displayString.Length]);
}
先不管后面那个WriteChar2。 里面的方法在屏幕上一次输出字符串中的字符。 好了,事件有了,对应的方法有了,剩下的工作就是去订阅这个事件。 使用`+=`运算符,给事件添加一个处理程序,形式是使用事件处理方法初始化一个新的**委托实例**。但是方法不止一种: 1. `myTimer.Elapsed += new ElapsedEventHandler(WriteChar);` 2. `Timer.Elapsed += WriteChar;` 上面两个方式都是可以的,使用第2种编译器会根据使用的上下文来指定委托类型。坏处就是降低可读性,你不能一下就知道这个委托类型是什么。 其实还有方法: **Lambda表达式**
myTimer.Elapsed += (sender, eventArgs) =>
{
Console.Write(displayString[counter++ % displayString.Length]);
};或者 **匿名方法**
myTimer.Elapsed += delegate(object sender, MessageArrivedEventArgs eventArgs)
{
Console.Write(displayString[counter++ % displayString.Length]);
};上面的`(object sender, MessageArrivedEventArgs eventArgs)`也是可以省略的。 至此,所有的工作都完成啦。 再理一遍:定时器每隔100ms引发一个事件,这个事件被我们的方法订阅,事件触发后去找那个订阅的处理程序,执行那个方法,在屏幕上输出内容。
创建事件
上面使用了Timer自带的事件,我们也可以创建我们自己的事件。这里是一个即时消息传送程序,创建一个Connection
对象,这个对象引发由Display
对象处理的事件。
首先定义一个委托类型,你一定知道用它干嘛。
public delegate void MessageHandler(string messageText);
这个委托类型称为MessageHandler,有一个
void
的方法签名,有一个string
参数。
在Connection
这个类里定义一个事件
public event MessageHandler MessageArrived
给事件命名,这里使用
MessageArrived
,在声明时,使用event
关键字,并指定要使用的委托类型MessageHandler
。这样声明以后,就可以引发它。方法是按名称来调用它。例如:
MessageArrived("This is a message.");
为什么是这样呢?
MessageArrived
这是一个委托实例,按照委托的方法传参。
要是定义的委托不需要参数,那就这样MessageArrived();
要是参数多就要用更多的代码区实现。
先上代码吧。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Timers;
namespace eve
{
public delegate void MessageHandler(string messageText);
public class Connection
{
public event MessageHandler MessageArrived;
private Timer pollTimer;
public Connection()
{
pollTimer = new Timer(100);
pollTimer.Elapsed += new ElapsedEventHandler(CheckForMessage);
}
public void Connect()
{
pollTimer.Start();
}
public void Disconnect()
{
pollTimer.Stop();
}
private static Random random = new Random();
private void CheckForMessage(object source, ElapsedEventArgs e)
{
Console.WriteLine("Checking for new messages.");
if ((random.Next(9) == 0) && (MessageArrived != null))
{
MessageArrived("Hello Mum!");
}
}
}
}
if ((random.Next(9) == 0) && (MessageArrived != null))
这里得到一个随机数,得到0时引发事件,后面的MessageArrived!=null
检查事件是否有被订阅,如果没有被订阅那么MessageArrived=null
不会引发事件。
下面是Display
类
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace eve
{
public class Display
{
public void DisplayMessage(string message)
{
Console.WriteLine("Message arrived: {0}", message);
}
}
}
其中的
public void DisplayMessage(string message)
满足上面定义的委托返回类型和参数,可以用它来订阅事件MessageArrived
主程序是这样的
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace eve
{
class Program
{
static void Main(string[] args)
{
Connection myConnection = new Connection();
Display myDisplay = new Display();
myConnection.MessageArrived +=
new MessageHandler(myDisplay.DisplayMessage);
myConnection.Connect();
System.Threading.Thread.Sleep(200);
Console.ReadKey();
}
}
}
下面是运行结果
多用途的事件处理程序
前面使用的Timer.Elapsed
事件的委托包含了事件处理程序中常见的两类参数
-
object source
引发事件的对象的引用 -
ElapsedEventArgs e
由事件传送的参数
在事件中使用object
类型的原因是,我们常常要为由不同对象引发的几个相同事件使用同一个事件处理程序,但仍要指定是哪个对象生成了事件。
下面扩展上面的示例。
添加一个新类MessageArrivedEventArgs
public class MessageArrivedEventArgs : EventArgs
{
private string message;
public string Message
{
get
{
return message;
}
}
public MessageArrivedEventArgs()
{
message = "No message sent.";
}
public MessageArrivedEventArgs(string newMessage)
{
message = newMessage;
}
}
修改Connection
类
public class Connection
{
public event EventHandler MessageArrived;
private Timer pollTimer;
public string Name { get; set; }
public Connection()
{
pollTimer = new Timer(100);
pollTimer.Elapsed += new ElapsedEventHandler(CheckForMessage);
}
public void Connect()
{
pollTimer.Start();
}
public void Disconnect()
{
pollTimer.Stop();
}
private static Random random = new Random();
private void CheckForMessage(object source, ElapsedEventArgs e)
{
Console.WriteLine("Checking for new messages.");
if ((random.Next(9) == 0) && (MessageArrived != null))
{
MessageArrived(this, new MessageArrivedEventArgs("Hello Mum!"));
}
}
}
修改Display
类
public class Display
{
public void DisplayMessage(object source, MessageArrivedEventArgs e)
{
Console.WriteLine("Message arrived from: {0}",
((Connection)source).Name);
Console.WriteLine("Message Text: {0}", e.Message);
}
}
主程序修改为
static void Main(string[] args)
{
Connection myConnection1 = new Connection();
myConnection1.Name = "First connection.";
Connection myConnection2 = new Connection();
myConnection2.Name = "Second connection.";
Display myDisplay = new Display();
myConnection1.MessageArrived += myDisplay.DisplayMessage;
myConnection2.MessageArrived += myDisplay.DisplayMessage;
myConnection1.Connect();
myConnection2.Connect();
System.Threading.Thread.Sleep(200);
Console.ReadKey();
}
发送一个引发事件的对象引用,把这个对象作为事件处理程序的一个参数,就可以为不同的对象定制处理程序的响应。可以利用这个对象访问源对象,包括它的属性。
通过发送包含在派生于System.EventArgs
类中的参数,就可以将其他信息作为参数提供。这些参数也得益于多态性。为MessageArrived
事件定义一个处理程序:
public void DisplayMessage(object source, MessageArrivedEventArgs e)
{
Console.WriteLine("Message arrived from: {0}",
((Connection)source).Name);
Console.WriteLine("Message Text: {0}", e.Message);
}
这个处理程序可以处理不限于Timers.Elapsed
的事件。但是要修改里面的代码,并注意检查null
值。
EventHandler
和泛型EventHandler
类型
这里有一个规范,事件处理程序的返回类型应为void
,参数应该是两个,第一个是object
,第二个参数是派生于System.EventArgs
。为此.Net
提供了两个委托类型EventHandler
和EventHandle
,以便定义事件。上面的例子中,删去了自己定义的委托转而使用EventHandle
泛型委托。
public event EventHandler
上面提到的两个委托类型EventHandler
和EventHandle
,他们的返回类型都是void
,参数列表
public delegate void EventHandler(object sender, System.EventArgs e)
public delegate void EventHandler(object sender, TEventArgs e)
这显然简化了我们的代码。一般,在定义事件时,最好使用这些委托类型。如果事件不需要事件实参数据,仍然可以用EventHandler
委托类型,但是要传递EventArgs.Empty
作为实参。
也可以为事件提供返回类型,但这有一个问题。引发给定的事件,可能会调用多个事件处理程序。如果这些处理程序都返回一个值,那么我们只能得到最后一个订阅该事件的处理程序的返回值。有些情况下可能是有用的,但最好使用void
作为返回类型。
匿名方法
前面提到过匿名方法,是为用作委托目的而创建的。要创建匿名方法,使用以下代码:
delegate(parameters){
//具体代码
}
其中
parameters
是一个参数列表,这些参数列表匹配正在实例化的委托类型,由匿名方法使用,例如:
delegate(Connection source, MessageArrivedEventArgs e){
> Console.WriteLine("Message arrived from: {0}",
((Connection)source).Name);
Console.WriteLine("Message Text: {0}", e.Message);
> };
使用下面的代码可以完全绕过Display.DisplayMessage()
方法:
myConnection1.MessageArrived += delegate(Connection source, MessageArrivedEventArgs e){
Console.WriteLine("Message arrived from: {0}",
((Connection)source).Name);
Console.WriteLine("Message Text: {0}", e.Message);
}
完
示例代码和部分内容来自《C#入门经典(第六版)》