如何在C#中实现链式事件最佳方式

7
我有以下情况。客户端代码只能访问FooHandler,而不能直接访问Foo实例。
public delegate void FooLoaded(object sender, EventArgs e);

class Foo {
    public event FooLoaded loaded;
    /* ... some code ... */
    public void Load() { load_asynchronously(); }
    public void callMeWhenLoadingIsDone() { loaded(this,EventArgs.Empty); }
}

class FooHandler {
    public event FooLoaded OneFooLoaded;

    /* ... some code ... */

    public void LoadAllFoos() { 
        foreach (Foo f in FooList) { 
            f.loaded += new FooLoaded(foo_loaded);
            f.Load(); 
        } 
    }

    void foo_loaded(object sender, EventArgs e) {
        OneFooLoaded(this, e);
    }

}

然后客户端将使用FooHandler类的OneFooLoaded事件来获取foos的加载通知。这种“事件链接”是正确的做法吗?有没有其他选择?我不喜欢这种方式(感觉不对,但我无法准确地表达为什么),但如果我想让处理程序成为访问点,似乎没有太多选择。

5个回答

4

如果感觉事件比内部通信需要更复杂和面向外部(我相信这至少在某种程度上是正确的,因为事件可以调用多个客户端,而您只需要通知一个客户端,对吧?),那么我提出以下替代方案。由于Foo本来就是内部的,所以您可以将回调参数添加到Foo的构造函数或Load方法中,以便Foo在加载完成后调用。如果您只有一个回调,则该参数可以仅为函数,如果您有多个回调,则可以是接口。以下是我认为使用简化的内部接口时代码的样子:

public delegate void FooLoaded(FooHandler sender, EventArgs e);

class Foo
{
  Action<Foo> callback;
  /* ... some code ... */
  public void Load(Action<Foo> callback) { this.callback = callback; load_asynchronously(); }
  public void callMeWhenLoadingIsDone() { callback(this); }
}

class FooHandler
{
  public event FooLoaded OneFooLoaded;

  /* ... some code ... */

  public void LoadAllFoos()
  {
     foreach (Foo f in FooList)
     {
        f.Load(foo_loaded);
     }
  }

  void foo_loaded(Foo foo)
  {
     // Create EventArgs based on values from foo if necessary
     OneFooLoaded(this, null);
  }

}

请注意,这也使您可以更加强类型化FooLoaded委托。
另一方面,如果感觉不对,因为事件不应该通过FooHandler传递到客户端,那么1)我会质疑,因为如果客户端不想处理单个Foo对象,则在该级别不应该从它们中接收事件,2)如果您真的想要这样做,即使Foo是私有的,也可以在Foo上实现一些公共回调接口,或者使用像Pavel建议的机制。然而,我认为,客户端喜欢实现较少的事件处理程序并在一个处理程序中区分来源,而不是必须连接(并可能断开连接)来自数十个较小对象的事件。

我希望在开始当前正在进行的项目之前已经看到了这一点,这使得我的工作变得更简单(也更容易进行单元测试)。 - ForbesLindesay

3

我刚发现这个,认为它非常棒,比起试图连接2或3个事件并且让代码变得混乱要简单得多。 - Calanus

2
一些可能有用的技巧...
将事件声明写成这样:
```html

像这样编写事件声明:

```
public event FooLoaded loaded = delegate {};

这样,即使没有客户报名,您也可以安全地解雇它。

关于事件链式调用,当您有两个事件:

public event EventHandler a = delegate {};
public event EventHandler b = delegate {};

您可能希望b的触发也导致a的触发:

b += (s, e) => a(s, e);

然后你可能会想,更简洁的表达方式是:

b += a;

事实上, Resharper 可能会向您建议这样做!但它意味着完全不同的事情。 它将 a当前内容追加到 b,因此,如果稍后有更多的处理程序在 a 注册,这不会导致它们在触发 b 时被调用。


1

你可以将事件的addremove委托出去,而不是使用raises关键字:

class FooHandler {
    public event FooLoaded OneFooLoaded {
       add { 
           foreach (Foo f in FooList) {
               f.loaded += new FooLoaded(value);
           }
       }
       remove {
           foreach (Foo f in FooList) {
               f.loaded -= new FooLoaded(value);
           }
       }
    }

    public void LoadAllFoos() { 
        foreach (Foo f in FooList) { 
            f.Load(); 
        } 
    }
}

以上假设在FooHandler的生命周期内,FooList是不可变的。如果它是可变的,那么您还需要跟踪添加/删除其中的项目,并相应地添加/删除处理程序。

1

我可以告诉你,这种“事件瀑布”在几个场合下我都自然而然地采用过,并且我还没有遇到过什么严重的问题。

虽然我不认为我曾经透明地传递过事件,但总是带有语义变化。例如,FooLoaded会变成AllFoosLoaded。如果你想为了简单而强制实施这样的语义变化,你可以将OneFooLoaded更改为百分比指示器(接收类需要知道加载了多少个Foo吗?)。

我认为这样的结构感觉不对,因为event是用于广播的。它并没有真正对广播它的类强制执行契约,也没有对订阅它的类强制执行契约。

然而,外观类和信息隐藏的一般原则旨在促进契约的执行。

我仍在整理我的思路,如果上面的内容有点不清楚,我很抱歉,但我不知道是否有更好的方法来实现你想要的。如果有的话,我和你一样感兴趣。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接