Prism框架中的事件聚合器EventAggregator(下)
背景
在上篇中我们就Prism8.x版本中的事件聚合器EventAggregator中的最重要的EventBase进行了一个完整的讲述,并由此引申出EventBase
中最重要的一个关联项EventSubscription
,这篇文章我们将主要分析我们在实际使用的时候PubSubEvent以及EventAggregator的原理,从而在知道怎么使用的时候更清楚这背后的原理。
代码分析
2 PubSubEvent
我们先来看看具体的代码,然后再一步步进行分析。
/// <summary>
/// Defines a class that manages publication and subscription to events.
/// </summary>
public class PubSubEvent : EventBase
{
/// <summary>
/// Subscribes a delegate to an event that will be published on the <see cref="ThreadOption.PublisherThread"/>.
/// <see cref="PubSubEvent"/> will maintain a <see cref="WeakReference"/> to the target of the supplied <paramref name="action"/> delegate.
/// </summary>
/// <param name="action">The delegate that gets executed when the event is published.</param>
/// <returns>A <see cref="SubscriptionToken"/> that uniquely identifies the added subscription.</returns>
/// <remarks>
/// The PubSubEvent collection is thread-safe.
/// </remarks>
public SubscriptionToken Subscribe(Action action)
{
return Subscribe(action, ThreadOption.PublisherThread);
}
/// <summary>
/// Subscribes a delegate to an event.
/// PubSubEvent will maintain a <see cref="WeakReference"/> to the Target of the supplied <paramref name="action"/> delegate.
/// </summary>
/// <param name="action">The delegate that gets executed when the event is raised.</param>
/// <param name="threadOption">Specifies on which thread to receive the delegate callback.</param>
/// <returns>A <see cref="SubscriptionToken"/> that uniquely identifies the added subscription.</returns>
/// <remarks>
/// The PubSubEvent collection is thread-safe.
/// </remarks>
public SubscriptionToken Subscribe(Action action, ThreadOption threadOption)
{
return Subscribe(action, threadOption, false);
}
/// <summary>
/// Subscribes a delegate to an event that will be published on the <see cref="ThreadOption.PublisherThread"/>.
/// </summary>
/// <param name="action">The delegate that gets executed when the event is published.</param>
/// <param name="keepSubscriberReferenceAlive">When <see langword="true"/>, the <see cref="PubSubEvent"/> keeps a reference to the subscriber so it does not get garbage collected.</param>
/// <returns>A <see cref="SubscriptionToken"/> that uniquely identifies the added subscription.</returns>
/// <remarks>
/// If <paramref name="keepSubscriberReferenceAlive"/> is set to <see langword="false" />, <see cref="PubSubEvent"/> will maintain a <see cref="WeakReference"/> to the Target of the supplied <paramref name="action"/> delegate.
/// If not using a WeakReference (<paramref name="keepSubscriberReferenceAlive"/> is <see langword="true" />), the user must explicitly call Unsubscribe for the event when disposing the subscriber in order to avoid memory leaks or unexpected behavior.
/// <para/>
/// The PubSubEvent collection is thread-safe.
/// </remarks>
public SubscriptionToken Subscribe(Action action, bool keepSubscriberReferenceAlive)
{
return Subscribe(action, ThreadOption.PublisherThread, keepSubscriberReferenceAlive);
}
/// <summary>
/// Subscribes a delegate to an event.
/// </summary>
/// <param name="action">The delegate that gets executed when the event is published.</param>
/// <param name="threadOption">Specifies on which thread to receive the delegate callback.</param>
/// <param name="keepSubscriberReferenceAlive">When <see langword="true"/>, the <see cref="PubSubEvent"/> keeps a reference to the subscriber so it does not get garbage collected.</param>
/// <returns>A <see cref="SubscriptionToken"/> that uniquely identifies the added subscription.</returns>
/// <remarks>
/// If <paramref name="keepSubscriberReferenceAlive"/> is set to <see langword="false" />, <see cref="PubSubEvent"/> will maintain a <see cref="WeakReference"/> to the Target of the supplied <paramref name="action"/> delegate.
/// If not using a WeakReference (<paramref name="keepSubscriberReferenceAlive"/> is <see langword="true" />), the user must explicitly call Unsubscribe for the event when disposing the subscriber in order to avoid memory leaks or unexpected behavior.
/// <para/>
/// The PubSubEvent collection is thread-safe.
/// </remarks>
public virtual SubscriptionToken Subscribe(Action action, ThreadOption threadOption, bool keepSubscriberReferenceAlive)
{
IDelegateReference actionReference = new DelegateReference(action, keepSubscriberReferenceAlive);
EventSubscription subscription;
switch (threadOption)
{
case ThreadOption.PublisherThread:
subscription = new EventSubscription(actionReference);
break;
case ThreadOption.BackgroundThread:
subscription = new BackgroundEventSubscription(actionReference);
break;
case ThreadOption.UIThread:
if (SynchronizationContext == null) throw new InvalidOperationException(Resources.EventAggregatorNotConstructedOnUIThread);
subscription = new DispatcherEventSubscription(actionReference, SynchronizationContext);
break;
default:
subscription = new EventSubscription(actionReference);
break;
}
return InternalSubscribe(subscription);
}
/// <summary>
/// Publishes the <see cref="PubSubEvent"/>.
/// </summary>
public virtual void Publish()
{
InternalPublish();
}
/// <summary>
/// Removes the first subscriber matching <see cref="Action"/> from the subscribers' list.
/// </summary>
/// <param name="subscriber">The <see cref="Action"/> used when subscribing to the event.</param>
public virtual void Unsubscribe(Action subscriber)
{
lock (Subscriptions)
{
IEventSubscription eventSubscription = Subscriptions.Cast<EventSubscription>().FirstOrDefault(evt => evt.Action == subscriber);
if (eventSubscription != null)
{
Subscriptions.Remove(eventSubscription);
}
}
}
/// <summary>
/// Returns <see langword="true"/> if there is a subscriber matching <see cref="Action"/>.
/// </summary>
/// <param name="subscriber">The <see cref="Action"/> used when subscribing to the event.</param>
/// <returns><see langword="true"/> if there is an <see cref="Action"/> that matches; otherwise <see langword="false"/>.</returns>
public virtual bool Contains(Action subscriber)
{
IEventSubscription eventSubscription;
lock (Subscriptions)
{
eventSubscription = Subscriptions.Cast<EventSubscription>().FirstOrDefault(evt => evt.Action == subscriber);
}
return eventSubscription != null;
}
}
这个里面我们先来关注Publish和Subscribe方法,这两个是我们用到最多的。我们先来看看Publish方法,这个里面是直接调用基类EventBase中的InternalPublish()方法,我们来看看基类中这个方法的实现。
2.1 PubSubEvent中的Publish
/// <summary>
/// Calls all the execution strategies exposed by the list of <see cref="IEventSubscription"/>.
/// </summary>
/// <param name="arguments">The arguments that will be passed to the listeners.</param>
/// <remarks>Before executing the strategies, this class will prune all the subscribers from the
/// list that return a <see langword="null" /> <see cref="Action{T}"/> when calling the
/// <see cref="IEventSubscription.GetExecutionStrategy"/> method.</remarks>
protected virtual void InternalPublish(params object[] arguments)
{
List<Action<object[]>> executionStrategies = PruneAndReturnStrategies();
foreach (var executionStrategy in executionStrategies)
{
executionStrategy(arguments);
}
}
这个方法调用一个定义在EventBase中的PruneAndReturnStrategies方法,然后获取到对应的Action<object[]>委托并执行该方法,我们来看看这个PruneAndReturnStrategies的具体实现
private List<Action<object[]>> PruneAndReturnStrategies()
{
List<Action<object[]>> returnList = new List<Action<object[]>>();
lock (Subscriptions)
{
for (var i = Subscriptions.Count - 1; i >= 0; i--)
{
Action<object[]> listItem =
_subscriptions[i].GetExecutionStrategy();
if (listItem == null)
{
// Prune from main list. Log?
_subscriptions.RemoveAt(i);
}
else
{
returnList.Add(listItem);
}
}
}
return returnList;
}
这里我们说过当前PubSubEvent中通过一个Subcriptions的集合存储当前PubSubEvent的订阅方所要执行的委托,当然一个事件可能有多个订阅方所以这里我们会通过IEventSubscription中定义的GetExecutionStrategy方法找到订阅方之前传入的针对该事件执行的委托,然后依次遍历并执行这个委托,所以到了这里我们的思路已经相当明确了,我们需要关注的就是当前PubSubEvent的订阅方所要执行的委托是怎么加入到PubSubEvent中这个Subcriptions的集合中来的,后面我们来关注PubSubEvent中Subscribe的过程。
2.2 PubSubEvent中的Subscribe
由于PubSubEvent中的Subscribe方法有多个重载,我们这里选择一个最具代表性的方法来讲述整个过程。
/// <summary>
/// Subscribes a delegate to an event.
/// </summary>
/// <param name="action">The delegate that gets executed when the event is published.</param>
/// <param name="threadOption">Specifies on which thread to receive the delegate callback.</param>
/// <param name="keepSubscriberReferenceAlive">When <see langword="true"/>, the <see cref="PubSubEvent"/> keeps a reference to the subscriber so it does not get garbage collected.</param>
/// <returns>A <see cref="SubscriptionToken"/> that uniquely identifies the added subscription.</returns>
/// <remarks>
/// If <paramref name="keepSubscriberReferenceAlive"/> is set to <see langword="false" />, <see cref="PubSubEvent"/> will maintain a <see cref="WeakReference"/> to the Target of the supplied <paramref name="action"/> delegate.
/// If not using a WeakReference (<paramref name="keepSubscriberReferenceAlive"/> is <see langword="true" />), the user must explicitly call Unsubscribe for the event when disposing the subscriber in order to avoid memory leaks or unexpected behavior.
/// <para/>
/// The PubSubEvent collection is thread-safe.
/// </remarks>
public virtual SubscriptionToken Subscribe(Action action, ThreadOption threadOption, bool keepSubscriberReferenceAlive)
{
IDelegateReference actionReference = new DelegateReference(action, keepSubscriberReferenceAlive);
EventSubscription subscription;
switch (threadOption)
{
case ThreadOption.PublisherThread:
subscription = new EventSubscription(actionReference);
break;
case ThreadOption.BackgroundThread:
subscription = new BackgroundEventSubscription(actionReference);
break;
case ThreadOption.UIThread:
if (SynchronizationContext == null) throw new InvalidOperationException(Resources.EventAggregatorNotConstructedOnUIThread);
subscription = new DispatcherEventSubscription(actionReference, SynchronizationContext);
break;
default:
subscription = new EventSubscription(actionReference);
break;
}
return InternalSubscribe(subscription);
}
这个方法首先会根据传入的ThreadOption的类型决定使用哪个EventSubscription,这里我们希望事件的订阅方可以在当前事件发布者线程中执行接收操作当然也可以在新的线程甚至是UI线程中执行当前的代码,这里我们看看这里最终调用基类EventBase中的InternalSubcribe方法,我们先来看看具体的代码
/// <summary>
/// Adds the specified <see cref="IEventSubscription"/> to the subscribers' collection.
/// </summary>
/// <param name="eventSubscription">The subscriber.</param>
/// <returns>The <see cref="SubscriptionToken"/> that uniquely identifies every subscriber.</returns>
/// <remarks>
/// Adds the subscription to the internal list and assigns it a new <see cref="SubscriptionToken"/>.
/// </remarks>
protected virtual SubscriptionToken InternalSubscribe(IEventSubscription eventSubscription)
{
if (eventSubscription == null) throw new ArgumentNullException(nameof(eventSubscription));
eventSubscription.SubscriptionToken = new SubscriptionToken(Unsubscribe);
lock (Subscriptions)
{
Subscriptions.Add(eventSubscription);
}
return eventSubscription.SubscriptionToken;
}
这里整个过程都十分清楚,但是我们发现在为当前EventSubscription的SubscriptionToken赋值的时候在构造函数中还传入了Unsubscribe的委托,我们来看看这个委托。
/// <summary>
/// Removes the subscriber matching the <see cref="SubscriptionToken"/>.
/// </summary>
/// <param name="token">The <see cref="SubscriptionToken"/> returned by <see cref="EventBase"/> while subscribing to the event.</param>
public virtual void Unsubscribe(SubscriptionToken token)
{
lock (Subscriptions)
{
IEventSubscription subscription = Subscriptions.FirstOrDefault(evt => evt.SubscriptionToken == token);
if (subscription != null)
{
Subscriptions.Remove(subscription);
}
}
}
这段代码刚好相反是取消特定的事件订阅操作,只不过将这个特定的操作放到了SubscriptionToken这个对象中,这个对象实现了IDisposable接口,只要当前唯一的PubSubEvent对应的Subscription被释放那么之前订阅方的操作一定会在PubSubEvent中被释放从而避免了内存泄漏的风险。
3 EventAggregator
这个类是对外直接暴露的类型,也是非常重要的一个入口,我们发现在这个Prism8.x版本中这个接口做了大量的简化,简化到最后只剩下一个函数就是获取特定的PubSubEvent,而这个EventAggregator回归了它最原始的属性就是一个事件聚合器。具体我们来看看。
/// <summary>
/// Implements <see cref="IEventAggregator"/>.
/// </summary>
public class EventAggregator : IEventAggregator
{
private readonly Dictionary<Type, EventBase> events = new Dictionary<Type, EventBase>();
// Captures the sync context for the UI thread when constructed on the UI thread
// in a platform agnostic way so it can be used for UI thread dispatching
private readonly SynchronizationContext syncContext = SynchronizationContext.Current;
/// <summary>
/// Gets the single instance of the event managed by this EventAggregator. Multiple calls to this method with the same <typeparamref name="TEventType"/> returns the same event instance.
/// </summary>
/// <typeparam name="TEventType">The type of event to get. This must inherit from <see cref="EventBase"/>.</typeparam>
/// <returns>A singleton instance of an event object of type <typeparamref name="TEventType"/>.</returns>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter")]
public TEventType GetEvent<TEventType>() where TEventType : EventBase, new()
{
lock (events)
{
EventBase existingEvent = null;
if (!events.TryGetValue(typeof(TEventType), out existingEvent))
{
TEventType newEvent = new TEventType();
newEvent.SynchronizationContext = syncContext;
events[typeof(TEventType)] = newEvent;
return newEvent;
}
else
{
return (TEventType)existingEvent;
}
}
}
}
我们看到在EventAggregator内部维护了一个Dictionary<Type, EventBase>用来存放当前所有的PubSubEvent,从而对外提供一个同一的方式获取当前的PubSubEvent,从而最终完成事件的发布与订阅服务。
总结
这个部分才是我们这个系列文章的核心,我们要分析Prism8.x为何要使用一个全新的EventAggregator?,以及EventAggregator要解决一个什么问题?
- 通过EventAggregator这个第三方的参与直接降低事件的发布方和订阅方的直接耦合。
- 通过现在新的实现方式替换在老版本中通过接口和反射带来的低性能问题。