Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Constructor that takes any delegate as a parameter

Tags:

c#

.net

delegates

Here's the simplified case. I have a class that stores a delegate that it will call on completion:

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

I have another utility class that I want to subscribe to various delegates. On construction I want itself to register to the delegate, but other than that it doesn't care about the type. The thing is, I don't know how to express that in the type system. Here's my pseudo-C#

public class WaitForDelegate
{
    public delegateFired = false;

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

Thanks in advance!


Thanks to Alberto Monteiro, I just use System.Action as the type for the event. My question now is, how to pass the event to the constructor so it can register itself? This might be a very dumb 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);
    }
}
like image 930
Alexander Kondratskiy Avatar asked Jan 10 '16 01:01

Alexander Kondratskiy


2 Answers

I'm afraid you can't do what you're asking.

First up, you can't constrain by delegates. The closest code to legal C# is this:

public class WaitForDelegate<F> where F : System.Delegate
{
    public bool delegateFired = false;

    public WaitForDelegate(F trigger)
    {
        trigger += () => { delegateFired = true; };
    }
}

But it won't compile.

But the bigger problem is that you can't pass delegates around like this anyway.

Consider this simplified class:

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

I then try to use it like this:

Action bar = () => Console.WriteLine("bar");

var wfd = new WaitForDelegate(bar);

bar();

The only output from this is:

bar

The word trigger doesn't appear. This is because delegates are copied by value so that the line trigger += () => { Console.WriteLine("trigger"); }; is only attaching the handler to trigger and not bar at all.

The way that you can make all of this work is to stop using events and use Microsoft's Reactive Extensions (NuGet "Rx-Main") which allows you to turn events into LINQ-based IObservable<T> instances that can get passed around.

Here's how my example code above would then work:

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

And you now call it like:

Action bar = () => Console.WriteLine("bar");

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

bar();

This now produces the output:

bar
trigger

Notice that the Observable.FromEvent call contains the code to attach and detach the handler in a scope that has access to do so. It allows the final subscription call to be unattached with a call to .Dispose().

I've made this class quite simple, but a more complete version would be 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 don't want to go for the full use of Rx is to do this:

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 like this:

Action bar = () => Console.WriteLine("bar");

var wfd = new WaitForDelegate(h => bar += h, h => bar -= h);

bar();

That still does the correct output.

like image 96
Enigmativity Avatar answered Oct 03 '22 04:10

Enigmativity


In .NET there is already a delegate that doesn't receive no parameters, it is the Action

So you Animation class could be like that:

public class Animation
{
     public event Action OnEnd;
}

But you can pass events as parameters, if you try that you will receive this compilation error

The event can only appear on the left hand side of += or -="

So lets create a interface, and declare the event there

public interface IAnimation
{
    event Action OnEnd;
}

Using the interface approach you have no external dependencies and you can have many classes that implements that, also is a good practice, depends of abstractions instead concrete types. There is acronym called SOLID that explain 5 principles about better OO code.

And then your animation class implements that

Obs.: The CallEnd method is just for test purpose

public class Animation : IAnimation
{
    public event Action OnEnd;

    public void CallEnd()
    {
        OnEnd();
    }
}

And now you WaitForDelegate will receive a 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 that we did with the following code

    public static void Main(string[] args)
    {
        var a = new Animation();

        var waitForDelegate = new WaitForDelegate<IAnimation>(a);

        a.CallEnd();
    }

The result is

trigger

Here is the working version on dotnetfiddle

https://dotnetfiddle.net/1mejBL

Important tip

If you are working with multithread, you must take some caution to avoid Null Reference Exception

Let's look again the CallEnd method that I've added for test

public void CallEnd()
{
    OnEnd();
}

OnEnd event could have not value, and then if you try to call it, you will receive Null Reference Exception.

So if you are using C# 5 or lower, do something like this

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

With C# 6 it could be like that

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

More explanation, you could have this code

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

This code that is above, probably make you think that you are safe from Null Reference Exception, but with multithread solution, you aren't. That's because the OnEnd event could be set to null between the execution of if (OnEnd != null) and OnEnd();

There is a nice article by Jon Skeet about it, you cann see Clean event handler invocation with C# 6

like image 20
Alberto Monteiro Avatar answered Oct 03 '22 04:10

Alberto Monteiro