The previous post discussed having anonymous methods as event handlers and ended with a question – why doesn’t unsubscription work while subscription works out alright?
Vivek got the answer spot on – the way the C# compiler handles and translates anonymous methods is the reason.
Here’s the code involved:
1: public void Initialize()
2: {
3: control.KeyPressed += IfEnabledThenDo(control_KeyPressed);
4: control.MouseMoved += IfEnabledThenDo(control_MouseMoved);
5: }
6:
7: public void Destroy()
8: {
9: control.KeyPressed -= IfEnabledThenDo(control_KeyPressed);
10: control.MouseMoved -= IfEnabledThenDo(control_MouseMoved);
11: }
12:
13: public EventHandler<Control.ControlEventArgs>
IfEnabledThenDo(EventHandler<Control.ControlEventArgs> actualAction)
14: {
15: return (sender, args) => { if (args.Control.Enabled) actualAction(sender, args); };
16: }
The compiler translates IfEnabledThenDo
into this:
1: public EventHandler<Control.ControlEventArgs>
IfEnabledThenDo(EventHandler<Control.ControlEventArgs> actualAction)
2: {
3: <>c__DisplayClass1 CS$<>8__locals2 = new <>c__DisplayClass1();
4: CS$<>8__locals2.actualAction = actualAction;
5: return new EventHandler<Control.ControlEventArgs>(
CS$<>8__locals2.<IfEnabledThenDo>b__0);
6: }
Now the problem should be fairly obvious – every time the function is called, a new object gets created, and the event handler returned actually refers to a method (<IfEnabledThenDo>b__0
) on the new instance. And that’s what breaks unsubscription. –=
will not remove a delegate of a different instance of the same class from the invocation list – if it did, the consequences would not be pleasant if multiple instances of the same class subscribe to an event.
But why does the compiler translate our lambda expression this way? Raymond Chen has a great blog post explaining why, but the short answer is that it is needed to “hold” the actualAction
(the method parameter to IfEnabledThenDo
) so that it is available when the event handler actually executes.
Now that we know why, the way to get around this issue is to cache the delegate instance returned by IfEnabledThenDo
and use the same instance for subscription and unsubscription.
1: EventHandler<Control.ControlEventArgs> keyPressed;
2: EventHandler<Control.ControlEventArgs> mouseMoved;
3:
4: public void Initialize()
5: {
6: keyPressed = IfEnabledThenDo(control_KeyPressed);
7: mouseMoved = IfEnabledThenDo(control_MouseMoved);
8:
9: control.KeyPressed += keyPressed;
10: control.MouseMoved += mouseMoved;
11: }
12:
13: public void Destroy()
14: {
15: control.KeyPressed -= keyPressed;
16: control.MouseMoved -= mouseMoved;
17: }
Knowing how things work under the hood has its advantages, I guess. :)
P.S.: A very small syntactic change to the original example would have made the code work right away. If you’ve followed along this far, you should be able to figure out why.
1: public void Initialize()
2: {
3: actualAction = control_KeyPressed;
4: control.KeyPressed += IfEnabledThenDo();
5: }
6:
7: public void Destroy()
8: {
9: control.KeyPressed -= IfEnabledThenDo();
10: }
11:
12: EventHandler<Control.ControlEventArgs> actualAction;
13: public EventHandler<Control.ControlEventArgs> IfEnabledThenDo()
14: {
15: return (sender, args) => { if (args.Control.Enabled) actualAction(sender, args); };
16: }