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來形成強類型的事件類型(而不是一個簡單的字符串)來避免了這些問題.