|
||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
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. Example User-InterfaceImagine 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: }
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 ProblemsThe two Where should the parsing logic go? If I put it inside the button click event handler, which Visual Studio automatically injects into 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 Another issue is dealing with threading. Since Applying the PatternTPMP 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: }
The 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: }
The methods of this interface represent requests; they all return void and they don’t accept out parameters. Also, note that the 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 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. The forms possess an 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 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, public static T createProxy<T>(T target, Control control) { // ...
The 1: ProgressForm progressForm = new ProgressForm();
2: progressForm.Owner = this;
3: IProgressForm progressFormProxy = ThreadProxyFactory.createProxy<IProgressForm>(
progressForm, this);
Here’s the complete definition of 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 Here’s the 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: }
WPF VersionThe source code link below contains a Visual Studio 2008 solution with two 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 Here’s the method of public static T createProxy<T>(T target, Dispatcher dispatcher) { // ...
Below are the definitions of 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: } References
|
|||||||||||||||||||||||||||||||||||||||||||||||