本节导读:
本节说明了如何使用EventAggregator提供模块间的通信。
2012-2-3
9.4 事件聚合器
Prism提供了事件机制以拿模块间的松耦合交流成为可能。在这个机制中,基于事件聚合服务,订阅者和发布者可以在不互相建立引用的情况下进行通信。
EventAggregator提供了多路广播发布者/订阅者的功能。也就是说多个发布者可以发布同一个事件或者同一个事件可以被多个订阅者所监听。考虑使用EventAggregator发布事件以确保消息可以在如控制器或者表现者之类的业务逻辑代码间发送。
举例而言,在the Stock Trader RI中,当Process Order按钮被按下并且订单成功处理;在这种情况下,其它模块需要知道订单已经被成功提交以使它们可以更新自己的View。
由Prism创建是事件是强类型事件,也就是说编译时类型检查会应用在事件上这使得程序可以在运行之前发现错误。在Prism中,EventAggregator允许订阅者或者发布者指定一个特定的EventBase。事件聚合器适用于多个发布者和多个订阅者,如下图所示。
【注意】:关于.NET Framework事件
使用.NET Framework的事件是在不需要松耦合的情况下跨模块通信最简便和最直接的方法。.NET Framework的事件实现了发布者-订阅者模式,但是需要订阅一个对象的事件就需要直接引用这个对象,而在复杂程序中,这个对象往往在其它模块里。这将会导致一个紧耦合的设计。因此,.NET Framework的事件被用于模块内交流而不是模块间的交流。
当使用.NET Framework的事件时要非常小心内存泄漏,特别时当静态类或者生命周期长的组件订阅了非静态类或者生命周期短的组件的事件时。如果订阅者不取消订阅,那么发布者就一直保持有效状态,从而导致垃圾回收机制无法回收发布者。
9.4.1 IEventAggregator
EventAggregator在容器中提供了一个可以通过IEventAggregator接口检索的服务。事件聚合器负责收集和定位事件并且在系统中保存一个事件集合。
public interface IEventAggregator
{
TEventType GetEvent() where TEventType : EventBase;
}
在EventAggregator没有构造时,如果被访问则构造一个事件。这个操作会使事件发布者和订阅者从需要监视的状态中移除,该操作无论事件是否可用。([This 代词,指EventAggregator] [relieves < the publisher or subscriber 发布者或者订阅者> from
9.4.2 CompositePresentationEvent
连接发布者和订阅者过程的实际操作者是CompositePresentationEvent类。它是在Prism中唯一的EventBase的实现。该类维护订阅者列表并且将事件发布到订阅者手中。
CompositePresentationEvent是一个泛类,作为泛类,定义时就需要一个负载类型。这有助于在编译时检查订阅者和发布者之间提供了正确的方法来保证事件的连接。以下代码是CompositePresentationEvent的一部分定义。
public class CompositePresentationEvent: EventBase
{
...
public SubscriptionToken Subscribe(Actionaction);
public SubscriptionToken Subscribe(Actionaction, ThreadOption threadOption);
public SubscriptionToken Subscribe(Actionaction, bool keepSubscriberReferenceAlive)
public virtual SubscriptionToken Subscribe(Actionaction, ThreadOption threadOption, bool keepSubscriberReferenceAlive);
public virtual SubscriptionToken Subscribe(Actionaction, ThreadOption threadOption, bool keepSubscriberReferenceAlive, Predicate filter);
public virtual void Publish(TPayload payload);
public virtual void Unsubscribe(Actionsubscriber);
public virtual bool Contains(Actionsubscriber)
...
}
9.4.3 创建并且发布事件
下文描述了如何通过IEventAggregator接口创建,发布,和订阅CompositePresentationEvent。
1)创建事件
CompositePresentationEvent
比如,以下代码是the Stock Trader Reference Implementation (Stock Trader RI)中的TickerSymbolSelectedEvent。其中负载内容就是包含Company Symbol的字符串。注意,该类的实现是空的。
public class TickerSymbolSelectedEvent : CompositePresentationEvent<string>{}
【注意】:在复杂项目中,事件需要在多个模块中频繁使用,所以将它们定义在共用的地方。在the Stock Trader RI中,定义在StockTraderRI.Infrastructure(译注,Infrastructure是基础结构的意思)项目里。
2)发布事件
发布者通过EventAggregator查到事件并且调用Publish方法发布。在构造函数中加入一个IEventAggregator类型的参数并且使用依赖注入就可以访问EventAggregator了。
下面的例子说明了发布TickerSymbolSelectedEvent的方法。
this.eventAggregator.GetEvent().Publish("STOCK0");
9.4.3 订阅事件
订阅者通过使用CompositePresentationEvent类中提供的Subscribe方法的某个可用重载订阅事件。订阅CompositePresentationEvents有许多方法,下面的标准可以帮助您决定。
l 如果事件触发时需要更新UI元素,那就在UI线程中订阅。
l 如果需要过滤事件,在订阅时提供一个筛选的委托。
l 如果需要关注事件的性能,在订阅时使用强委托引用并且之后再手动的取消订阅CompositePresentationEvent。
l 如果上述情况都不适用,那就使用默认配置。
下文描述了这些情况。
1)在UI线程中订阅
通常,订阅者在响应事件时需要更新在UI元素。在WPF和Silverlight中,只有UI线程才能更新UI元素。
默认情况下,订阅者在发布者的线程中收到事件。如果发布者在UI线程中发送事件,那订阅者就可以更新UI元素。然而,如果发布者的线程是一个后台线程,那订阅者就不能直接更新UI元素了。在这种情况下,订阅者就需要使用Dispatcher类以按预定时间更新UI元素。
Prism提供的CompositePresentationEvent类帮助订阅者自动在UI线程中接收事件。订阅者只要在订阅时说明这个情况就行了。
public void Run()
{
...
this.eventAggregator.GetEvent().Subscribe(ShowNews, ThreadOption.UIThread);
);
}
public void ShowNews(string companySymbol)
{
this.articlePresentationModel.SetTickerSymbol(companySymbol);
}
ThreadOption提供了以下选项:
l PublisherThread。该设定使事件在发布者的线程中接收,这是默认设置。
l BackgroundThread。该设定则会在.NET Framework线程池中线程上异步接收事件。
l UIThread。该设定会在UI线程上接收事件。
2)订阅的筛选
订阅者可能不需要处理已发布事件的所有对象。在这种情况下,订阅者可以使用filter参数。filter是System.Predicate
通常,筛选器都由一个Lambda表达式提供,如下所示。
FundAddedEvent fundAddedEvent = this.eventAggregator.GetEvent();
fundAddedEvent.Subscribe(FundAddedEventHandler, ThreadOption.UIThread, false,
fundOrder => fundOrder.CustomerId == this.customerId);
【注意】:Silverlight不支持弱引用的Lambda表达式或者匿名委托。
在Silverlight中,就需要调用一个独立的公用方法,如下所示。
public bool FundOrderFilter(FundOrder fundOrder)
{
return fundOrder.CustomerId == this.customerId;
}
...
FundAddedEvent fundAddedEvent = this.eventAggregator.GetEvent();
subscriptionToken = fundAddedEvent.Subscribe(FundAddedEventHandler, ThreadOption.UIThread, false, FundOrderFilter);
【注意】:Subscribe返回一个类型为Microsoft.Practices.Prism.Events.SubscriptionToken的订阅令牌,该令牌可以在之后提供移除事件订阅的功能。该令牌在回调函数为匿名委托或者Lambda表达式或者通过不同筛选器订阅同一个事件是非常有用。
【注意】:在事件回调函数中不建议修改负载对象因为会有多个线程同时文章负载对象。您必须保证负载对象不变以避免并发错误。
3)通过强委托引用订阅
如果在短时间内触发多个需要注意性能的事件,可能需要将它们以强委托引用的方式订阅。以这种方式订阅的事件需要在释放订阅者的时候手动取消订阅事件。
在默认情况下,CompositePresentationEvent在订阅时维持着订阅者和筛选器的弱委托引用。也就是说CompositePresentationEvent中的引用不会阻止垃圾回收机制回收订阅者。使用弱委托引用使订阅者不需要解除订阅,也允许垃圾回收。
当然,维护一个弱委托引用比一般的强引用要慢一些。在大多数应用程序中,性能不需要特别注意,但是如果应用程序在短时间内发布了大量事件,就需要在CompositePresentationEvent中使用强引用。如果使用了强委托引用,那么在订阅对象不需要使用的时候需要反订阅事件以使本身的垃圾回收可以作用于订阅对象。
要订阅强引用事件,在Subscribe方法中使用keepSubscriberReferenceAlive参数,如下所示。
FundAddedEvent fundAddedEvent = eventAggregator.GetEvent();
bool keepSubscriberReferenceAlive = true;
fundAddedEvent.Subscribe(FundAddedEventHandler, ThreadOption.UIThread, keepSubscriberReferenceAlive, fundOrder => fundOrder.CustomerId == _customerId);
keepSubscriberReferenceAlive参数是bool类型的。
l 当设置为true时,事件实例就会保持对订阅者的强引用,从而使订阅者不会被回收。关于如何取消订阅,参见稍后提及的“取消订阅事件”一节。
l 当设置为false(这是参数省略时的默认值),事件就维持订阅者对象的弱引用,这样允许垃圾回收在订阅者对象不再被其它对象引用时自动翻译对象。当订阅者被回收时,事件自动取消订阅。
4)默认订阅
在使用默认值订阅时,订阅者必需提供一个拥有正确函数原型的回调方法以接收事件通知。比如,TickerSymbolSelectedEvent的处理者就需要有一个字符串型的参数,如下所示。
public void Run()
{
...
this.eventAggregator.GetEvent().Subscribe(ShowNews);
}
public void ShowNews(string companySymbol)
{
articlePresentationModel.SetTickerSymbol(companySymbol);
}
5)取消订阅事件
如果订阅者不再需要接收事件时,可以使用订阅者或者订阅信息取消订阅。
下面的例子说明了如何直接取消订阅。
FundAddedEvent fundAddedEvent = this.eventAggregator.GetEvent();
fundAddedEvent.Subscribe(FundAddedEventHandler, ThreadOption.PublisherThread);
fundAddedEvent.Unsubscribe(FundAddedEventHandler);
下面的例子说明了如何使用订阅令牌取消订阅。这个令牌是由Subscribe方法返回的。
FundAddedEvent fundAddedEvent = this.eventAggregator.GetEvent();
subscriptionToken = fundAddedEvent.Subscribe(FundAddedEventHandler, ThreadOption.UIThread, false, fundOrder => fundOrder.CustomerId == this.customerId);
fundAddedEvent.Unsubscribe(subscriptionToken);