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

EventBroker:用于同步和异步、松耦合事件处理的通知组件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.89/5 (23投票s)

2008年10月11日

Apache

9分钟阅读

viewsIcon

209630

downloadIcon

2069

EventBroker 是一个用于(异步)松耦合事件处理的通知组件。

EventBroker.png

前言

本文是对 Daniel Grunwald 的文章 Weak Events in C# 的回应。

引言

EventBroker 是一个您可以在系统中用于触发和接收通知的组件。

特点

  • 松耦合:
  • 订阅者无需了解发布者。两者只需知道 EventTopic URI(一个唯一标识系统中 EventTopic 的字符串)。这有助于构建松耦合的系统。

  • 线程同步:
  • 订阅者定义订阅处理程序在哪种线程上执行

    • 与发布者相同的线程(同步)
    • 后台线程(异步)
    • 用户界面线程(同步或异步)

    发布者可以限制事件订阅为同步或异步。

  • 多个发布者/订阅者:
  • 多个发布者/订阅者可以触发/处理同一个 EventTopic

  • 弱引用:
  • 发布者和订阅者通过弱引用进行引用,这不会阻止它们被垃圾回收——就像在常规事件注册模型中一样。

  • 范围:
    • 每个 EventBroker 的范围
    • 只有在同一个 EventBroker 上注册的发布者和订阅者在触发事件时才会被连接起来。通常,您会在系统中只使用一个 EventBroker 来处理所有事件通知。然而,在特殊情况下,为子系统定义一个新的 EventBroker 是很有用的。这使您能够为事件通知定义一个范围。

    • 具有分层命名的范围
    • 发布者和订阅者可以进行分层命名,并且事件可以是全局的、仅对父级或仅对子级。发布者和订阅者都可以定义它们发布到/接收自的范围。

背景

EventBroker 基于 Microsoft 的 Composite (UI) Application Block 中的 EventBroker。有关差异,请参阅下面的与 CAB EventBroker 的比较部分。

使用代码

示例发布者

发布事件主题

public class Publisher
{
    [EventPublication("topic://EventBrokerSample/SimpleEvent")]
    public event EventHandler SimpleEvent;
    
    ///<summary>Fires the SimpleEvent</summary>
    public void CallSimpleEvent()
    {
        SimpleEvent(this, EventArgs.Empty);
    }
}

将发布者注册到您的事件代理(您必须在代码中的某个地方保留事件代理的实例)。示例假设有一个服务为我们保留事件代理实例

EventBroker eb = Service.EventBroker;
Publisher p = new Publisher();
eb.Register(p);

在注册发布者时,事件代理会检查发布者是否发布了事件(带有 EventPublication 属性的事件)。

示例订阅者

订阅事件主题

public class Subscriber
{
    [EventSubscription(
        "topic://EventBrokerSample/SimpleEvent", 
        typeof(Handlers.Publisher))]
    public void SimpleEvent(object sender, EventArgs e)
    {
        // do something useful or at least funny
    }
}

将订阅者注册到事件代理

EventBroker eb = Service.EventBroker; 
Subscriber s = new Subscriber();
eb.Register(s); 

事件代理在注册时会检查订阅者是否订阅了事件主题(带有 EventSubscription 属性的方法)。

如果发布者触发了订阅者已注册的事件主题,则事件代理会通过调用订阅处理程序方法,使用发布者用于触发事件的 senderEventargs,将它们中继给订阅者。

发布选项

简单

[EventPublication("Simple")]
public event EventHandler SimpleEvent;

使用自定义 Eventargs

注意:CustomEventArgs 只需要继承自 EventArgs

[EventPublication("CustomEventArgs")]
public event EventHandler<CustomEventArguments> CustomEventArgs;

一次发布多个事件主题

[EventPublication("Event1")]
[EventPublication("Event2")]
[EventPublication("Event3")]
public event EventHandler MultiplePublicationTopics;

仅允许同步订阅处理程序

有关更多详细信息,请参阅与 CAB EventBroker/订阅处理程序限制部分。

[EventPublication("test", HandlerRestriction.Synchronous)]
public event EventHandler AnEvent; 

仅允许异步订阅处理程序

有关更多详细信息,请参阅与 CAB EventBroker/订阅处理程序限制部分。

[EventPublication("test", HandlerRestriction.Asynchronous)]
public event EventHandler AnEvent;

