什么是在C#中实现链式事件的最佳方式
我有以下情况。 客户端代码只能访问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的通知。 这个’事件链接’是正确的吗? 还有其他选择吗? 我不喜欢这个(感觉不对,我无法准确地表达原因),但如果我希望处理程序成为访问点,我似乎没有很多选择。
如果感觉不对,因为事件比内部通信所需的更复杂和外向(我相信至少部分是真的,因为事件可以调用多个客户端,而你知道你只需要通知一个,对吧?),那么我提出以下备选方案。 而不是使用事件来传递Foo到FooHandler的完成,因为无论如何Foo都是内部的,你可以在构造函数或Foo的Load方法中添加一个回调参数,Foo在加载完成后可以调用它。 如果您只有一个回调,则此参数可以只是一个函数,如果您有多个回调,它可以是一个接口。 以下是我认为您的代码在简化的内部界面中的外观:
public delegate void FooLoaded(FooHandler sender, EventArgs e); class Foo { Action callback; /* ... some code ... */ public void Load(Action 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建议的机制。 但是,我认为客户端喜欢简化实现较少的事件处理程序并在一个处理程序中区分源代码而不必连接(并可能断开)来自几十个较小对象的事件。
另一种方法是在域中创建所有事件通过的单个点(单个类)。 使用该域的任何类都将连接到该类,该类具有静态事件列表,并且该类将侦听域中的任何内部类事件,从而至少避免域中的事件链接。
参考文献:
- Udi Dahan在域事件上有代码示例
- Martin Fowler对领域事件更为一般
一些可能有帮助或可能没有帮助的提示……
像这样写下事件声明:
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
,则不会导致在触发b
时调用它们。
我可以告诉你,这种事件瀑布是我在很多时候相当自然地得到的东西,我还没有遇到过它们的严重问题。
虽然我认为我没有透明地传递事件,但总是会发生语义变化。 例如, FooLoaded
将成为AllFoosLoaded
。 如果你想仅仅为了它而强加这样的语义变化,你可以将OneFooLoaded
更改为百分比指示符(例如,接收类是否需要知道加载了多少个Foo
?)。
我认为像这样的结构感觉不对,因为一个event
是为了广播。 它并没有真正对播放它的课程施加合同,也没有在订阅它的类别上签订合同。
然而,外观类和信息隐藏的一般原则旨在促进合同的执行。
我仍在收集我对这个问题的想法,对不起,如果上面有点不清楚,但我不知道是否有更好的方法来做你想要的。 如果有,我有兴趣看到你的样子。
您可以在事件上委派add
和remove
,而不是引发:
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(); } } }
以上假设FooList
在FooHandler
的生命周期中是不可变的。 如果它是可变的,那么你还必须跟踪项目的添加/删除,并相应地添加/删除处理程序。