65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.43/5 (8投票s)

2016年8月20日

CPOL

4分钟阅读

viewsIcon

13787

downloadIcon

158

探讨紧耦合和松耦合组件的设计。

下载演示

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 的事件聚合器非常有用。我们可以立即使用它,或者等待官方文档和书籍以了解更多信息。

© . All rights reserved.