Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

One time generic event call?

Tags:

c#

What I am trying to achieve is to create convenient syntax for calling a callback on an event only once.

public static class Extensions
{
    public static void Once<T>(this EventHandler<T> @event, EventHandler<T> callback)
    {
        EventHandler<T> cb = null;
        cb = (object that, T info) => {
            if (callback != null) {
                callback(that, info);
            }
            @event -= cb;
        };
        @event += cb;
    }
}

This should allow us to write something like this:

obj.OnSomething.Once((sender, obj) => Console.WriteLine(obj));

Note that OnSomething is an event EventHandler of some type.

The problem is that I get the error message:

Error CS0070: The event SharpTox.Core.Tox.OnFriendMessage' can only appear on the left hand side of += or -= when used outside of the type SharpTox.Core.Tox' (CS0070) (SharpTox.Tests)

Is there no way to achieve this that easily? Do I have to remove event from OnSomething and make it a simple delegate?

like image 268
Andrius Bentkus Avatar asked Jan 09 '23 04:01

Andrius Bentkus


2 Answers

Unfortunately, you can't have a nice syntax here. Like the error message says, all you can do with an event field from outside of its defining class is referencing it at the left of += or -=.

Here's the method that Rx uses to bind an observable to an event:

var observable = Observable.FromEventPattern(
    handler => obj.OnSomething += handler,
    handler => obj.OnSomething -= handler);

Basically, FromEventPattern is a helper that takes two lambdas: one for subscription, and the other for unsubscription. You could use a similar pattern, or just use Rx to achieve the same effect:

Observable.FromEventPattern(h => obj.OnSomething += h, h => obj.OnSomething -= h)
          .Take(1)
          .Subscribe(e => ...);

On a side note, this will keep a reference to obj from the lambda used in Subscribe (there are intermediary glue objects but this is irrelevant). This means that if the event is never called, and if the lambda is long-lived, then obj won't be eligible for GC (a situation called event memory leak). This may or may not be a problem for your case.

like image 82
Lucas Trzesniewski Avatar answered Jan 18 '23 21:01

Lucas Trzesniewski


An alternative approach would be to return the callback. In the extension method the callback is deleted/removed. The drawback of this approach is still the fact that the event handler function needs to be defined separately and can not be a lambda.

The extension method:

public static class Extensions
{   
    public static EventHandler<T> Once<T>(this Action<Object, T> callback)
    {
        return (Object s, T e) => 
        {
            if (callback != null) {
                callback(s, e);
                callback = null;
            }
        };
    }
}

A demo class that has different events:

public class Demo
{
    public event EventHandler<String> StringEvent = null;
    public event EventHandler<Int32> IntEvent = null;

    public void NotifyOnWork()
    {
        for (int i = 0; i < 5; i++)
        {
            Console.WriteLine(i);
            if (this.StringEvent != null) { this.StringEvent(this, i.ToString()); }
            if (this.IntEvent != null) { this.IntEvent(this, i); }
        }
    }
}

Usage of the Demo class:

var demo = new Demo();
Action<Object, String> handlerString = (s, e) =>  { Console.WriteLine("echo string {0}", e); };
Action<Object, Int32> handlerInt = (s, e) =>  { Console.WriteLine("echo int {0}", e); };

demo.StringEvent += handlerString.Once();
demo.IntEvent += handlerInt.Once();
demo.StringEvent += (s, e) => Console.WriteLine("i = {0}", e); 
demo.NotifyOnWork();

And the output is:

0
echo string 0
i = 0
echo int 0
1
i = 1
2
i = 2
3
i = 3
4
i = 4
like image 38
keenthinker Avatar answered Jan 18 '23 22:01

keenthinker