This week I was called on to do a very strange on-site custom course. They basically couldn’t decide on any one topic so they wanted me to talk a little bit about everything, mainly concentrating on:
- Advanced Windows Forms
- Windows Communication Foundation
- Unit Testing
As I have covered Windows Communication Foundation many times, and I didn’t have quite enough time to do unit testing any justice, the most interesting of the talks (I thought) was Windows Forms. We covered MDI, Notify Icons, and to my surprise had an interesting eventing discussion. Someone had asked about the difference between WPF and Windows Forms. I was trying to describe how they can be very similar but if you program WPF the right way how very different they really were and how complicated WPF was compared to Windows Forms. As part of this description I mentioned that Windows Forms has only the direct event model, but WPF has both bubbling and tunneling in addition to the direct model. Again someone raised their hand and asked if there was no way to simulate the bubbling model that they had in MFC (and WPF) using Windows Forms. He mentioned that they were having problems with coupling between components. I told them that the Client Application Block had something like this built in. He said that they found the CAB a little too heavy, and I agreed. I told him I would think more about it and get back to him tomorrow.
When I got back to the hotel that evening I started thinking about it and realized that there was a way to simulate it using a publish and subscribe type mechanism. This is basically a lightweight version of what the CAB clock does. I named my class the same as theirs as a form of flattery.
When you specify that you want to subscribe to a button click using the following syntax.
EventBroker.Instance.Subscribe(this, typeof(Button), "Click", OnButtonClick);
The EventBroker basically just walks the tree of controls recursively searching for controls of the type button, and then adds itself as a subscriber to that button. If someone else in the hierarchy also subscribes for button clicks the EventBroker knows which subscriber to call first. If the inner subscriber handles the event then the outer subscriber never sees the event, just like bubbling.
Best of all, you only need to drop in this one file, and away you go.
Here is the file in its entirety:
using System; using System.Windows.Forms; using System.Collections.Generic; using System.Diagnostics; using System.Reflection; namespace WindowsFormsApp { // helper method to make the code below more readable public static class ControlExtensions { public static bool IsParentOf(this Control c, Control other) { Control parent = other; while (parent != null) { if (c == parent) return true; parent = parent.Parent; } return false; } } public class EventBroker { class EventHolder { public Control Control { get; set; } public EventHandler<BubblingEventArgs> EventHandler { get; set; } } // singleton public static readonly EventBroker Instance = new EventBroker(); readonly Dictionary<Control, List<EventHolder>> subscribers = new Dictionary<Control, List<EventHolder>>(); public void Subscribe(Control c, Type type, string eventName, EventHandler<BubblingEventArgs> callback) { SubscribeRecursive(c, c, type, eventName, callback); } private void SubscribeRecursive(Control subscriber, Control c, Type type, string eventName, EventHandler<BubblingEventArgs> callback) { if (type.IsInstanceOfType(c)) { // check to see if this eventName exists and subscribe Debug.WriteLine(string.Format("{0} is of type {1}", c.Name, type.Name)); List<EventHolder> list; var newHolder = new EventHolder { Control = subscriber, EventHandler = callback, }; // do we already have a subscriber for this object? if (subscribers.TryGetValue(c, out list)) { // walk through the list trying to find where to insert this subscriber bool inserted = false; for (int cnt = 0; cnt < list.Count; cnt++) { EventHolder holder = list[cnt]; // uses the extension method above if (holder.Control.IsParentOf(subscriber)) { list.Insert(cnt, newHolder); inserted = true; break; } } // if they weren't inserted add them at the end if (!inserted) { list.Add(newHolder); } } else { // this is a new object, subscribe to its event EventInfo eventInfo = c.GetType().GetEvent(eventName); eventInfo.AddEventHandler(c, new EventHandler(OnEvent)); // then create a new list of subscribers keyed to this object list = new List<EventHolder> {newHolder}; subscribers.Add(c, list); } } // now recursively subscribe foreach (Control child in c.Controls) { SubscribeRecursive(subscriber, child, type, eventName, callback); } } public void OnEvent(object sender, EventArgs e) { // grab the list of subscribers for this control List<EventHolder> list = subscribers[(Control)sender]; foreach (EventHolder holder in list) { Debug.WriteLine(string.Format("About to call {0}", holder.Control.Name)); // call this subscriber var handledArgs = new BubblingEventArgs {InnerArgs = e}; holder.EventHandler(sender, handledArgs); // if they handled the event don't propogate further if (handledArgs.Handled) break; } } } }