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

时间:2022-09-02 11:16:48

I have the following scenario. The client code only has access to FooHandler, not directly to Foo instances.

我有以下场景。客户端代码只能访问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);
    }

}

Then the clients would use the OneFooLoaded event of the FooHandler class to get notifications of loading of foos. Is this 'event chaining' the right thing to do? Are there any alternatives? I don't like this (it feels wrong, I cannot precisely express why), but if I want the handler to be the point of access I don't seem to have many alternatives.

然后客户端将使用FooHandler类的OneFooLoaded事件来获取加载foos的通知。这个'事件链接'是正确的吗?还有其他选择吗?我不喜欢这个(感觉不对,我无法准确地表达原因),但如果我希望处理程序成为访问点,我似乎没有很多选择。

5 个解决方案

#1


If it feels wrong because events are more sophisticated and outward-facing than necessary for your internal communications (which I believe is at least partially true considering events can call multiple clients whereas you know you only need to notify one, right?), then I propose the following alternative. Instead of using events to communicate the completion of Foo to FooHandler, since Foo is internal anyway, you could add a callback parameter to the constructor or Load method of Foo, which Foo can call when it is done loading. This parameter can be just a function if you only have one callback, or it can be an interface if you have many. Here's how I think your code would look with the simplified internal interface:

如果感觉不对,因为事件比内部通信所需的更复杂,更外向(我认为至少部分是真的,考虑到事件可以调用多个客户,而你知道你只需要通知一个,对吧?),那么我提出以下备选方案。而不是使用事件来传递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);
  }

}

Notice that this also allows you to be more strongly typed with the FooLoaded delegate.

请注意,这也允许您使用FooLoaded委托更强类型。

If, on the other hand, it feels wrong because the event shouldn't have to go through FooHandler to get to the client, then 1) I would dispute that because if the client doesn't want to deal with the individual Foo objects, it shouldn't be sinking events from them at that level either, and 2) If you really wanted to do that, you could implement some public callback interface on Foo even though Foo is private, or use a mechanism like Pavel suggested. I think, however, that clients like the simplicity of implementing fewer event handlers and distinguishing the source within the one handler rather than having to connect (and potentially disconnect) events from dozens of smaller objects.

另一方面,如果感觉错误,因为事件不应该通过FooHandler到达客户端,那么1)我会争议,因为如果客户端不想处理单个Foo对象,它也不应该是在那个级别下沉它们的事件,2)如果你真的想这样做,你可以在Foo上实现一些公共回调接口,即使Foo是私有的,或者使用像Pavel建议的机制。但是,我认为客户端喜欢简单地实现更少的事件处理程序,并在一个处理程序中区分源,而不是必须连接(并可能断开)来自几十个较小对象的事件。

#2


A different way to do it is to create a single point (a single class) in the domain where all events go through. Any classes using the domain would hook up to that class, which has a list of static events and any internal class event in the domain would be listened to by this class, thereby avoiding event chaining in the domain at least.

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

References:

#3


Couple of tips that may or may not be helpful...

一些可能有帮助或可能没有帮助的提示......

Write the event declaration like this:

像这样写下事件声明:

public event FooLoaded loaded = delegate {};

That way you can safely fire it even if no clients have enlisted.

这样,即使没有客户入伍,您也可以安全地开火。

On the subject of chaining events, when you have two events:

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

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

You may want the firing of b to also cause the firing of a:

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

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

And then you might look at that and think it would be more succinct to say:

然后你可能会看到这一点,并认为它会更简洁地说:

b += a;

Indeed, Resharper may even suggest it to you! But it means something completely different. It appends the current contents of a to b, so if later more handlers enlist with a, this will not cause them to be called when b is fired.

事实上,Resharper甚至可以向你推荐它!但它意味着完全不同的东西。它将a的当前内容附加到b,因此如果稍后有更多处理程序使用a,则不会导致在触发b时调用它们。

#4


I can tell you that this kind of event waterfall is something I've arrived at fairly naturally on several occasions, and I've yet to encounter a serious problem with them.

我可以告诉你,这种事件瀑布是我在很多时候相当自然地得到的东西,我还没有遇到过它们的严重问题。

Although I don't think I've ever passed on events transparently, but always with a semantic change. FooLoaded would become AllFoosLoaded, for example. If you want to impose such a semantic change simply for the sake of it, you could change OneFooLoaded to a percentage indicator (does the receiving class need to know how many Foos are loaded?), for example.

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

I think that constructions like these feel wrong because an event is meant for broadcasting. It doesn't really impose a contract on the class that broadcasts it, nor does it impose a contract on the class subscribing to it.

我认为像这样的结构是错误的,因为一个事件是为了广播。它并没有真正对播放它的课程施加合同,也没有在订阅它的类别上签订合同。

Façade classes and the general principles of information hiding, however, are designed to facilitate the enforcement of contracts.

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

