A constructor that accepts any delegate as a parameter

Here is a simplified case. I have a class that stores a delegate, which it calls completion:

public class Animation { public delegate void AnimationEnd(); public event AnimationEnd OnEnd; } 

I have another utility class that I want to subscribe to different delegates. By construction, I want me to register for the delegate, but other than that, he doesn't care about the type. The fact is, I don’t know how to express this in a type system. Here is my pseudo- C#

 public class WaitForDelegate { public delegateFired = false; // How to express the generic type here? public WaitForDelegate<F that a delegate>(F trigger) { trigger += () => { delegateFired = true; }; } } 

Thanks in advance!


Thanks to Alberto Monteiro, I just use System.Action as an event type. My question is how to pass the event to the constructor so that it can register itself? This can be a very stupid question.

 public class Example { Animation animation; // assume initialized public void example() { // Here I can't pass the delegate, and get an error like // "The event can only appear on the left hand side of += or -=" WaitForDelegate waiter = new WaitForDelegate(animation.OnEnd); } } 
+6
source share
2 answers

I'm afraid you cannot do what you ask.

First, you cannot limit yourself to delegates. Closest code to legal C #:

 public class WaitForDelegate<F> where F : System.Delegate { public bool delegateFired = false; public WaitForDelegate(F trigger) { trigger += () => { delegateFired = true; }; } } 

But it will not compile.

But the big problem is that you still can't delegate delegates.

Consider this simplified class:

 public class WaitForDelegate { public WaitForDelegate(Action trigger) { trigger += () => { Console.WriteLine("trigger"); }; } } 

Then I will try to use it as follows:

 Action bar = () => Console.WriteLine("bar"); var wfd = new WaitForDelegate(bar); bar(); 

The only result of this is:

 bar 

The word trigger not displayed. This is because delegates are copied by value, so the string trigger += () => { Console.WriteLine("trigger"); }; trigger += () => { Console.WriteLine("trigger"); }; binds the handler only to trigger , not to bar .

The way you can do all this work is to stop using events and use Microsoft Reactive Extensions (NuGet "Rx-Main"), which allows you to convert events to LINQ-based IObservable<T> instances that can be passed .

Here's how my sample code above works:

 public class WaitForDelegate { public WaitForDelegate(IObservable<Unit> trigger) { trigger.Subscribe(_ => { Console.WriteLine("trigger"); }); } } 

And you now call it like this:

 Action bar = () => Console.WriteLine("bar"); var wfd = new WaitForDelegate(Observable.FromEvent(h => bar += h, h => bar -= h)); bar(); 

Now it outputs the output:

 bar trigger 

Note that the Observable.FromEvent call contains code to attach and detach a handler in an area that has access to this. This allows you to disable the final subscriber call with the .Dispose() call.

I made this class pretty simple, but a more complete version would be like this:

 public class WaitForDelegate : IDisposable { private IDisposable _subscription; public WaitForDelegate(IObservable<Unit> trigger) { _subscription = trigger.Subscribe(_ => { Console.WriteLine("trigger"); }); } public void Dispose() { _subscription.Dispose(); } } 

An alternative if you do not want to use Rx completely is the following:

 public class WaitForDelegate : IDisposable { private Action _detach; public WaitForDelegate(Action<Action> add, Action<Action> remove) { Action handler = () => Console.WriteLine("trigger"); _detach = () => remove(handler); add(handler); } public void Dispose() { if (_detach != null) { _detach(); _detach = null; } } } 

You call it this way:

 Action bar = () => Console.WriteLine("bar"); var wfd = new WaitForDelegate(h => bar += h, h => bar -= h); bar(); 

This still draws the correct conclusion.

+4
source

.NET already has a delegate that does not receive any parameters, this is Action

So you have an animation class:

 public class Animation { public event Action OnEnd; } 

But you can pass events as parameters if you try to get this compilation error

An event can only appear on the left side + = or - = "

So, let's create an interface and declare an event there

 public interface IAnimation { event Action OnEnd; } 

Using the interface approach, you do not have external dependencies, and you can have many classes that implement it, it is also good practice, depends on abstractions instead of specific types. There is an abbreviation SOLID, which explains 5 principles for improving OO code.

And then your animation class implements that

Designation: CallEnd method is for testing only.

 public class Animation : IAnimation { public event Action OnEnd; public void CallEnd() { OnEnd(); } } 

And now you WaitForDelegate will get IAnimation , so the class can handle any class that implements the IAnimation class

 public class WaitForDelegate<T> where T : IAnimation { public WaitForDelegate(T animation) { animation.OnEnd += () => { Console.WriteLine("trigger"); }; } } 

Then we can test the code we made with the following code

  public static void Main(string[] args) { var a = new Animation(); var waitForDelegate = new WaitForDelegate<IAnimation>(a); a.CallEnd(); } 

Result

trigger

Here is the working version on dotnetfiddle

https://dotnetfiddle.net/1mejBL

Important tip

If you are working with multithread, you should take some precaution to avoid a Null Reference Exception

Look again at the CallEnd method that I added for testing

 public void CallEnd() { OnEnd(); } 

The OnEnd event may not matter, and then if you try to raise it, you will get a Null Reference Exception.

So, if you use C # 5 or below, do something like this

 public void CallEnd() { var @event = OnEnd; if (@event != null) @event(); } 

With C # 6 it could be like

 public void CallEnd() => OnEnd?.Invoke(); 

More explanation, you could have this code

 public void CallEnd() { if (OnEnd != null) OnEnd(); } 

This code, which is above, will probably make you think that you are safe from a Null Reference Exception, but with a multi-threaded solution, you are not. This is because the OnEnd event can be set to zero between the execution if (OnEnd != null) and OnEnd();

There is a good article by John Skeet about this, you cannot see Clear an event handler call using C # 6

+2
source

All Articles