订阅选项

简单

[EventSubscription("Simple", typeof(Handlers.Publisher)]
public void SimpleEvent(object sender, EventArgs e) {} 

自定义 Eventargs

[EventSubscription("CustomEventArgs"), typeof(Handlers.Publisher))]
public void CustomEventArgs(object sender, CustomEventArgs e) {} 

订阅多个事件主题

[EventSubscription("Event1", typeof(Handlers.Publisher))]
[EventSubscription("Event2", typeof(Handlers.Publisher))]
[EventSubscription("Event3", typeof(Handlers.Publisher))]
public void MultipleSubscriptionTopics(object sender, EventArgs e) {} 

在后台线程上执行处理程序(异步)

事件代理创建一个工作线程来在该线程上执行处理程序方法。发布者可以立即继续处理。

[EventSubscription("Background", typeof(Handlers.Background))]
public void BackgroundThread(object sender, EventArgs e) {} 

在 UI 线程上执行处理程序

如果从后台工作线程调用用户界面组件以更新用户界面,请使用此选项——不再需要 Control.Invoke(...)

[EventSubscription("UI", typeof(Handlers.UserInterface))]
public void UI(object sender, EventArgs e) {}

请注意,如果您使用 UserInterface 处理程序,则必须确保您是在用户界面线程上注册订阅者。否则,EventBroker 将无法切换到用户界面线程,并会抛出异常。

在 UI 线程上异步执行处理程序

与上面相同,但发布者不会被阻塞,直到订阅者处理完事件。

[EventSubscription("UIAsync", typeof(Handlers.UserInterfaceAsync))]
public void UI(object sender, EventArgs e) {}  

直接在 EventBroker 上触发事件主题

事件主题可以直接在 EventBroker 上触发,无需注册发布者。当您需要从一个只存在很短时间的对象的对象触发事件主题时,这一点非常有用。

这种情况的一个好例子是计划作业执行。在计划的时间,会实例化并执行作业类的实例。注册此实例、触发事件、然后再次取消注册会很麻烦——直接在 EventBroker 上触发事件要容易得多。

eventBroker.Fire("topic", sender, eventArgs);

范围

有时需要限制事件发布范围。这可以通过两种方式实现

多个事件代理实例

订阅者只能监听在同一事件代理上注册的发布者的事件。因此,事件代理会自动构建一个范围。

这是在应用程序中拥有多个事件处理范围的最简单解决方案,应始终优先考虑。然而,有时您需要在单个事件代理内的对象中更精确地控制范围。这种情况将在下一节中介绍。

分层命名

发布者和订阅者可以通过实现 INamedItem 接口来命名。该接口提供一个单一的属性

string EventBrokerItemName { get; }

这允许在事件代理中标识一个对象,而发布和订阅属性始终绑定到类。

命名是分层的,使用与命名空间相同的方案

  • TestTest.MyPublisher1 的父级
  • Test.MyName1Test.MyName2 的同级
  • Test.MyName1.SubnameTest.MyName1 的子级
  • Test.MyName1Test.MyName1 的同卵双胞胎(两个同名对象是同卵双胞胎)

现在,发布者可以通过在发布属性中定义范围来定义它要发布事件的范围

[EventPublication("Topic")]
public event EventHandler PublishedGlobally;

