3/29/2005

Decoupled UI

My original question:

I want to be able to do this but it will not compile because the EventHandler does not have a no arg constructor.


IDictionary collection = new Hashtable();
collection.Add("myevent", new EventHandler());

A single event has the syntactic oddities

event EventHandler fred;
fred += new EventHandler(this.SomeMethod);
fred("x", eventArgs);

So how do I add fred to a collection so that I can do this
collection["myevent"] += new EventHandler(this.SomeMethod);


The answer:

I found the answer so here it is. Thanks to Mike Wood for his expertise.

The first and simple answer is that you can't do what I was asking. Events can't be constructed in the way that we would expect so can not be stored in a hashtable.

Background

The new information that I learned was about multicast delegates. These are the normal delegates that are created for us in our WinForms apps but they actually have the ability to store more than one callback.

It turns out that an event is completely unnecessary when declaring a callback. MSDN says that its just a safer syntactic representation of a multicast delegate (An Event to Remember).
delegate void MyCallback();
MyCallback fred;
fred += new MyCallback(this.SomeMethod);
This will fail to compile because fred is not assigned and you should not += to it.
delegate void MyCallback();
MyCallback fred;

fred = new MyCallback(this.SomeMethod);
fred += new MyCallback(this.SomeMethod);
This will work because fred is first assigned with a delegate and then one more is added. Note that we can not re-assign a callback to the delegate to clear all existing delegates. This is the danger of the delegate syntax. This is now a multi-cast delegate since fred contains multiple callbacks that can all be invoked in turn by:
fred();
So the alternate syntax allows for the += in any situation. By placing the keyword "event" in front of your delegate declaration you allow the += to always work. This doesn't prevent you from doing an assignment so its not much protection but it does mean that whenever you see an assignment to an event you should be suspicious.
delegate void MyCallback();
event MyCallback fred;
fred += new MyCallback(this.SomeMethod);

Problem

What I wanted was a single class that could manage a set of delegates, categorized by data type, that could be subscribed to and published from different areas of your code to ensure that there was no compile time coupling between them.

Example of the problem:

class A
{
private B b;

public A()
{
b = new B();
}

public OnButtonPressed()
{
b.MakeBread();
}
}

class B
{
public MakeBread()
{
Console.Write("rise rise rise");
}
}

Notice how A has a reference to B. This means that when I want C to do something when the ButtonPressed event occurs I have to edit A to add a new reference. Also, If I want to add another layer between A and B then I have to change A to make the new reference and move the B reference into the new location. Lots of code changing is bad from a maintenance and testing perspective.

Design principle. Open Close Principle

"Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification."

C2 Wiki - OpenClosedPrinciple

In this example I had to change lots of existing code to rearrange my structure. This is very common in UI applications as you move the UI around the elements of the UI have to be moved between source files and the call backs have to be moved as well.

Solution

The solution to the original problem is to put a delegate in the hashtable and then add additional delegates to the same element. No events were used during the making of this code.

I wrapped this in a class called PubSub with two static methods Subscribe and Publish (Download Solution)

Example:

class A
{
public A()
{
}

public OnButtonPressed()
{
PubSub.Publish(new UiEventButtonPressed());
}
}

class B
{
public B()
{
PubSub.Subscribe(typeof(UiEventButtonPressed), new EventHandler(MakeBread));
}

public MakeBread(object sender, EventArgs args)
{
Console.Write("rise rise rise");
}
}

Notice now that class A doesn't know who is reacting to the ButtonPressed event, all its doing it notifying the world that something happened. Class B on the other hand is a simple standalone class that knows it has to do something when the button is pressed so subscribes for that event.

I could now write a class C that subscribed for the same event without changing any existing code at all. This is a massive maintenance and test advantage. Imagine being able to extend an existing system by dropping a new dll into the deployment. Sweetness.

This type of abstraction also offers extension advantages. Imagine the day when your UI gets so slow that you have to start backgrounding some of the actions. You can add extend your PubSub class to submit all the callbacks on background threads.

What about the idea of distributing your application. Say you want a background service to manage the synchronization of data from your UI to some external storage. You could extend PubSub to post events across process or machine boundaries and plug into your existing application without changing it.

Obviously, I am simplifying this a little but the abstractions are the key to making your system flexible enough to be tolerant of these kinds of changes.

1 comment:

Anonymous said...
This comment has been removed by a blog administrator.