13

I'm fairly convinced that this isn't possible, but I'm going to ask nonetheless.

In order to make a single-shot subscription to events, I frequently find myself using this (self-invented) pattern:

EventHandler handler=null;
handler = (sender, e) =>
{
    SomeEvent -= handler;
    Initialize();
};
SomeEvent += handler;

It's quite a lot of boiler-plate, and it also makes Resharper whinge about modified closures. Is there a way of turning this pattern into an extension method or similar? A better way of doing it?

Ideally, I'd like something like:

SomeEvent.OneShot(handler)
spender
  • 117,338
  • 33
  • 229
  • 351
  • I see no reason why you can't put the boilerplate code into an extension method. Or didn't I got the question? – Achim Apr 11 '11 at 15:33
  • How do you pass the event for unsubscription? Does it have a common base type? Isn't a delegate a value type (i.e. if you pass the event somehow, you're dealing with a copy) – spender Apr 11 '11 at 15:34
  • @spender: All delegates are reference types. – Ben Voigt Apr 11 '11 at 15:37
  • Yes, I just sussed that... but to pass an event, you'd essential end up passing a copy. – spender Apr 11 '11 at 15:38
  • You're right. It's not that easy with events. Will think about it. ;-) – Achim Apr 11 '11 at 15:42
  • Are you sure you really want to use events? What about just creating a concurrent queue of subscribers, and when the event occurs pull the "subscribed" delegates out of the queue and fire em off. Then they're removed from the queue and you're clean. – James Michael Hare Apr 11 '11 at 15:50
  • To be honest, I rarely use events in my own code, but in this case (and upon inspection, all of the other times I use this pattern), I'm stuck with a framework event where I only need to catch the first firing. – spender Apr 11 '11 at 15:54

3 Answers3

4

It's not very easy to refactor to an extension method, because the only way you can refer to an event in C# is by subscribing (+=) to or unsubscribing (-=) from it (unless it's declared in the current class).

You could use the same approach as in Reactive Extensions: Observable.FromEvent takes two delegates to subscribe to the event an unsubscribe from it. So you could do something like that:

public static class EventHelper
{
    public static void SubscribeOneShot(
        Action<EventHandler> subscribe,
        Action<EventHandler> unsubscribe,
        EventHandler handler)
    {
        EventHandler actualHandler = null;
        actualHandler = (sender, e) =>
        {
            unsubscribe(actualHandler);
            handler(sender, e);
        };
        subscribe(actualHandler);
    }
}

...

Foo f = new Foo();
EventHelper.SubscribeOneShot(
    handler => f.Bar += handler,
    handler => f.Bar -= handler,
    (sender, e) => { /* whatever */ });
Thomas Levesque
  • 286,951
  • 70
  • 623
  • 758
  • Nice try, but with all the delegating of subscription/unsubscription it's about the same amount of boilerplate, but using an unfamiliar interface that's non-obvious on first read. There's also no enforcement of subscription/unsubscription in the user supplied delegates, which means it would be easy to break and leaves me concerned about the name of the method. Thanks, but I'll stick with mine fttb :) – spender Apr 11 '11 at 16:05
  • I would say that this is a step in the right direction. After all, the intention of the code is clearly stated by the method name and there is slightly less code to type. It's certainly a start. – Jeff Yates Apr 11 '11 at 17:05
  • 1
    +1, you can't abstract `f.Bar += / -= handler` away from the caller anyway, and subscription / removal being performed in the extension method instead of the handler can be considered a feature, not a bug :) – Frédéric Hamidi Apr 11 '11 at 19:21
  • 1
    @spender, yes, it's still quite ugly... but I can't think of a better way. Unfortunately the language lacks some features that would make it easier (specifically, events as first-class citizens)... @Jeff Yates, actually there isn't much less code, but it's simpler code, less error-prone. – Thomas Levesque Apr 11 '11 at 19:22
1

The following code works for me. It's not perfect to have to specify the event via a string, but I have no glue how to solve that. I guess it's not possible in the current C# version.

using System;
using System.Reflection;

namespace TestProject
{
    public delegate void MyEventHandler(object sender, EventArgs e);

    public class MyClass
    {
        public event MyEventHandler MyEvent;

        public void TriggerMyEvent()
        {
            if (MyEvent != null)
            {
                MyEvent(null, null);
            }
            else
            {
                Console.WriteLine("No event handler registered.");
            }
        }
    }

    public static class MyExt
    {
        public static void OneShot<TA>(this TA instance, string eventName, MyEventHandler handler)
        {
            EventInfo i = typeof (TA).GetEvent(eventName);
            MyEventHandler newHandler = null;
            newHandler = (sender, e) =>
                             {
                                 handler(sender, e);
                                 i.RemoveEventHandler(instance, newHandler);
                             };
            i.AddEventHandler(instance, newHandler);
        }
    }

    public class Program
    {
        static void Main(string[] args)
        {
            MyClass c = new MyClass();
            c.OneShot("MyEvent",(sender,e) => Console.WriteLine("Handler executed."));
            c.TriggerMyEvent();
            c.TriggerMyEvent();
        }
    }
}
Achim
  • 15,415
  • 15
  • 80
  • 144
  • Just found out, that a custom event accessor might also be an option. But that depends on your preferences and requirements. – Achim Apr 11 '11 at 21:45
1

I would suggest using a "custom" event so that you have access to the invocation list, and then raise the event by using Interlocked.Exchange to simultaneously read and clear the invocation list. If desired, event subscription/unsubscription/raising could be done in thread-safe manner by using a simple linked-list stack; when the event is raised, the code could, after the Interlocked.Exchange, reverse the order of stack items. For the unsubscribe method, I'd probably suggest simply setting a flag within the invocation-list item. This could in theory cause a memory leak if events were repeatedly subscribed and unsubscribed without the event ever being raised, but it would make for a very easy thread-safe unsubscribe method. If one wanted to avoid a memory leak, one could keep a count of how many unsubscribed events are still in the list; if too many unsubscribed events are in the list when an attempt is made to add a new one, the add method could go through the list and remove them. Still workable in entirely lock-free thread-safe code, but more complicated.

supercat
  • 77,689
  • 9
  • 166
  • 211