Prism EventAggregator 示例






4.96/5 (30投票s)
这只是对 EventAggregator 的介绍,并附带一个易于理解的示例。
引言
我在网上查找信息时,对理解Prism的EventAggregator
的难度感到非常沮丧。我以前用过它,但那些文章和示例似乎都很难理解。既然我以前用过它,我就知道它并没有那么难。我认为现在使用Prism 4库中的CompositePresentationEvent
会更容易。
这只是对EventAggregator
的介绍,并附带一个易于理解的示例。该示例仅在窗体内部进行通信,这有点傻,但这样所有东西都在一起了。我发现现有示例的一个问题是代码分散在许多模块中,使得理解发生了什么变得非常困难。
使用 Prism
可以从http://www.microsoft.com/download/en/details.aspx?displaylang=en&id=28950安装Prism 4.1库。在项目中,我只取了我需要的两个DLL并将它们包含在解决方案中。本项目使用的两个DLL是:
- Microsoft.Practices.Composite.Events
- Microsoft.Practices.Composite.Presentation.Events
Prism的功能远不止EventAggregator
,微软在Pattern and Practices中发布了关于Prism的广泛信息:http://compositewpf.codeplex.com/releases/view/55580。
EventAggregator
EventAggregator
允许组合应用程序中的组件以松耦合的方式进行通信。Prism EventAggregator
实现了一个发布/订阅事件机制。事件的发布/订阅由用于发布/订阅泛型的一个类决定。这通过最小化模块之间的依赖关系来支持良好的设计,使它们更加松耦合。订阅模块只需要知道将要生成事件的模块(如果有的话),只需要知道订阅它们的类。
Prism主要用于支持桌面应用程序,但我认为Event Aggregator更灵活。Prism文档侧重于在桌面或Silverlight应用程序中使用Event Aggregator与视图进行通信,并提供了一种创建松耦合桌面体验的集成方法。
代码
本示例是一个非常简单的应用程序,使用WPF创建,并且只有代码隐藏。我想清楚地展示如何实现一个简单的EventAggregator
,所以我把所有东西都放在了代码隐藏中,包括订阅和发布所需的类。
using System;
using System.Windows;
using Microsoft.Practices.Composite.Events;
using Microsoft.Practices.Composite.Presentation.Events;
namespace EventAggregatorSample
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
///
public class test
{
}
public partial class MainWindow : Window, IDisposable
{
readonly IEventAggregator _aggregator = new EventAggregator();
private SubscriptionToken _intInt1SubscriptionToken;
private SubscriptionToken _intInt2SubscriptionToken;
public MainWindow()
{
InitializeComponent();
// Next line only shows can unsubscribe before subscribing, like for events.
_aggregator.GetEvent<SampleStringEvent>().Unsubscribe(StringAction1);
// 4 subscriptions
_aggregator.GetEvent<SampleStringEvent>().Subscribe(StringAction1);
// Example with filtering
_aggregator.GetEvent<SampleStringEvent>().Subscribe(StringAction2,
ThreadOption.UIThread, false, i => i.Contains("b"));
// Next line shows saving the subscription token, used to unsubscribe
_intInt1SubscriptionToken = _aggregator.GetEvent<SampleIntEvent>().
Subscribe(IntAction1, ThreadOption.UIThread, false, i => i > 0);
_intInt2SubscriptionToken = _aggregator.GetEvent<SampleIntEvent>().
Subscribe(IntAction1, ThreadOption.UIThread, false, i => i < 0);
// Extra Credit
_aggregator.GetEvent<SampleStringEvent>().
Subscribe(StringAction3, ThreadOption.UIThread, false,
s =>
{
int i;
return int.TryParse(s, out i);
});
}
// EventAggregator Event handlers for the 4 subscriptions
private void StringAction1(string s) { TextBoxReceiveString1.Text = s; }
private void StringAction2(string s) { TextBoxReceiveString2.Text = s; }
private void StringAction3(string s) { TextBoxReceiveStringInt.Text = s; }
private void IntAction1(int i) { TextBoxReceiveInt.Text = i.ToString(); }
// Event handlers for the 3 "Send" buttons
private void ButtonSendString1(object sender, RoutedEventArgs e)
{
_aggregator.GetEvent<SampleStringEvent>().Publish(TextBoxSendString1.Text);
}
private void ButtonSendString2(object sender, RoutedEventArgs e)
{
_aggregator.GetEvent<SampleStringEvent>().Publish(TextBoxSendString2.Text);
}
private void ButtonSendInt(object sender, RoutedEventArgs e)
{
int output;
_aggregator.GetEvent<SampleStringEvent>().Publish(TextBoxSendInt.Text);
if (int.TryParse(TextBoxSendInt.Text, out output))
{
_aggregator.GetEvent<SampleIntEvent>().Publish(output);
if (output < 0) // Only allow negative number once
_aggregator.GetEvent<SampleIntEvent>().Unsubscribe(_intInt2SubscriptionToken);
if (output < -1000) // Clear all
_aggregator.GetEvent<SampleIntEvent>().Unsubscribe(IntAction1);
if (output == 0) // reset all
{
//Need to clear first...
_aggregator.GetEvent<SampleIntEvent>().Unsubscribe(IntAction1);
// Next line shows saving the subscription token, used to unsubscribe
_intInt1SubscriptionToken = _aggregator.GetEvent<SampleIntEvent>().
Subscribe(IntAction1, ThreadOption.UIThread, false, i => i > 0);
_intInt2SubscriptionToken = _aggregator.GetEvent<SampleIntEvent>().
Subscribe(IntAction1, ThreadOption.UIThread, false, i => i < 0);
}
}
else
{
MessageBox.Show("The value is not an integer");
}
}
// Dispose of the 4 subscriptions (really need to also have the Destructor)
// see http://bytes.com/topic/c-sharp/answers/226921-dispose-destructor-guidance-long
public void Dispose()
{
_aggregator.GetEvent<SampleStringEvent>().Unsubscribe(StringAction1);
_aggregator.GetEvent<SampleStringEvent>().Unsubscribe(StringAction2);
_aggregator.GetEvent<SampleIntEvent>().Unsubscribe(_intInt1SubscriptionToken);
_aggregator.GetEvent<SampleIntEvent>().Unsubscribe(_intInt2SubscriptionToken);
_aggregator.GetEvent<SampleStringEvent>().Unsubscribe(StringAction3);
}
}
public class SampleStringEvent : CompositePresentationEvent<string> { }
public class SampleIntEvent : CompositePresentationEvent<int> { }
}
在此代码中,我创建了两个事件,一个包含字符串包,另一个包含整数(SampleStringEvent
和SampleIntEvent<
)。有三个对SampleStringEvent
的订阅,其中一个没有条件,一个要求字符串包包含“B”,另一个要求字符串代表一个整数。有两个对SampleIntEvent
的订阅,一个用于大于零的数字,另一个用于小于零的数字(这意味着值为“0”不会触发事件)。这两个整数事件使用相同的事件处理程序,并且都保存了一个订阅令牌。我创建了第三个对SampleStringEvent
的订阅,要求字符串代表一个整数,只是为了与SampleIntEvent
进行比较。
在用于整数的“发送”按钮的事件处理程序中,我检查整数值,并根据值执行操作。
- 如果值为负数,则使用小于零的值的订阅令牌取消订阅
SampleIntEvent
。 - 如果值为零,则重新订阅两个整数事件。
- 如果值小于-1000,则使用事件处理程序的地址取消订阅两个事件。
这允许演示订阅令牌的一个特殊功能,并且还表明取消订阅有效。
结果
- 您会注意到,我通过创建两个派生自
CompositePresentationEvent<T>
的类来定义了两个不同的事件。此模式的一个优点是,许多事件可以使用相同的载荷进行定义。EventAggregator
提供了一个实例供订阅者订阅事件,然后发布者通知订阅者事件已发生。GetEvent<T>()
方法允许访问特定事件,其中类类型T
定义事件。返回的TEventType
类型的对象然后可用于订阅/取消订阅事件或触发/发布事件。 - 除了第一个订阅调用之外,所有其他订阅调用都使用一个额外的参数来告诉
EventAggregator
事件将在UI线程上接收。如果发布者与UI不在同一线程上,并且需要更新UI元素,这一点很重要。在这个简单的示例中这不是问题。您可以测试将此参数更改为ThreadOption.BackgroundThread
会发生什么,会发生线程异常,因为尝试从后台线程更新文本框。 - 您会注意到,在构造函数中的订阅之前有一个取消订阅。这是为了表明当尝试取消订阅尚未订阅的事件时不会生成异常。
- 前两个订阅和第五个订阅订阅同一个事件,而另外两个订阅另一个事件。它们使用不同的事件处理程序(它们写入不同的文本框),并且大多数包含一个过滤器(例如,第二个事件处理程序将在字符串包含“b”时触发事件)。这是订阅事件时的过滤选项。将lambda表达式作为
Subscribe
方法中的第四个参数传递,该表达式将指定过滤器,从而无需事件处理程序检查事件是否适用。 - 对于第三个和第四个订阅,保存来自
Subscribe
方法的订阅令牌。由于保存了订阅令牌,Unsubscribe
方法可以使用订阅令牌作为参数,而不是事件处理程序的委托。使用订阅令牌允许取消订阅使用同一个事件处理程序的多个事件中的一个,而无需取消订阅使用该事件处理程序的所有事件。通过在第三个TextBox
中输入不同的数字,使用订阅令牌取消订阅一个事件,使用事件处理程序变量取消订阅两个事件,或者重新订阅两个事件。 - 为了展示良好的设计实践,我还继承了
IDisposable
。在单例中订阅事件意味着存在一个可能导致内存泄漏的资源。因此,最好使用EventAggregator
来处理资源。默认情况下,订阅是弱引用,因此对于许多应用程序来说,内存泄漏不是问题。订阅时还有一个选项(Subscribe
方法中的bool
参数)是在调用构造函数时保持订阅(强引用)。这意味着内存管理不会在没有明确指示的情况下处理聚合器。
这个示例非常简单,并没有展示EventAggregator
在模块之间,甚至项目之间的工作方式,但它在项目之间工作得非常好。Prism适用于代码分布在多个项目中的大型项目,以帮助管理代码开发。
优点/缺点
缺点
- 注册:让监听者进行注册会在每个监听者中添加重复的代码。
- 可发现性/可追溯性:事件聚合是一种间接形式,这使得系统更难理解。
- 严重的内存泄漏问题,因为
EventAggregator
维护着对订阅者的引用。 - 不支持事件闩锁,意味着有时可以忽略事件,然后支持更新。
- 事件顺序不能保证。
- 通过
GetEvent<T>()
方法获取事件以发布或订阅被认为有点笨拙。
优点
- 易于添加新功能。
- 强类型允许快速确定特定事件的发布者和订阅者。
- 事件过滤意味着不关心所有实例的订阅者可以忽略它们不关心的实例。并且过滤是在Event Aggregator中完成的。
- 对处理应在哪个线程上执行有精细控制。
- 仪器仪表化的中央场所。
注意
似乎很容易为订阅添加一个禁用方法,然后一个Enable
方法就可以让事件再次生成,而不会增加太多成本/复杂性。这消除了一个缺点。
结论
EventAggregator
是创建更易于维护的应用程序的强大工具。它也可能被轻易滥用,导致应用程序难以理解。它是Prism库的一部分,但它是库中那些用途远超UI的元素之一。我可以看到在我的当前项目中,它本可以极大地简化工作。
微软Prism文档中的文章实际上写得非常好,但我对示例有些问题。在阅读完这篇文章后,我建议您查阅以下文档中的信息:http://msdn.microsoft.com/en-us/library/ff649187.aspx.