事件访问器
为了挂钩订阅者到发布者,可以直接访问发布者的事件成员变量。但以公共的方法暴露类成员会带来一些麻烦。它违反了封装性和信息隐藏的核心面向对象设计原则,并且使所有订阅者与具体的成员变量定义相耦合。为了减轻这个问题,C#提供了一个类似于属性的机制,称为“event accessor(事件访问器)”。访问器提供类似于属性的优点,在保持原有易用性的同时隐藏实际的类成员。C#使用add和remove——分别执行+=和-=操作符函数,从而封装了事件成员变量:
using MyEventHandler = GenericEventHandler<string>;
public class MyPublisher
{
MyEventHandler m_MyEvent;
public event MyEventHandler MyEvent
{
add
{
m_MyEvent += value;
}
remove
{
m_MyEvent -= value;
}
}
public void FireEvent()
{
EventsHelper.Fire(m_MyEvent, "Hello");
}
}
public class MySubscriber
{
public void OnEvent(string message)
{
MessageBox.Show(message);
}
}
//客户端代码
MyPublisher publisher = new MyPublisher();
MySubscriber subscriber = new MySubscriber();
publisher.MyEvent += subscriber.OnEvent;
publisher.FireEvent();
publisher.MyEvent -= subscriber.OnEvent;
管理大数量事件
想像一下,当开发框架时会常常遇到发布大量事件的类。例如,在System.Windows.Forms命名空间下的Control类有数十个事件对应多个窗口消息。处理如此数目众多的事件带来的问题是为每个事件分配一个类成员是不切实际的:类定义,文档,CASE工具图解,甚至智能感知都会变得难以管理。为解决这个窘境,.NET提供了EventHandlerList类(System.ComponentModel命名空间下):
public sealed class EventHandlerList : IDisposable
{
public EventHandlerList();
public Delegate this[object key] { get; set;}
public void AddHandler(object key, Delegate value);
public void AddHandlers(EventHandlerList listToAddFrom);
public void RemoveHandler(object key, Delegate value);
public virtual void Dispose();
}
EventHandler是一个存储键/值对的线性列表。键是一个惟一标识事件的对象,值则是System.Delegate的一个实例。由于索引是一个object,所以它可以是一个整数索引,或是一个字符串,一个具体的Button实例等等。分别使用AddHandler和RemoveHandler来新增和移除单独的事件处理方法。也可以使用AddHandlers方法添加一个已存在的EventHandlerList的内容。为了激发事件,就需要使用具有键对象的索引来访问事件列表,得到一个System.Delegate对象。赋值到实际事件委托并激发对象。
下面的例子展示了当要实现一个类似于Windows Froms的按钮类的MyButton类时,如何使用EventHandlerList。该按钮支持很多事件,如鼠标点击和鼠标移动,都导向到同一事件列表。使用事件访问器做到对于客户端的完全封装:
using ClickEventHandler=GenericEventHandler<MyButton, EventArgs>;
using MouseEventHandler=GenericEventHandler<MyButton, MouseEventArgs>;
public class MyButton
{
EventHandlerList m_EventList;
public MyButton()
{
m_EventList = new EventHandlerList();
/* 其余初始化工作 */
}
public event ClickEventHandler Click
{
add
{
m_EventList.AddHandler("Click", value);
}
remove
{
m_EventList.RemoveHandler("Click", value);
}
}
public event MouseEventHandler MouseMove
{
add
{
m_EventList.AddHandler("MouseMove", value);
}
remove
{
m_EventList.RemoveHandler("MouseMove", value);
}
}
void FireClick()
{
ClickEventHandler handler = m_EventList["Click"] as ClickEventHandler;
EventsHelper.Fire(handler, this, EventArgs.Empty);
}
void FireMouseMove(MouseButtons button, int clicks, int x, int y, int delta)
{
MouseEventHandler handler = m_EventList["MouseMove"] as MouseEventHandler;
MouseEventArgs args = new MouseEventArgs(button, clicks, x, y, delta);
EventsHelper.Fire(handler, this, args);
}
/* 其他方法和事件定义 */
}
上面代码存在的问题是例如鼠标移动或鼠标点击事件过于频繁地被提出,并且创建一个新字符串作为键,这样的每次调用对托管堆增加了压力。一个更好的方法是使用实现安排好的静态键集,在所有实例中共享它们:
public class MyButton
{
EventHandlerList m_EventList;
static object m_MouseMoveEventKey = new object();
public event MouseEventHandler MouseMove
{
add
{
m_EventList.AddHandler(m_MouseMoveEventKey, value);
}
remove
{
m_EventList.RemoveHandler(m_MouseMoveEventKey, value);
}
}
void FireMouseMove(MouseButtons button, int clicks, int x, int y, int delta)
{
MouseEventHandler handler = m_EventList[m_MouseMoveEventKey] as MouseEventHandler;
MouseEventArgs args = new MouseEventArgs(button, clicks, x, y, delta);
EventsHelper.Fire(handler, this, args);
}
/* 其他方法和事件定义 */
}
编写接收接口
通过隐藏实际的事件成员,事件访问器仅仅满足了封装性的要求。对于该模型还可以继续改进。举例说明,考虑一个订阅者希望订阅一组事件的情况。在这种情况下,为什么要使用多种潜在的高消耗的调用来建立和解除发布者和订阅者之间的连接? 为什么订阅者需要事先知道事件访问器的信息?如果订阅者想要接收接口上的事件,而不是单独的方法应怎样?下面提供一种简单但通用的方式来管理发布者和订阅者之间的连接——可以保存冗余的调用,封装事件访问器和成员,并允许接收接口。现在考虑定义了一组事件的接口,IMySubcriber接口:
public interface IMySubscriber
{
void OnEvent1(object sender, EventArgs eventArgs);
void OnEvent2(object sender, EventArgs eventArgs);
void OnEvent3(object sender, EventArgs eventArgs);
}
谁都可以实现该接口,并且该接口应当被发布者完全获知:
public class MySubscriber : IMySubscriber
{
#region IMySubscriber 成员
public void OnEvent1(object sender, EventArgs eventArgs)
{...}
public void OnEvent2(object sender, EventArgs eventArgs)
{...}
public void OnEvent3(object sender, EventArgs eventArgs)
{...}
#endregion
}
下面定义一个事件的枚举类型,并使用Flags属性标记该枚举类型。Flags属性表明该枚举类型的值可以进行位屏蔽(注意EventType.OnAllEvents的定义)。这就允许使用|(OR)位操作符组合不同的枚举值或者使用&(AND)操作符屏蔽它们:
[Flags]
public enum EventType
{
OnEvent1,
OnEvent2,
OnEvent3,
OnAllEvent=OnEvent1|OnEvent2|OnEvent3
}
发布者提供了两个方法,Subscribe()和Unsubscribe(),每个都接收两个参数:接口和一个位屏蔽标记来指明要订阅到接收接口的事件。在内部,发布者可以对于接收接口上的每个方法拥有一个事件委托成员,或者对于所有方法使用一个(这属于实现细节,对于订阅者隐藏)。下面示例代码中对于接收接口中的每个方法使用了一个事件成员变量。也展示了具有错误处理功能的Subscribe(),UnSubscribe()和FireEvent()方法:
public class MyPublisher
{
MyEventHandler m_Event1;
MyEventHandler m_Event2;
MyEventHandler m_Event3;
public void Subscribe(IMySubscriber subscriber, EventType eventType)
{
if ((eventType & EventType.OnEvent1) == EventType.OnEvent1)
{
m_Event1 += subscriber.OnEvent1;
}
if ((eventType & EventType.OnEvent2) == EventType.OnEvent2)
{
m_Event2 += subscriber.OnEvent2;
}
if ((eventType & EventType.OnEvent3) == EventType.OnEvent3)
{
m_Event3 += subscriber.OnEvent3;
}
}
public void Unsubscribe(IMySubscriber subscriber, EventType eventType)
{
if ((eventType & EventType.OnEvent1) == EventType.OnEvent1)
{
m_Event1 -= subscriber.OnEvent1;
}
if ((eventType & EventType.OnEvent2) == EventType.OnEvent2)
{
m_Event2 -= subscriber.OnEvent2;
}
if ((eventType & EventType.OnEvent3) == EventType.OnEvent3)
{
m_Event3 -= subscriber.OnEvent3;
}
}
public void FireEvent(EventType eventType)
{
if ((eventType & EventType.OnEvent1) == EventType.OnEvent1)
{
EventsHelper.Fire<object, EventArgs>(m_Event1, this, EventArgs.Empty);
}
if ((eventType & EventType.OnEvent2) == EventType.OnEvent2)
{
EventsHelper.Fire<object, EventArgs>(m_Event2, this, EventArgs.Empty);
}
if ((eventType & EventType.OnEvent3) == EventType.OnEvent3)
{
EventsHelper.Fire<object, EventArgs>(m_Event3, this, EventArgs.Empty);
}
}
}
订阅和撤销订阅的代码要求是一样的,这样就可以优雅地使用一个调用来接受整个接口,同时完全封装了实际的事件类成员:
MyPublisher publisher = new MyPublisher();
IMySubscriber subscriber = new MySubscriber();
//订阅到事件1和2
publisher.Subscribe(subscriber, EventType.OnEvent1 | EventType.OnEvent2);
//只激发事件1
publisher.FireEvent(EventType.OnEvent1);
根据原版英文翻译,所以不足和错误之处请大家不吝指正,谢谢:)