The Thread Proxy Mediator Pattern (TPMP) is a behavioral design pattern that solves many of the problems that plague GUI developers. These problems include:
I’ll explain the pattern with a C# example.
Imagine that you have developed an advanced mathematics library. You license this library to users and they in turn integrate it into their own projects. Now, you want to build a user-interface on top of the library and market the result as a separate product. To maintain product separation, the library must be fully decoupled from the user-interface code.
The following code represents the mathematics library:
1: public delegate void DivideListener(int percent);
2:
3: public class Divider {
4:
5: private volatile bool running;
6:
7: public void CancelDivision() {
8: running = false;
9: }
10:
11: public void Divide(int dividend, int divisor, out int quotient, out bool canceled,
12: DivideListener divideListener) {
13: running = true;
14: for (int i = 0; i <= 100 && running; i++) {
15: Thread.Sleep(100);
16: if (divideListener != null) {
17: divideListener(i);
18: }
19: }
20: if (running) {
21: quotient = dividend / divisor;
22: canceled = false;
23: } else {
24: quotient = 0;
25: canceled = true;
26: }
27: }
28: }
Divider.Divide()
accepts a dividend and a divisor and performs integer division to computer their quotient. The for-loop represents a long computational delay. The progress of the long computation can be monitored via the DivideListener
delegate, passed in as an optional parameter. If the divisor is 0, Divide()
will throw an exception. Finally, while Divide()
is executing, another thread may cancel the operation by calling CancelDivision()
. It sets a boolean flag that causes the for-loop to break out early. When that happens, it sets the canceled
out parameter to true to indicate that quotient
isn’t a valid value.
The Windows Forms user-interface will look like this:
When the user keys in a dividend and a divisor and presses the Divide button, the following progress dialog appears:
The forms above are encapsulated as DivisionForm
and ProgressForm
respectively. To make this example more interesting, I’ll create WPF versions of these windows later on.
The 2 TextBox
es on the DivisionForm
accept the dividend and the divisor as strings. Those strings must be parsed into integers to perform the division. If the user enters invalid values, a MessageBox
will appear that displays an appropriate error.
Where should the parsing logic go? If I put it inside the button click event handler, which Visual Studio automatically injects into DivisionForm
, I won’t be able to reuse it later in the WPF version. Not only that, I can’t unit test event handlers. The only way to debug the logic within event handlers is to run the application.
When the division is performed, it may throw an exception as a result of attempting to divide by zero. Where should I catch that exception and convert it into a error message for the user? Again, if I put it into DivisionForm
, I won’t be able to unit test or reuse it in the WPF version.
Another issue is dealing with threading. Since Divider.Divide()
takes a very long time to return, the click event handler of the Divide button on the DivisionForm
cannot invoke it directly. The application message loop is executed by a single thread. That thread, which I’ll refer to as the “GUI Thread”, services all user-interface events, including repainting controls. If an event handler doesn’t process an event in a timely manner, the user-interface will appear to lockup.
TPMP decouples the user-interface code from the rest of the system as much as possible. Similar to how a traditional web application works, it uses a request-response model. The GUI makes asynchronous requests into rest of the system and responses eventually follow. Though, unlike a web application, the system can push data out to the GUI without a request.
The GUI code is kept very dumb. The parsing and validation logic is moved into separate classes that sit in between the GUI and the rest of the system. This middle-tier known as the “mediator,” acts as a communication bridge and a coordinator between the other tiers. Messages are not allowed to circumvent the mediator.
The first step is to create interfaces that represent the forms:
1: public interface IDividerForm {
2: void DisplayQuotient(string quotient);
3: void ShowError(string errorMessage);
4: }
5:
6: public interface IProgressForm {
7: void DisplayProgress(int percent);
8: void Display();
9: }
DivisionForm
and ProgressForm
implement these interfaces respectively. Later, the WPF windows will use them too. The important point is that the mediator sends information out to the GUI through these interfaces. It is oblivious to the implementation behind them. Also, note that all methods return void and they don’t contain out parameters. The methods of these interfaces represent responses to requests.
The Display()
method on line 8 renders the ProgressForm
visible. If 100 is passed to DisplayProgress()
on line 7, the ProgressForm
vanishes.
Next, the mediator is represented by an interface:
1: public interface IDivisionMediator {
2: void Divide(string dividend, string divisor,
3: IDividerForm dividerFormProxy, IProgressForm progressFormProxy);
4: void CancelDivision();
5: }
DivisionForm
and ProgressForm
hold a reference to the mediator as this interface type. Since they are unaware of its implementation, you can switch the mediator with a class that simply logs the values passed into each method. Or, the test mediator can even simulate the rest of the system by responding to requests with predetermined responses. Test mediators make it possible to develop forms outside of system with confidence that they will integrate later without a problem.
The methods of this interface represent requests; they all return void and they don’t accept out parameters. Also, note that the Divide()
method on line 2 accepts the dividend and divisor as strings since it’s the mediators job to parse them. Divide()
accepts an IDividerForm
to enable the mediator to display the quotient or an error message. It also accepts an IProgressForm
handle. After successfully parsing the inputs, the mediator renders the IProgressForm
visible via its Display()
method. Hence, the mediator also contains behavioral logic. It’s the entity that controls and coordinates the forms in addition to parsing and validating inputs.
Here’s the implementation of the mediator:
1: public class DivisionMediator : IDivisionMediator {
2:
3: private volatile Divider divider = new Divider();
4:
5: public void Divide(string dividend, string divisor,
6: IDividerForm dividerFormProxy, IProgressForm progressFormProxy) {
7:
8: int a = 0;
9: int b = 0;
10: try {
11: a = Int32.Parse(dividend);
12: } catch {
13: dividerFormProxy.ShowError("Dividend is not a valid number.");
14: return;
15: }
16: try {
17: b = Int32.Parse(divisor);
18: } catch {
19: dividerFormProxy.ShowError("Divisor is not a valid number.");
20: return;
21: }
22:
23: progressFormProxy.Display();
24:
25: int quotient;
26: bool canceled;
27: try {
28: divider.Divide(a, b, out quotient, out canceled, percent => progressFormProxy.DisplayProgress(percent));
29: } catch (Exception e) {
30: progressFormProxy.DisplayProgress(100);
31: dividerFormProxy.DisplayQuotient("?");
32: dividerFormProxy.ShowError(e.Message);
33: return;
34: }
35:
36: if (canceled) {
37: progressFormProxy.DisplayProgress(100);
38: dividerFormProxy.DisplayQuotient("?");
39: } else {
40: dividerFormProxy.DisplayQuotient(quotient.ToString());
41: }
42: }
43:
44: public void CancelDivision() {
45: divider.CancelDivision();
46: }
47: }
Lines 8—21 parse the inputs and display error messages if need be. Line 23 renders the ProgressForm
visible if the inputs parsed successfully. Line 28 calls the Divider.Divide()
method. Note the use of a lambda expression for the DivideListener
parameter. Also, lines 30 and 37 pass in 100 percent into the ProgressDialog
to force it to disappear on error or cancellation. If the division was successful, the result is passed back to the DividerForm
on line 40.
With these interfaces in place, the parsing and validation logic can be reused and a set of unit testing classes can be developed to simulate the front-end. And, as mentioned, the forms themselves can be developed and tested outside of the system and integrated later. As it turns out, the interfaces also provide the means of solving the threading problems.
The TPMP introduces a thread barrier between the forms and the mediator. The GUI thread is retrained to the forms. It’s not allowed to cross the barrier into the mediator. In fact, the only thread executing within the forms is the GUI thread. On the other side of the wall are worker threads. Worker threads exist within the mediator and the rest of the system, but they can’t cross the barrier into the forms. It achieves this by introducing proxies. Consider this code:
1: public class HypotheticalDivisionMediatorProxy : IDivisionMediator {
2:
3: private IDivisionMediator target;
4:
5: public HypotheticalDivisionMediatorProxy(IDivisionMediator target) {
6: this.target = target;
7: }
8:
9: public void Divide(string dividend, string divisor,
10: IDividerForm dividerFormProxy, IProgressForm progressFormProxy) {
11: DivideClass divideClass = new DivideClass(target);
12: divideClass.dividend = dividend;
13: divideClass.divisor = divisor;
14: divideClass.dividerFormProxy = dividerFormProxy;
15: divideClass.progressFormProxy = progressFormProxy;
16: ThreadPool.QueueUserWorkItem(new WaitCallback(divideClass.Run));
17: }
18:
19: public void CancelDivision() {
20: CancelDivisionClass cancelDivisionClass = new CancelDivisionClass(target);
21: ThreadPool.QueueUserWorkItem(new WaitCallback(cancelDivisionClass.Run));
22: }
23: }
24:
25: public class DivideClass {
26:
27: private IDivisionMediator target;
28: public string dividend;
29: public string divisor;
30: public IDividerForm dividerFormProxy;
31: public IProgressForm progressFormProxy;
32:
33: public DivideClass(IDivisionMediator target) {
34: this.target = target;
35: }
36:
37: public void Run(object stateInfo) {
38: target.Divide(dividend, divisor, dividerFormProxy, progressFormProxy);
39: }
40: }
41:
42: public class CancelDivisionClass {
43:
44: private IDivisionMediator target;
45:
46: public CancelDivisionClass(IDivisionMediator target) {
47: this.target = target;
48: }
49:
50: public void Run(object stateInfo) {
51: target.CancelDivision();
52: }
53: }
Above are 3 classes. HypotheticalDivisionMediatorProxy
implements IDivisionMediator
. It acts as a middleman between a caller and the target IDivisionMediator
passed into the constructor. However, each call is delegated on a pooled worker thread. The DivideClass
and the CancelDivisionClass
are introduced to encapsulate the arguments passed into the call.
The forms possess an IDivisionMediator
handle to this hypothetical proxy that transfers the call from the GUI thread to a worker thread. All the methods of the proxy are asynchronous; they return immediately. The code within the forms is kept very clean. From their point of view, there is no barrier between the forms and the mediator. They make calls as if it they were talking directly to the mediator.
However, coding such proxies by hand is tedious, repetitive and error prone. The trick is to generate them dynamically. The .NET framework, at the time of this writing, does not contain a dynamic proxy. But, it’s possible to generate dynamic classes with the System.Reflection.Emit
namespace. In the source code link below, you’ll find a class called ThreadProxyFactory
that provides methods for dynamically generating proxies. The method for generating the GUI-thread-to-worker-thread proxy, has this signature:
public static T createProxy<T>(T target) { // ...
It’s used like this:
1: DivisionMediator divisionMediator = new DivisionMediator();
2: IDivisionMediator divisionMediatorProxy = ThreadProxyFactory.createProxy<IDivisionMediator>(divisionMediator);
For the reverse direction, Control
—the base class of Form
and all the other Windows Forms controls—provides a method called BeginInvoke()
that adds a request to the GUI event queue for servicing a brief time later by the GUI thread. BeginInvoke()
returns immediately. To dynamically generate a proxy for the forms interfaces, use this method:
public static T createProxy<T>(T target, Control control) { // ...
The DividerForm
makes use of the following code (i.e. this
is DividerForm
):
1: ProgressForm progressForm = new ProgressForm();
2: progressForm.Owner = this;
3: IProgressForm progressFormProxy = ThreadProxyFactory.createProxy<IProgressForm>(progressForm, this);
Here’s the complete definition of DividerForm
:
1: public partial class DividerForm : Form, IDividerForm {
2: public DividerForm() {
3: InitializeComponent();
4: }
5:
6: private void divideButton_Click(object sender, EventArgs e) {
7:
8: divideButton.Enabled = false;
9:
10: ProgressForm progressForm = new ProgressForm();
11: progressForm.Owner = this;
12:
13: DivisionMediator divisionMediator = new DivisionMediator();
14:
15: IDividerForm dividerFormProxy = ThreadProxyFactory.createProxy<IDividerForm>(this, this);
16: IProgressForm progressFormProxy = ThreadProxyFactory.createProxy<IProgressForm>(progressForm, this);
17: IDivisionMediator divisionMediatorProxy = ThreadProxyFactory.createProxy<IDivisionMediator>(divisionMediator);
18: progressForm.DivisionMediatorProxy = divisionMediatorProxy;
19:
20: divisionMediatorProxy.Divide(dividendTextBox.Text, divisorTextBox.Text, dividerFormProxy, progressFormProxy);
21: }
22:
23: public void DisplayQuotient(string quotient) {
24: quotientLabel.Text = quotient;
25: divideButton.Enabled = true;
26: }
27:
28: public void ShowError(string errorMessage) {
29: MessageBox.Show(this, errorMessage, "Division Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
30: divideButton.Enabled = true;
31: }
32: }
For simplicity, the button click handler creates the mediator and dynamically generates all the proxies before calling Divide()
. In a real application, the mediator and many of the proxies would be created on startup. Everything would be wired together, ready to be invoked. Also, note that when the button is clicked, it is disabled to prevent successive clicks making parallel calls into the mediator. It is re-enabled only after a result comes back from the mediator.
Here’s the ProgressForm
definition:
1: public partial class ProgressForm : Form, IProgressForm {
2:
3: private IDivisionMediator divisionMediatorProxy;
4:
5: public ProgressForm() {
6: InitializeComponent();
7: }
8:
9: public IDivisionMediator DivisionMediatorProxy {
10: set {
11: divisionMediatorProxy = value;
12: }
13: }
14:
15: private void cancelButton_Click(object sender, EventArgs e) {
16: cancelButton.Enabled = false;
17: divisionMediatorProxy.CancelDivision();
18: }
19:
20: public void Display() {
21: ShowDialog(Owner);
22: }
23:
24: public void DisplayProgress(int percent) {
25: progressBar.Value = percent;
26: if (percent == 100) {
27: Dispose();
28: }
29: }
30: }
The source code link below contains a Visual Studio 2008 solution with 2 projects: a Windows Forms version of the example and a WPF version. The WPF version looks like this:
I renamed the interfaces in the WPF version to make the code easier to understand (i.e. I changed “Form” to “Window”), but all the method signatures are exactly the same. Aside from that, the mediator along with its parsing and validation logic is completely reused.
The threading model in WPF is very similar to Windows Forms. WPF controls, including Window
s, derive from the abstract class DispatcherObject
. DispatcherObject
provides a Dispatcher
property that returns type Dispatcher
. Dispatcher.BeginInvoke()
works analogously to Control.BeginInvoke()
.
Here’s the method of ThreadProxyFactory
that you’ll need to create a dynamic proxy for transferring calls from worker threads to the WPF GUI thread:
public static T createProxy<T>(T target, Dispatcher dispatcher) { // ...
Below are the definitions of DividerWindow
and ProgressWindow
. They are virtually identical to the Windows Forms verions.
1: public partial class DividerWindow : Window, IDividerWindow {
2: public DividerWindow() {
3: InitializeComponent();
4: }
5:
6: private void DivideButton_Click(object sender, RoutedEventArgs e) {
7: divideButton.IsEnabled = false;
8:
9: ProgressWindow progressWindow = new ProgressWindow();
10: progressWindow.Owner = this;
11:
12: DivisionMediator divisionMediator = new DivisionMediator();
13:
14: IDividerWindow dividerWindowProxy = ThreadProxyFactory.createProxy<IDividerWindow>(this, Dispatcher);
15: IProgressWindow progressWindowProxy
16: = ThreadProxyFactory.createProxy<IProgressWindow>(progressWindow, Dispatcher);
17: IDivisionMediator divisionMediatorProxy = ThreadProxyFactory.createProxy<IDivisionMediator>(divisionMediator);
18: progressWindow.DivisionMediatorProxy = divisionMediatorProxy;
19:
20: divisionMediatorProxy.Divide(
21: dividendTextBox.Text, divisorTextBox.Text, dividerWindowProxy, progressWindowProxy);
22: }
23:
24: #region IDividerWindow Members
25:
26: public void DisplayQuotient(string quotient) {
27: quotientLabel.Content = quotient;
28: divideButton.IsEnabled = true;
29: }
30:
31: public void ShowError(string errorMessage) {
32: MessageBox.Show(this, errorMessage, "Division Error");
33: divideButton.IsEnabled = true;
34: }
35:
36: #endregion
37: }
38:
39: public partial class ProgressWindow : Window, IProgressWindow {
40:
41: private IDivisionMediator divisionMediatorProxy;
42:
43: public ProgressWindow() {
44: InitializeComponent();
45: }
46:
47: public IDivisionMediator DivisionMediatorProxy {
48: set {
49: divisionMediatorProxy = value;
50: }
51: }
52:
53: private void cancelButton_Click(object sender, RoutedEventArgs e) {
54: cancelButton.IsEnabled = false;
55: divisionMediatorProxy.CancelDivision();
56: }
57:
58: private void Window_Closed(object sender, EventArgs e) {
59: cancelButton.IsEnabled = false;
60: divisionMediatorProxy.CancelDivision();
61: }
62:
63: #region IProgressWindow Members
64:
65: public void DisplayProgress(int percent) {
66: progressBar.Value = percent;
67: if (percent == 100) {
68: Close();
69: }
70: }
71:
72: public void Display() {
73: ShowDialog();
74: }
75:
76: #endregion
77: }
Download the source here.
Dispatcher.BeginInvoke()
: http://msdn.microsoft.com/en-us/library/system.windows.threading.dispatcher.begininvoke.aspxCopyright © 2008 Michael Birken
Updated: 2008.07.21