什么是在C#中实现链式事件的最佳方式

时间:2009-07-12 08:08:36

标签: c# .net events

我有以下情况。客户端代码只能访问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 个答案:

答案 0 :(得分:4)

如果感觉不对,因为事件比内部通信所需要的更复杂和外向(我认为至少部分是真的,因为事件可以调用多个客户端,而你知道你只需要通知一个,对吧?) ,然后我提出以下备选方案。而不是使用事件来传递Foo到FooHandler的完成,因为无论如何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建议的机制。但是,我认为客户喜欢简化实现较少的事件处理程序并在一个处理程序中区分源代码而不必连接(并可能断开)来自几十个较小对象的事件。

答案 1 :(得分:3)

另一种方法是在所有事件通过的域中创建单个点(单个类)。使用该域的任何类都将连接到该类,该类具有静态事件列表,并且该类将侦听域中的任何内部类事件,从而至少避免域中的事件链接。

参考文献:

答案 2 :(得分:2)

一些可能有用或可能无用的提示......

写下这样的事件声明:

public event FooLoaded loaded = delegate {};

即使没有客户入伍,你也可以放心地开火。

关于链接事件的主题,当你有两个事件时:

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

你可能希望射击b也会导致射击:

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

然后你可以看一下,并认为说:

会更简洁
b += a;

确实,Resharper甚至可能会向你推荐它!但它意味着完全不同的东西。它会将a当前内容附加到b,因此如果以后有更多处理程序使用a,则不会导致{{1}时调用它们被解雇了。

答案 3 :(得分:1)

我可以告诉你,这种事件瀑布是我多次自然而然地遇到的事情,我还没有遇到过严重的问题。

虽然我认为我没有透明地传递事件,但总是会发生语义变化。例如,FooLoaded将成为AllFoosLoaded。如果你想仅仅为了它而强加这样的语义变化,你可以将OneFooLoaded更改为百分比指示符(接收类是否需要知道加载了多少Foo?),for示例

我认为像这样的结构感觉不对,因为event用于广播。它并没有真正对播放它的班级施加合同,也没有在订阅它的班级上签订合同。

然而,外观课程和信息隐藏的一般原则旨在促进合同的执行。

我仍在收集我对这个问题的想法,对不起,如果上面有点不清楚,但我不知道是否有更好的方法来做你想要的。如果有的话,我有兴趣看到它就像你一样。

答案 4 :(得分:1)

您可以在活动上委派addremove,而不是加注:

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(); 
        } 
    }
}

以上假设FooListFooHandler的生命周期内是不可变的。如果它是可变的,那么你还必须跟踪项目的添加/删除,并相应地添加/删除处理程序。