使用 .NET Framework 事件和 Prism 的事件聚合器进行事件通信






4.43/5 (8投票s)
探讨紧耦合和松耦合组件的设计。
1. 背景
在开发应用程序时,例如遵循 MVVM 模式的应用程序,我们需要一种机制来在视图模型 (VM) 之间进行通信。例如,一个包含所有员工列表的主 VM,以及一个显示当前选定员工的详细 VM。当主 VM 中当前选定的员工发生更改时,我们需要通知详细 VM。此外,当详细 VM 更新其选定的员工时(例如更改员工姓名或删除该员工),主 VM 需要知晓,以便主 VM 可以更新其所有员工列表。
虽然使用 .NET 委托/事件的各种传统技术都可以完成任务,但它们的缺点促使我们采用一种新方法:事件聚合器。本文旨在回顾“标准”的 .NET 事件驱动编程技术,然后介绍 Prism 库提供的、用于松耦合组件之间通信的、更易于维护和测试的方法。
这里的例子是关于政府对个人收入事件感兴趣。政府(扮演订阅者角色),个人(扮演发布者角色),感兴趣的事件是当个人的总收入超过 30 美元时。
让我们从传统的 .NET 事件开始。
2. 使用 .NET Framework 事件
在深入之前,请记住以下要点
- 发布者决定何时触发事件。
- 订阅者决定如何处理事件:当事件触发时做什么。
- 多个订阅者可以订阅发布者的事件。在这种情况下,事件处理程序将按照事件触发的顺序被调用。
- 此外,一个订阅者可以订阅多个发布者的多个事件。
我们将使用通用委托 **EventHandler<TEventArgs>**。其签名如下:
public delegate void EventHandler(object sender, TEventArgs e)
在这里,**sender** 是事件的来源。这意味着它是引发事件的发布者实例对象,由 'this' 关键字表示。
以下是需要遵循的步骤
2.1. 实现发布者
public class Person
{
// Constructors, properties,...
double _income;
public void MakeMoney(double money)
{
_income += money;
// When total income becomes greater than 30, then raise the event 'IncomeReached'
// with related information encapsulated in an 'IncomeReachedEventArg' object.
if (_income > 30)
{
var incomeReachedEventArg = new IncomeReachedEventArg
{
TotalIncome = _income,
IncomeReportedDate = DateTime.Now
};
OnIncomeReached(incomeReachedEventArg);
}
}
protected virtual void OnIncomeReached(IncomeReachedEventArg e)
{
IncomeReached?.Invoke(this, e);
}
public event EventHandler IncomeReached;
}
其中 **IncomeReachedEventArg** 是 **TEventArgs**。我们可以将尽可能多的必要数据封装到其中,以传递给订阅者的事件处理程序。
public class IncomeReachedEventArg
{
public double TotalIncome { get; set; }
public DateTime IncomeReportedDate { get; set; }
// ... more if needed
}
根据约定,通常这个事件数据 **IncomeReachedEventArg** 派生自 **EventArgs** 基类。虽然不这样做也可以。
2.2. 实现订阅者
public class Government
{
// Constructors and properties omitted...
// What to do in response to the event
public void Person_IncomeReached(object sender, IncomeReachedEventArg e)
{
// Logic to determine amount of tax based on other factors, such as income, age, etc.
WriteLine($"On {e.IncomeReportedDate.ToString("d")}, {((Person)sender).Name} has reached the total taxable income of {e.TotalIncome} USD.");
}
}
这里,发布者的事件处理程序 **Person_IncomeReached** 必须满足委托 **EventHandler<TEventArgs>** 的签名,其中 **TEventArgs** 是 **IncomeReachedEventArg**。
2.3. 订阅事件并触发事件
static void Main()
{
WriteLine("\t.Demo 1: using .NET Event\n");
Person person = new Person("John Doe"); // publisher
Government gov = new Government(); // subscriber
// Subscribe to the event
// (here, one or multiple subscribers can subscribe/un-subscribe to the event)
// In this case, the government object subscribes to the 'IncomeReached' of the person object
person.IncomeReached += gov.Person_IncomeReached;
// Now, try to raise the event
person.MakeMoney(15);
person.MakeMoney(16); // Condition met, fire the event
person.MakeMoney(-10);
person.MakeMoney(20); // Condition met, fire the event
ReadLine();
}
运行应用程序,结果如下:
此 .NET Framework 事件解决方案的完整代码
namespace DotNetEvent
{
class Program
{
static void Main()
{
WriteLine("\t.Demo 1: using .NET Event\n");
Person person = new Person("John Doe"); // publisher
Government gov = new Government(); // subscriber
// Subscribe to the event
person.IncomeReached += gov.Person_IncomeReached;
person.MakeMoney(15);
person.MakeMoney(16); // Condition met, fire the event
person.MakeMoney(-10);
person.MakeMoney(20); // Condition met, fire the event
ReadLine();
}
}
// Event Publisher
public class Person
{
double _income;
string _personName;
public Person(string personName)
{
_personName = personName;
}
public string Name
{
get { return _personName; }
}
public void MakeMoney(double money)
{
_income += money;
// When total income becomes greater than 30, then raise the event 'IncomeReached'
// with related information encapsulated in an 'IncomeReachedEventArg' object.
if (_income > 30)
{
var incomeReachedEventArg = new IncomeReachedEventArg
{
TotalIncome = _income,
IncomeReportedDate = DateTime.Now
};
OnIncomeReached(incomeReachedEventArg);
}
}
protected virtual void OnIncomeReached(IncomeReachedEventArg e)
{
IncomeReached?.Invoke(this, e);
}
public event EventHandler IncomeReached;
}
public class IncomeReachedEventArg
{
public double TotalIncome { get; set; }
public DateTime IncomeReportedDate { get; set; }
}
// Event Subscriber
public class Government
{
// Constructors and properties omitted...
// What to do in response to the event
public void Person_IncomeReached(object sender, IncomeReachedEventArg e)
{
// Logic to determine amount of tax based on other factors, such as income, age, etc.
WriteLine($"On {e.IncomeReportedDate.ToString("d")}, {((Person)sender).Name} has reached the total taxable income of {e.TotalIncome} USD.");
}
}
}
关于 .NET Framework 事件方法的说明
订阅者(政府对象)需要发布者(个人对象)的引用来将发布者的事件(**IncomeReached**)连接到订阅者的方法处理程序(**Person_IncomeReached**)。换句话说,政府对象需要知道并订阅,比如说,许多个人对象:这太工作量太大了!如果政府和个人都不需要了解对方,是否会更容易?(是的,Prism 的事件聚合器将提供帮助)。
(上述解决方案)会导致紧耦合的设计,难以进行单元测试和维护。此外,还可能发生内存泄漏。例如,如果政府对象在取消订阅之前被垃圾回收,那么个人对象也无法被垃圾回收。
现在,我们进入事件聚合器部分。
3. 使用 Prism 的事件聚合器
Prism 事件聚合器的目的是解耦发布者和订阅者。换句话说,它使得应用程序中松耦合组件之间的通信成为可能。发布者和订阅者可以进行通信(发送和接收事件),而无需维护彼此的引用。
图片来自 Magnus Montin .NET 博客。
以下是需要遵循的步骤
3.1 添加 NuGet Prism 包
由于 **Prism.PubSubEvents** 已过时,请改用新的 **Prism.Core** (6.2.0)
3.2. 创建事件
我们的自定义事件名为 **IncomeReachedEvent**,它派生自 **Prism.Events** 命名空间中的 **PubSubEvent<TPayload>** 基类,其中 **TPayload** 是将传递给订阅者的消息。与 EventArgs 类似,我们可以将尽可能多的必要数据封装到其中(**IncomeMessage**)。
public class IncomeReachedEvent : PubSubEvent { }
public class IncomeMessage
{
public string PersonName { get; set; }
public double TotalIncome { get; set; }
public DateTime IncomeReportedDate { get; set; }
... more related data if needed
}
**PubSubEvent<TPayload>** 类是连接发布者和订阅者最重要的一个类,因为它完成了订阅/取消订阅、发布等所有工作。
3.3. 实现发布者
public class Person
{
double _income;
string _personName;
IEventAggregator _eventAggregator;
public Person(string personName, EventAggregator eventAggregator)
{
this._personName = personName;
this._eventAggregator = eventAggregator;
}
public void MakeMoney(double money)
{
_income += money;
// When total income becomes greater than 30, then fire the event 'IncomeReached'
// with related information encapsulated in an 'IncomeReachedEventArg' object.
if (_income > 30)
{
var message = new IncomeMessage
{
PersonName = _personName,
TotalIncome = _income,
IncomeReportedDate = DateTime.Now
};
_eventAggregator.GetEvent().Publish(message);
}
}
}
如上所示,个人(发布者)通过从 **IEventAggregator** 对象中检索事件(类型为 **IncomeReachedEvent**)来触发事件,并调用 **Publish(TPayload payload)** 方法,其中 **payload** 是我们的自定义消息对象(**IncomeMessage**)。
_eventAggregator.GetEvent().Publish(message);
3.4. 实现订阅者
**PubSubEvent** 类提供了几种订阅方法的重载,具体取决于不同的情况。例如,是否更新 UI、过滤事件或存在性能顾虑。有关更多信息,请访问 MSDN Prism 5 指南(版本 5,虽然旧但仍然相关且有用,同时我们也在等待版本 6 的文档)。这里使用了默认订阅。
public class Government
{
// Constructor, properties,...
// Method or property setter that does the subscription
private void Init()
{
// Subscribe to the interested event, and handle when it happens
var incomeReachedEvent = _eventAggregator.GetEvent();
incomeReachedEvent.Subscribe(message =>
{
WriteLine($"On {message.IncomeReportedDate.ToString("d")}, {message.PersonName} has reached the total taxable income of {message.TotalIncome} USD.");
});
}
}
3.5. 触发事件
static void Main()
{
WriteLine("\t.Demo 2: using Prism's Event Aggregator\n");
var evt = new EventAggregator();
var person = new Person("John Doe", evt);
var gov = new Government(evt);
person.MakeMoney(15);
person.MakeMoney(16); // Condition met, fire the event
person.MakeMoney(-10);
person.MakeMoney(20); // Condition met, fire the event
ReadLine();
}
结果是:
Prism 事件聚合器解决方案的完整代码
namespace DotNetPrismEventAggregator
{
class Program
{
static void Main()
{
WriteLine("\t.Demo 2: using Prism's Event Aggregator\n");
var evt = new EventAggregator();
var person = new Person("John Doe", evt);
var gov = new Government(evt);
person.MakeMoney(15);
person.MakeMoney(16); // Condition met, fire the event
person.MakeMoney(-10);
person.MakeMoney(20); // Condition met, fire the event
ReadLine();
}
}
// Event Publisher
public class Person
{
double _income;
string _personName;
IEventAggregator _eventAggregator;
public Person(string personName, EventAggregator eventAggregator)
{
this._personName = personName;
this._eventAggregator = eventAggregator;
}
public void MakeMoney(double money)
{
_income += money;
// When total income becomes greater than 30, then fire the event 'IncomeReached'
// with related information encapsulated in an 'IncomeReachedEventArg' object.
if (_income > 30)
{
var message = new IncomeMessage
{
PersonName = _personName,
TotalIncome = _income,
IncomeReportedDate = DateTime.Now
};
_eventAggregator.GetEvent().Publish(message);
}
}
}
public class IncomeReachedEvent : PubSubEvent { }
public class IncomeMessage
{
public string PersonName { get; set; }
public double TotalIncome { get; set; }
public DateTime IncomeReportedDate { get; set; }
}
// Event Subscriber
public class Government
{
IEventAggregator _eventAggregator;
public Government(EventAggregator eventAggregator)
{
_eventAggregator = eventAggregator;
Init(); // Do some necessary things
}
private void Init()
{
// Subscribe to the interested event, and handle when it happens
var incomeReachedEvent = _eventAggregator.GetEvent();
incomeReachedEvent.Subscribe(message =>
{
WriteLine($"On {message.IncomeReportedDate.ToString("d")}, {message.PersonName} has reached the total taxable income of {message.TotalIncome} USD.");
});
}
}
}
结论
正如我们所见,Prism 的事件聚合器非常有用。我们可以立即使用它,或者等待官方文档和书籍以了解更多信息。