简体   繁体   中英

what's the best way to implement chained events in C#

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

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.

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:

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.

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.

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:

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 += (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.

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 Foo s are loaded?), for example.

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.

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM