Event bubbling in C#
Published by Carlos Blé on 08/04/2016
How to propagate an event from a low level class to a top level one:
public class TopLevel{ | |
public bool Bubbled { get; private set; } | |
private MiddleLevel observable; | |
public TopLevel(MiddleLevel observable){ | |
this.observable = observable; | |
observable.Triggered += (s, e) => { | |
Bubbled = true; | |
}; | |
} | |
} | |
public class MiddleLevel{ | |
public event EventHandler Triggered; | |
private BottomLevel observable; | |
public MiddleLevel(BottomLevel observable){ | |
this.observable = observable; | |
//One may be tempted to bubble like this: | |
//observable.Triggered += Triggered; | |
//However, Triggered is null unless there is already | |
//a subscriber. This is a better approach: | |
observable.Triggered += (s, e) => { | |
Triggered(s, e); | |
}; | |
} | |
} | |
public class BottomLevel{ | |
public event EventHandler Triggered; | |
public void DoSomething(){ | |
Triggered(this, EventArgs.Empty); | |
} | |
} | |
[TestFixture] | |
public class TestingEventBubbling { | |
[Test] | |
public void Bubbling(){ | |
var bottom = new BottomLevel(); | |
var middle = new MiddleLevel(bottom); | |
var top = new TopLevel(middle); | |
bottom.DoSomething(); | |
top.Bubbled.Should().BeTrue(); | |
} | |
} |
Events can only be raised from within the declaring type. Unfortunately they can’t be be passed in as arguments to methods. Only += and -= operators are allowed out of the declaring type. One way to stub out the event could be through inheritance:
public class BottomLevel{ | |
public virtual event EventHandler Triggered; | |
public void DoSomething(){ | |
Triggered(this, EventArgs.Empty); | |
} | |
} | |
public class StubbedBottomLevel : BottomLevel { | |
public override event EventHandler Triggered; | |
public void RaiseEvent(){ | |
Triggered(this, EventArgs.Empty); | |
} | |
} | |
[TestFixture] | |
public class TestingEventBubbling { | |
[Test] | |
public void BubblingWithStub(){ | |
var bottom = new StubbedBottomLevel(); | |
var middle = new MiddleLevel(bottom); | |
var top = new TopLevel(middle); | |
bottom.RaiseEvent(); | |
//bottom.DoSomething(); will not throw the event! | |
top.Bubbled.Should().BeTrue(); | |
} | |
} |
But declaring the event as virtual and then overriding it, is very tricky: replacing the call to RaiseEvent to DoSomething, makes the test fail! Looks like events where not designed to be overridden.
A better approach would be:
public class BottomLevel{ | |
public event EventHandler Triggered; | |
public virtual void DoSomething(){ | |
//SomeLogic would go here... | |
Raise(EventArgs.Empty); | |
} | |
protected virtual void Raise(EventArgs args){ | |
Triggered(this, args); | |
} | |
} | |
public class StubbedBottomLevel : BottomLevel { | |
public override void DoSomething(){ | |
Raise(EventArgs.Empty); | |
} | |
} |
Originally published in Carlos Blé's blog.