终于说到Event了。阅读本篇之前,请参阅我的另一篇关于事件的文章:CLR笔记:10.事件
Prism自带的示例与MVP模式的耦合性太大了,以至于看不出Prism框架中独特的Event机制。于是,我自己写了一个超级简单的Sample,以飨读者。
示例代码下载:code.zip
事件的实现很简单,以下是傻瓜化Step by Step:
1. 在公共类库中定义事件AddNotamsEvent :
public class AddNotamsEvent : CompositePresentationEvent<NotamsInfo> { }
其中,NotamsInfo为一个自定义的实体:
public class NotamsInfo
{
public string IATA
{
get;
set;
}
public string ICAO
{
get;
set;
}
}
当然也可以直接使用简单类型,这样就不用自定义实体了:
public class AddAircraftEvent : CompositePresentationEvent<int>
2. 在事件发布方:
News news = new News() {
Title = "Bao's demo was published.",
Content = "This message is wonderful."
};
eventAggregator.GetEvent<NewsAddedEvent>().Publish(news);
3. 在事件订阅方:
this.eventAggregator.GetEvent<NewsAddedEvent>().Subscribe(ShowMessage);
这里,ShowMessage是一个具有News类型参数的方法:
public void ShowMessage(News news)
{
tbSimpleParam.Text = news.Title;
}
这样,一个完整的Prism事件机制就完成了。
让我们回过头来,看一下IEventAggregator,它有一个常用的方法GetEvent<T>,用来获取注册到其中的事件T。
对于事件的订阅,更正规的写法如下所示:
StandardMessageAddedEvent messageAddedEvent = eventAggregator.GetEvent<StandardMessageAddedEvent>();
if (subscriptionToken != null)
{
messageAddedEvent.Unsubscribe(subscriptionToken);
}
subscriptionToken = messageAddedEvent.Subscribe(ShowMessage2, ThreadOption.UIThread, true, Filter);
同时,在ShowMessage2方法中,补充一段代码,用来取消事件的调阅:
public void ShowMessage2(string text)
{
tbSimpleParam.Text = text;
StandardMessageAddedEvent messageAddedEvent = eventAggregator.GetEvent<StandardMessageAddedEvent>();
if (subscriptionToken != null)
{
messageAddedEvent.Unsubscribe(subscriptionToken);
}
}
在上面的代码中,我们看到这样的顺序:
1. 先从IEventAggregator中获取注册到其中的事件对象,也就是messageAddedEvent:
StandardMessageAddedEvent messageAddedEvent = eventAggregator.GetEvent<StandardMessageAddedEvent>();
2. 这个messageAddedEvent对象具有Subscribe方法,它返回一个SubscriptionToken类型的对象,用来标志事件触发后,在订阅一方所调用的方法:
subscriptionToken = messageAddedEvent.Subscribe(ShowMessage2, ThreadOption.UIThread, true, Filter);
3. 但是在第2步之前,我们要检查SubscriptionToken对象是否为空,否则就要注销之前的messageAddedEventHandler方法:
if (subscriptionToken != null)
{
messageAddedEvent.Unsubscribe(subscriptionToken);
}
之所以这么写,是因为我实在不放心弱引用后的垃圾自动回收时间,于是,我将Subscribe方法的第3个参数设置为了true(默认为false,弱引用,垃圾自动回收),这样,订阅的就是强引用了,Prism就会要求我们必须手动取消事件的订阅。
在上面的Subscribe方法中,我们看到它有4个参数,这是它最完整的一个重载,签名如下所示:
public virtual SubscriptionToken Subscribe(Action<TPayload> action, ThreadOption threadOption, bool keepSubscriberReferenceAlive, Predicate<TPayload> filter)
其中,第一个参数就是订阅一方所要调用的方法,在上面的例子中为ShowMessage2。
第二个参数是ThreadOption枚举,相应说明见注释:
public enum ThreadOption
{
// 在Publisher所在的线程上执行,默认值
PublisherThread,
/// 在UI线程上触发
UIThread,
///在后台线程上异步调用
BackgroundThread
}
其中,PublisherThread表示在Publisher所在的线程上执行,这是一个默认值。
UIThread线程表示在UI线程上执行,即调用wpf Dispatcher的BeginInvoke方法,如下所示:
Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Normal, method, arg);
BackgroundThread表示在后台线程上异步调用,即通过BackgroundWorker类来异步操作
public override void InvokeAction(Action<TPayload> action, TPayload argument)
{
BackgroundWorker worker = new BackgroundWorker();
worker.DoWork += ((sender, e) => action((TPayload)e.Argument));
worker.RunWorkerAsync(argument);
}
关于这3个枚举的区别,我也写了一个Demo,请大家根据我的解释去理解(暂付阙如)。
第三个参数是一个bool值,true表示强引用,要手动取消事件的订阅;false(默认值)表示弱引用,会自动进行垃圾回收。我们在前面有介绍过。
第四个参数是一个bool类型的委托,它以事件传递的消息类型作为参数,我们观察Demo中的代码:
subscriptionToken = messageAddedEvent.Subscribe(ShowMessage2, ThreadOption.UIThread, true, Filter);
public bool Filter(string text)
{
return true;
//return false;
}
使用F5一步步调试,会发现Filter方法是在ShowMessage2方法之前执行的,而且,Filter方法返回true,才会继续执行ShowMessage2方法。于是,我们可以根据这个时间差,来完成一些高难度动作。看下面这个例子,点击Add按钮前后的效果如下图所示:
没错,很像Prism自带的那个Demo,位于C:"Users"baoj"Desktop"PRISM"Quickstarts"EventAggregation,
但是这个例子太复杂了,正如我前面提到的那样,和MVP搞在了一起,于是我对其进行了简化,包包版的示例下载:code.zip
这个例子呢,根据我们选择的是Customer1还是Customer2,来决定文字在Customer1VIew中显示,还是在Customer2VIew中显示。
对,这就是Filter的用武之处了。2个View,即Customer1View和Customer2View,都订阅了同一个事件源。于是,当该事件发布时,2个View都会被触发,根据事件所带的参数是Customer1还是Customer2,来决定是否在自身View中显示。核心语法就是那个Filter方法了,如下所示:
public bool FundOrderFilter(FundOrder fundOrder)
{
return fundOrder.CustomerId == customerId;
}
但是,在实际项目中,MVP模式和EventAggregator技术往往是同时出现的,这才显示出Prism框架的强大。所以,还是建议大家参考Prism自带的Demo。
那,为什么不使用.NET Framework自带的事件机制呢?我们知道,在.NET基本事件模型中,我们要手动创建一个事件管理器,来负责事件的订阅和发布。而在Prism中,因为依赖注入的引进,我们不再需要手动创建这个管理器了,Prism框架会为我们自动创建管理器的一个实例,也就是EventAggregator,它位于另一个Module中,当然这个Module是Prism框架自带的了,还记得UnityBootstrapper的ConfigureContainer方法么,对,就是在这里进行注册的。
那那那,如果我不想传递参数呢?我就是想点击ViewA的按钮,然后ViewB就disabled了。够BT的需求吧。你还别说,Prism框架还真不能提供这样一套不传递参数的机制。否则,你也Publish这样的事件,我也Publish这样的事件,但你我相应的订阅机制不同,可是Prism分不清啊,于是这个世界就乱套了。
别说Prism设计不出这样的事件机制,就连.NET事件机制也做不到,只能采取折中的办法,比如说Button的Click事件:
private void button1_Click(object sender, EventArgs e)
其中,在点击按钮的同时,会把这个按钮对象作为参数sender传递进去,于是,每个按钮的点击事件就区别开了。
照猫画虎,为了在Prism事件机制中也实现传递空参数的技术,我自己创建了一个空类NUllClass,就靠它混饭吃了:
public class NullableEvent : CompositePresentationEvent<NullClass> { }
public class NullClass { }
于是,在发布方传递null:
eventAggregator.GetEvent<NullableEvent>().Publish(null);
而在订阅方订阅依旧,只是在调用方法时对NUllClass参数置之不理:
this.eventAggregator.GetEvent<NullableEvent>().Subscribe(DoSomething);
public void DoSomething(NullClass nullClass)
{
//Do Something();
}
最后说一句,什么时候使用Event,而什么时候使用Command,貌似它们都能解决相同的问题。在前面的介绍中,我们看到,Event主要适用于View之间的通信。而Command,则主要用于单独的View中。我会在下一章看到Command在Prism中的应用。
还有,关于Subscribe方法的最后一个参数Filter,还可以写成这样的形式:
subscriptionToken = fundAddedEvent.Subscribe(FundAddedEventHandler, ThreadOption.UIThread, false, fundOrder => fundOrder.CustomerId == customerId);
但是这只能在WPF中使用,因为Silverlight不支持Lambda表达式。