[Prism]Composite Application Guidance for WPF(8)——事件
周银辉
Prism的事件并没有直接使用C#的Event或WPF的RoutedEvent, 而是CompositeWpfEvent, 今天简单地谈一谈
1, 为什么是全新打造的CompositeWpfEvent,而非C#的Event或WPF的RoutedEvent?
原因一个: 断开事件发布者和事件订阅者之间的耦合.比如说模块A要订阅模块B的某一个时间,而模块A没有对模块B进行直接引用而是通用依赖注入等方式进行沟通的,所以在编译时无法进行事件的订阅.
而对事件的发布和注册则是通过EventAggregator来统一管理的,其是订阅者和订阅者之间的桥梁,关于EventAggregator,稍后会谈到
不过值得一提的是,CompositeWpfEvent更多地专注在模块之间的事件沟通,他并不是用来取代c#的Event和WPF的RoutedEvent的,他们在处于不同的层面上.
2, EventAggregator
Prism采用了一个称为EventAggregator的聚合器来统一管理事件(CompositeWpfEvent).
这实际上也是一种较常用的模式, 其思想比较简单,提供所有事件的实例:
/// <summary>
/// 定义一个接口,其用于获取事件类型的实例
/// </summary>
public interface IEventAggregator
{
/// <summary>
/// 获取事件类型的一个实例
/// </summary>
/// <typeparam name="TEventType">要获取实例的事件类型</typeparam>
/// <returns>事件类型<typeparamref name="TEventType"/>的一个实例.</returns>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter")]
TEventType GetEvent<TEventType>() where TEventType : EventBase;
}
所有的事件发布者和订阅者都从EventAggregator从去获取事件来进行发布和订阅, 发布者不关心订阅者是谁,
订阅者也不关心发布者时谁,同一事件可以有着不同的发布者,同一事件也可以有多个订阅者,这形成了多对多的关系.
但可能有人会问, 当我需要指定事件的发布者是谁时该怎么办呢, 就像普通事件中的Sender参数?
很简单, 事件可以传递任意类型的TPlayload参数(相当于XXXEventArgs)
关于EventAggregator模式,你可以参考这里http://martinfowler.com/eaaDev/EventAggregator.html
在Prism中,EventAggregator作为一个基础服务添加到一容器中
3, 如何创建和发布一个事件
public class LoginSucessedEventArgs
{
}
public class LoginSucessedEvent : CompositeWpfEvent<LoginSucessedEventArgs>
{
}
首先创建一个要在事件中传递的参数类型,这既可以是string等简单类型,也可以是自定义的负责类型, 它仅仅是包含数据的Object,
所以没有必要像C#一样继承于EventArgs
其次,创建一个自定义的XXXEvent, 其继承与CompositeWpfEvent<T>,其中T是你要传递的参数;
而CompositeWpfEvent是Observer模式的一个实现,自然地实现了订阅、取消订阅等基本方法,
通过这样的继承方式避免你为你的每一个事件都手动实现一个Observer模式.
C#的Event不是也可以使用+=与-=来实现订阅和取消订阅吗, 为什么还要重写实现一个Observer模式?
CompositeWpfEvent比C#的Event提供了更多更高级的功能:
在指定的线程内进行事件处理器的回调
一共提供了三种方式, 一是发布线程,二是UI线程,三是后台线程(由一个新的BackgroundWorker来调用),且看下面的枚举定义:
/// <summary>
/// 指定在哪一个线程上调用<see cref="CompositeWpfEvent{TPayload}"/>订阅
/// </summary>
public enum ThreadOption
{
/// <summary>
/// 调用将发生在与<see cref="CompositeWpfEvent{TPayload}"/>被发布时的相同线程上
/// </summary>
PublisherThread,
/// <summary>
/// 调用将发生在UI线程上
/// </summary>
UIThread,
/// <summary>
/// 将在后台线程上进行异步调用
/// </summary>
BackgroundThread
}
可指定对订阅者保持强引用还是弱引用.
默认情况下是保持弱引用, 如果指定为强引用时则除非手动取消订阅,否则订阅者不会被内存回收
可指定事件接受筛选器.
一般情况下,只要我们对某事件进行了订阅的话,当该事件被引发时订阅者总是会收到消息, 但这并不总是有用的,
那么这时你可以提供一个筛选器, 以便告诉事件发布者仅将那些满足一定条件的事件引发通知于你
这是非常有用的,在我的编码经验中就有不少时候为了达到这个功能而不得不放弃C# Event.
参考下面的代码:
internal class LoginModule : IModule
{
private readonly IRegionManager regionManager;
private readonly IEventAggregator eventAggregator;
public LoginModule(IRegionManager regionManager, IEventAggregator eventAggregator)
{
this.regionManager = regionManager;
this.eventAggregator = eventAggregator;
}
#region IModule Members
public void Initialize()
{
IRegion region = regionManager.Regions[Regions.LoginRegionName];
if (region != null)
{
var loginView = new DefaultLoginView();
loginView.LoginSucessed += loginView_LoginSucessed;
region.Add(loginView);
region.Activate(loginView);
}
}
void loginView_LoginSucessed(object sender, LoginSucessedEventArgs e)
{
if(eventAggregator != null)
{
eventAggregator.GetEvent<LoginSucessedEvent>().Publish(new LoginSucessedEventArgs());
}
}
#endregion
}
我们通过形如public LoginModule(IRegionManager regionManager, IEventAggregator eventAggregator)这种构造器注入的方式
来取得eventAggregator,然后就可以从eventAggregator中来获取我们要使用的事件了,然后采用如下的形式发布它:
eventAggregator.GetEvent<LoginSucessedEvent>().Publish(new LoginSucessedEventArgs());
其中默认情况下EventAggregator是通过反射的方式并按照单例的形式来取得我们自定义的事件类型的实例的,
打开Prism的源代码,我们可以看到其是这样实现的
public TEventType GetEvent<TEventType>() where TEventType : EventBase
{
var eventInstance = _events.FirstOrDefault(evt => evt.GetType() == typeof(TEventType)) as TEventType;
if (eventInstance == null)
{
eventInstance = Activator.CreateInstance<TEventType>();
_events.Add(eventInstance);
}
return eventInstance;
}
4, 如何订阅事件
很简单,从EventAggregator取得事件实例,然后调用Subscribe方法便可.
下面是Subscribe方法各个参数的含义:
/// <summary>
/// 添加一个事件订阅
/// </summary>
/// <param name="action">当事件被发布时所要执行的动作</param>
/// <param name="threadOption">指定哪一个线程将接收这个代理的回调.</param>
/// <param name="keepSubscriberReferenceAlive">当为<see langword="true"/>时, <seealso cref="CompositeWpfEvent{TPayload}"/>将保持对订阅者的引用,以便其不会被作为垃圾回收.</param>
/// <param name="filter">用于确定订阅者是否接收该事件的筛选器.</param>
/// <returns>唯一标志被添加的订阅的<see cref="SubscriptionToken"/></returns>
/// <remarks>
/// 如果<paramref name="keepSubscriberReferenceAlive"/> 被设置成 <see langword="false" />, <see cref="CompositeWpfEvent{TPayload}"/> 将维护由<paramref name="action"/>提供的代理目标的<seealso cref="WeakReference"/>
/// 如果不使用弱引用(<paramref name="keepSubscriberReferenceAlive"/> 为 <see langword="true" />)的话,
/// 当Dispose订阅者时用户必须显示为事件取消订阅(调用Unsubscribe)以避免内存泄漏或其他异常情况的发生.
/// CompositeWpfEvent集合是线程安全的.
/// </remarks>
public virtual SubscriptionToken Subscribe(Action<TPayload> action, ThreadOption threadOption, bool keepSubscriberReferenceAlive, Predicate<TPayload> filter)
当然, 你也可以通过Unsubscribe方法来取消订阅
5, 与CAB的Event有何不同?
在CAB中,我们一般采用下面这样的方式来进行事件的订阅和注册:
[EventPublication(“event://MyEventSignature”, EventScope.Global)]
event EventHandler<Object> MyEvent;
public void RaiseMyEvent()
{
//fire the event as a normal event
if MyEvent != null)
{
MyEvent(this, someObj ) ;
}
}
[EventSubscription(“event://MyEventSignature”, EventScope.Global )]
public void OnMyEvent(object sender, object someObj)
{
}
这有一个很明显的弊端: 无法在编译时对事件订阅和发布的正确性进行检查
原因在于我们在发布和订阅事件时均采用了Attribute进行标记,并使用的是"event://MyEventSignature”这样的Magic String.
如果我们将其写成了event://MyWrongEventSignature这样的错误签名,在编译时也是无法发现的. 硬编码字符串的其他弊端就更不用说了.
另外一个弊端是CAB需要去扫描每个Event是否有EventPublicationAttribute与每个方法是否有EventSubscriptionAttribute特性(Attribute)以便确定是否是事件发布和订阅,这对性能的影响不可小觑.
而Prism通过继承于CompositeWpfEvent来形成强类型的事件类型(而不是一个简单的字符串)来避免了这些问题.