I'm still collecting my thoughts on the issue, sorry if the above is a bit unclear, but I don't know if there is a better way to do what you want. And if there is, I'm as interested to see it as you are.

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

#5


You could delegate add and remove on the event, instead of 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(); 
        } 
    }
}

The above assumes that FooList is immutable for the lifetime of FooHandler. If it's mutable, then you'll also have to track addition / deletion of items to it, and add / remove handlers accordingly.

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

#1


If it feels wrong because events are more sophisticated and outward-facing than necessary for your internal communications (which I believe is at least partially true considering events can call multiple clients whereas you know you only need to notify one, right?), then I propose the following alternative. Instead of using events to communicate the completion of Foo to FooHandler, since Foo is internal anyway, you could add a callback parameter to the constructor or Load method of Foo, which Foo can call when it is done loading. This parameter can be just a function if you only have one callback, or it can be an interface if you have many. Here's how I think your code would look with the simplified internal interface:

如果感觉不对,因为事件比内部通信所需的更复杂,更外向(我认为至少部分是真的,考虑到事件可以调用多个客户,而你知道你只需要通知一个,对吧?),那么我提出以下备选方案。而不是使用事件来传递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);
  }

}

Notice that this also allows you to be more strongly typed with the FooLoaded delegate.

请注意,这也允许您使用FooLoaded委托更强类型。

If, on the other hand, it feels wrong because the event shouldn't have to go through FooHandler to get to the client, then 1) I would dispute that because if the client doesn't want to deal with the individual Foo objects, it shouldn't be sinking events from them at that level either, and 2) If you really wanted to do that, you could implement some public callback interface on Foo even though Foo is private, or use a mechanism like Pavel suggested. I think, however, that clients like the simplicity of implementing fewer event handlers and distinguishing the source within the one handler rather than having to connect (and potentially disconnect) events from dozens of smaller objects.

另一方面,如果感觉错误,因为事件不应该通过FooHandler到达客户端,那么1)我会争议,因为如果客户端不想处理单个Foo对象,它也不应该是在那个级别下沉它们的事件,2)如果你真的想这样做,你可以在Foo上实现一些公共回调接口,即使Foo是私有的,或者使用像Pavel建议的机制。但是,我认为客户端喜欢简单地实现更少的事件处理程序,并在一个处理程序中区分源,而不是必须连接(并可能断开)来自几十个较小对象的事件。

#2


A different way to do it is to create a single point (a single class) in the domain where all events go through. Any classes using the domain would hook up to that class, which has a list of static events and any internal class event in the domain would be listened to by this class, thereby avoiding event chaining in the domain at least.

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

References:

#3


Couple of tips that may or may not be helpful...

一些可能有帮助或可能没有帮助的提示......

Write the event declaration like this:

像这样写下事件声明:

public event FooLoaded loaded = delegate {};

That way you can safely fire it even if no clients have enlisted.

这样,即使没有客户入伍,您也可以安全地开火。

On the subject of chaining events, when you have two events:

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

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

You may want the firing of b to also cause the firing of a:

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

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

And then you might look at that and think it would be more succinct to say:

然后你可能会看到这一点,并认为它会更简洁地说:

b += a;

Indeed, Resharper may even suggest it to you! But it means something completely different. It appends the current contents of a to b, so if later more handlers enlist with a, this will not cause them to be called when b is fired.

事实上,Resharper甚至可以向你推荐它!但它意味着完全不同的东西。它将a的当前内容附加到b,因此如果稍后有更多处理程序使用a,则不会导致在触发b时调用它们。

#4


I can tell you that this kind of event waterfall is something I've arrived at fairly naturally on several occasions, and I've yet to encounter a serious problem with them.

我可以告诉你,这种事件瀑布是我在很多时候相当自然地得到的东西,我还没有遇到过它们的严重问题。

Although I don't think I've ever passed on events transparently, but always with a semantic change. FooLoaded would become AllFoosLoaded, for example. If you want to impose such a semantic change simply for the sake of it, you could change OneFooLoaded to a percentage indicator (does the receiving class need to know how many Foos are loaded?), for example.

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

I think that constructions like these feel wrong because an event is meant for broadcasting. It doesn't really impose a contract on the class that broadcasts it, nor does it impose a contract on the class subscribing to it.

我认为像这样的结构是错误的,因为一个事件是为了广播。它并没有真正对播放它的课程施加合同,也没有在订阅它的类别上签订合同。

Façade classes and the general principles of information hiding, however, are designed to facilitate the enforcement of contracts.

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

I'm still collecting my thoughts on the issue, sorry if the above is a bit unclear, but I don't know if there is a better way to do what you want. And if there is, I'm as interested to see it as you are.

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

#5


You could delegate add and remove on the event, instead of 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(); 
        } 
    }
}

The above assumes that FooList is immutable for the lifetime of FooHandler. If it's mutable, then you'll also have to track addition / deletion of items to it, and add / remove handlers accordingly.

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