[EventPublication("Topic", typeof(ScopeMatchers.PublishToParents)]
public event EventHandler PublishedToParentsAndTwinsOnly;

[EventPublication("Topic", typeof(ScopeMatchers.PublishToChildren)]
public event EventHandler PublishedToChildrenAndTwinsOnly;

第一个事件是全局事件,所有订阅者都可以接收。第二个事件仅传递给发布者的父级或同卵双胞胎订阅者。第三个事件仅传递给发布者的子级或同卵双胞胎订阅者。

订阅者可以相应地定义它要接收事件的范围

[EventSubscription("Topic", typeof(Handlers.Publisher)]
public void GlobalHandler(object sender, EventArgs e)

[EventSubscription(
    "Topic", 
    typeof(Handlers.Publisher), 
    typeof(ScopeMatchers.SubscribeToParents)]
public void ParentHandler(object sender, EventArgs e)

[EventSubscription(
    "Topic", 
    typeof(Handlers.Publisher), 
    typeof(ScopeMatchers.SubscribeToChildren)]
public void ChildrenHandler(object sender, EventArgs e)

第一个订阅是全局订阅,将处理传递给它的所有事件。仅当订阅者的父级触发事件时,第二个订阅才会被调用。仅当订阅者的子级触发事件时,第三个订阅才会被调用。

同卵双胞胎(同名但不同的对象)会得到特殊处理。同卵双胞胎总是与其同卵双胞胎的父级和子级,因此将始终接收其同卵双胞胎的所有事件。

与 CAB EventBroker 的比较

背景部分所述,我的 EventBroker 基于 Microsoft 的 Practices and Patterns 组的 CAB(Composite UI Application Block)中的 EventBroker

本节描述了差异。

Standalone

bbv.Common EventBroker 可独立使用。无论何时需要通知,您都可以在项目中使用它,没有任何框架限制,而 CAB 会强制要求。

开发人员指南

我尝试以一种尽可能早地暴露错误的方式实现 EventBroker。我将举例说明这意味着什么

  • 如果发布的事件提供的 EventHandler 类型与订阅处理程序提供的签名不匹配,则会在注册时抛出异常,而不是在事件触发时。
  • 如果您使用 UserInterfaceUserInterfaceAsync 订阅处理程序,并且当前线程不是用户界面线程(即不存在 WindowsFormsSynchronizationContext),则会在注册时抛出异常。这会导致跨线程异常。

订阅处理程序限制

注意:此功能仅在托管在 Sourceforge.net 上的版本中可用(请参阅下载部分),而不在附加的解决方案中(它太新了 ;-))。

我们遇到了一个问题,某些发布者必须确保所有订阅者同步处理其事件。例如,如果您发布一个取消事件,为所有订阅者提供取消当前操作的方法。只有当没有订阅者将 CancelEventArgs 上的 Cancel 属性设置为 true 时,发布者才能继续其操作。因此,发布者必须将此事件的所有订阅限制为同步。

[EventPublication("test", HandlerRestriction.Synchronous)]
public event EventHandler AnEvent;

如果订阅者为此事件注册了异步处理程序,则会抛出异常。请注意,异常是在注册时抛出的,而不是在事件触发时。这大大简化了编写一致的代码。

此外,发布者可以限制订阅处理程序为异步,因为发布者不想被阻塞。

[EventPublication("test", HandlerRestriction.Asynchronous)]
public event EventHandler AnEvent;

日志记录

bbv.CommonEventBroker 在日志消息方面相当“健谈”。这使您能够看到发布者何时触发事件,它们如何路由到订阅者,如何处理它们,以及发生了哪些异常。

注意:bbv.Common 使用 log4net 来记录消息。这意味着您可以为每个组件配置要记录的消息级别。

可扩展性

ThreadOption --> Handler

我用可扩展的 Handlers 替换了 enum ThreadOption,以定义事件的处理方式(同步、异步)。

Scope --> Scope Matcher

您可以实现自己的范围匹配器,以提供适合您需求的事件层次结构,而不是使用 enum 指定范围。

内部

下次更新本文时,我们将深入探讨其内部机制。在此之前,请参考下载中提供的源代码。

下载

本文顶部的下载包含三个 Visual Studio 2008 项目

  1. bbv.Common.EventBroker:事件代理组件。
  2. bbv.Common.EventBroker.Test:单元测试(NUnit)。
  3. bbv.Common.EventBroker.Sample:一个小型示例应用程序(事件代理的基本用法)。

bbv.Common.EventBroker 是一个更大的库的一部分,其中包含其他一些很棒的组件,可以在 这里 找到。

此下载只是一个精简版。要试用,请打开解决方案,将启动项目设置为bbv.Common.EventBroker.Sample,然后按 F5。

请注意,附加到本文的版本不是最新版本。请务必查看 Sourceforge 页面以获取最新版本。

许可证

请注意,bbv.Common.EventBroker 在 Apache License 2.0 下许可,但由于 EventBroker 包含 Microsoft CAB 的部分内容,因此必须应用额外的许可限制,有关详细信息,请参阅源代码中的文件头。

历史

  • 2008-10-11 - 初始版本。
  • 2008-10-19 - 添加了与 CAB EventBroker 的比较,添加了指向 Sourceforge.net 的链接。
  • 2008-10-26 - 添加了直接在 EventBroker 上触发事件主题部分。
© . All rights reserved.