Earlier today, a friend described a scenario at his work where he needed to hook up an EventHandler
that was only raised once.
A naïve solution could be to merely check whether the critical code had been executed within the handler; then set the state appropriately upon the first execution. Something like:
public class TestClass { public event EventHandler SimpleEvent; public void RaiseAll() { if (SimpleEvent != null) SimpleEvent(null, EventArgs.Empty); } } // elsewhere: TestClass test = new TestClass(); bool raised = false; test.SimpleEvent += delegate { if (!raised) { // critical code: raised = true; Console.WriteLine("Hello world!"); } }; test.RaiseAll(); test.RaiseAll(); test.RaiseAll(); // output: // Hello world!
Okay, that's fine for an event on a simple, transient instance of an object. But what if, in the lifecycle of our application, we could potentially throw away hundered of EventHandlers
? And what if, further complicating the problem, the event
is static
? It sure would be nice if we could actually remove the EventHandler
, once it has been raised.
Well, we can. There are two ways to achieve this: one is simple but requires duplication for reuse; the other is complicated but easily reusable.
Let's look at the simple one first:
TestClass test = new TestClass(); EventHandler handler = null; // avoid: error CS0165: Use of unassigned local variable 'handler' test.SimpleEvent += handler = delegate { test.SimpleEvent -= handler; // remove self before executing the critical code Console.WriteLine("Hello world!"); }; test.RaiseAll(); test.RaiseAll(); test.RaiseAll(); // output: // Hello world!
There are a couple of things to note here:
- Since we know the type of
TestClass
'sSimpleEvent
, we can declare a strongly typedEventHandler
and assign an anonymous method to it. Not knowing this type at compile-time is the source of much of the complication of the reusable solution below. - Also, we temporarily assign
null
tohandler
before referring tohandler
within the body of the anonymous method to avoid the noted compiler error. Then, we assign the anonymous method tohandler
before attachinghandler
to theevent
.
But, Jacob, this works fine. Why would we care about "improving" it?
Well, for starters, it's not very extensible. The critical code is embedded into the anonymous method. So anytime we want to bring different functionality to this event, we'll need to repeat this pattern. Also, not only is the critical code not pluggable, but we've constrained ourselves to only EventHandler
events. There are other types of strongly typed event handling delegates with far more interesting EventArgs
(and how does the name "EventArgs" not violate the Framework Design Guidelines, anyway?). And lastly, … well… because we can:
[At this point in the post, the author suddenly switches voices: the hand-holdy, instructive teacher is replaced with the programmer who has spent too much time with the material at hand and pastes in swaths of code assuming his audience will understand. Apologies for the lack of exposition to follow.]
public static class EventUtility { public static void AttachRaisedOnce<TTarget>(TTarget target, string eventName, EventHandler raisedOnce) where TTarget : class { AttachRaisedOnce<TTarget, EventArgs>(target, eventName, CastDelegate<EventHandler<EventArgs>>(raisedOnce)); } public static void AttachRaisedOnce<TTarget, TEventArgs>(TTarget target, string eventName, EventHandler<TEventArgs> raisedOnce) where TTarget : class where TEventArgs : EventArgs { EventInfo eventTarget = typeof(TTarget).GetEvent(eventName); if (eventTarget == null) throw new ArgumentException(String.Format("Couldn't find event with name '{0}'", eventName), "eventName"); Delegate self = null; // avoid unassigned local EventHandler<TEventArgs> localMethod = delegate(object sender, TEventArgs e) { eventTarget.RemoveEventHandler(target, self); raisedOnce(sender, e); }; self = Delegate.CreateDelegate(eventTarget.EventHandlerType, localMethod.Target, localMethod.Method); eventTarget.AddEventHandler(target, self); } // see earlier post // also: belongs elsewhere; maybe a static DelegateUtility class private static T CastDelegate<T>(Delegate source) where T : class // CS0702: Constraint cannot be special class 'System.Delegate' { if (source == null) return null; Delegate[] delegates = source.GetInvocationList(); if (delegates.Length == 1) return Delegate.CreateDelegate(typeof(T), delegates[0].Target, delegates[0].Method) as T; for (int i = 0; i < delegates.Length; i++) delegates[i] = Delegate.CreateDelegate(typeof(T), delegates[i].Target, delegates[i].Method); return Delegate.Combine(delegates) as T; } }
Calling code looks like:
TestClass test = new TestClass(); EventUtility.AttachRaisedOnce(test, "SimpleEvent", delegate { Console.WriteLine("Hello world!"); }); test.RaiseAll(); test.RaiseAll(); test.RaiseAll(); // output: // Hello world!
For strongly typed EventHandlers, like the System.Web.UI.ImageClickEventHandler
delegate, the calling code looks a little weird:
EventUtility.AttachRaisedOnce<ImageButton, ImageClickEventArgs>(button, "Click", delegate { Response.Write("Hello web!"); });
Note that the second type argument is the type of the EventArgs
, not the type of the EventHandler
delegate itself. This is due to the inablity to apply a Delegate
constraint on a type argument, coupled with the need to assign an anonymous method to a local variable with the right type. We rely on the fact that we can convert from an EventHandler<T>
to the strongly typed EventHandler
delegate. This means that it is also possible to compile code with the wrong event/delegate combinations; but don't worry: the runtime will "inform you" of any conversion failures.
Enjoy.