CodeBetter.Com
CodeBetter.Com
RSS 2.0 via Feedburner
           Do you Twitter? Follow us @CodeBetter

Jeremy D. Miller -- The Shade Tree Developer

Under the hood and working with .Net, TDD, Software Design, and Agile Stuff

A Simple Example of the "Humble Dialog Box"

At the Agile Austin lunch today, we talked a bit about different ways to apply the Model View Presenter pattern with WinForms clients. I promised to put up an example of using Michael Feather's "Humble Dialog Box" to create more testable user interface code.

The current thinking for writing automated unit tests against rich clients is a modification or variation of the classic Model View Controller (MVC) architecture. Specifically, take the view part of MVC and slice it as thin as possible so that it is only a skin around the actual UI components and make it completely passive. The controller, now called the "presenter," is responsible for all interaction with the rest of the system. There is a pattern of symbiosis between the view and the presenter. The presenter directs the view what and when to display and the view captures and relays user events to the presenter. Check the links above for a more comprehensive explanation from the professionals.

Here's a common scenario. You have some kind of form in your application for editing a piece of data. If the user trys to close the form and there are pending changes, put up a dialog box giving the user a chance to cancel the close operation. In this case the dialog box is the major impediment to automated testing, so we're going to hide the message box creation behind an interface that can be mocked (or stubbed if that's your predilection). Do the same thing for the view/presenter separation. Use the Dependency Inversion Principle to abstract the view away from the presenter and mock the view in the unit tests.




using System;
using System.Windows.Forms;
using NMock;
using NUnit.Framework;

namespace SampleCode.HumbleDialogBox
{
public interface IMessageBoxCreator
{
bool AskYesNoQuestion(string title, string message);
}

public class MessageBoxCreator : IMessageBoxCreator
{
public bool AskYesNoQuestion(string title, string message)
{
DialogResult result = MessageBox.Show(message, title, MessageBoxButtons.OKCancel);
return result == DialogResult.OK;
}
}

public interface IView
{
void Close();
bool IsDirty();
}

public class Presenter
{
public const string DIRTY_CLOSE_WARNING = "Changes are pending. "
+ "Ok to continue, cancel to return to the edit screen.";
public const string CLOSE_WARNING_TITLE = "Changes Pending";

private readonly IView _view;
private readonly IMessageBoxCreator _msgBox;

//
public Presenter(IView view, IMessageBoxCreator msgBox)
{
_view = view;
_msgBox = msgBox;
}

public void Close()
{
bool canClose = true;

if (_view.IsDirty())
{
canClose = _msgBox.AskYesNoQuestion(CLOSE_WARNING_TITLE, DIRTY_CLOSE_WARNING);
}

if (canClose)
{
_view.Close();
}
}
}

[TestFixture]
public class PresenterTester
{
private DynamicMock _viewMock;
private DynamicMock _msgBoxMock;
private Presenter _presenter;

[SetUp]
public void SetUp()
{
_msgBoxMock = new DynamicMock(typeof(IMessageBoxCreator));
_viewMock = new DynamicMock(typeof(IView));
_presenter = new Presenter((IView) _viewMock.MockInstance, (IMessageBoxCreator) _msgBoxMock.MockInstance);
}


[Test]
public void CloseViewWhenViewIsNotDirty()
{
// Define the expected interaction
_msgBoxMock.ExpectNoCall("AskYesNoQuestion", typeof(string), typeof(string));

_viewMock.ExpectAndReturn("IsDirty", false);
_viewMock.Expect("Close");

// Perform the unit of work
_presenter.Close();

// Verify the interaction
_msgBoxMock.Verify();
_viewMock.Verify();
}


[Test]
public void CloseViewWhenViewIsDirtyAndUserRespondsOk()
{
// Define the expected interaction
_msgBoxMock.ExpectAndReturn(
"AskYesNoQuestion",
true,
Presenter.CLOSE_WARNING_TITLE,
Presenter.DIRTY_CLOSE_WARNING);

_viewMock.ExpectAndReturn("IsDirty", true);
_viewMock.Expect("Close");

// Perform the unit of work
_presenter.Close();

// Verify the interaction
_msgBoxMock.Verify();
_viewMock.Verify();
}


[Test]
public void DoNotCloseViewWhenViewIsDirtyAndUserRespondsCancel()
{
// Define the expected interaction
_msgBoxMock.ExpectAndReturn(
"AskYesNoQuestion",
false,
Presenter.CLOSE_WARNING_TITLE,
Presenter.DIRTY_CLOSE_WARNING);

_viewMock.ExpectAndReturn("IsDirty", true);
_viewMock.ExpectNoCall("Close");

// Perform the unit of work
_presenter.Close();

// Verify the interaction
_msgBoxMock.Verify();
_viewMock.Verify();
}
}
}




Here's a rundown of the pieces from the example code.

  1. IMessageBoxCreator/MessageBoxCreator - An interface and wrapper class around the WinForms MessageBox class. The methods in the .NET framework for dialogs are all static, and static methods cannot be mocked.
  2. IView interface - An interface that establishes the public contract between the actual form and the presenter. I didn't show it, but assume the actual View has a reference to the Presenter.
  3. Presenter - the Presenter class drives the IView and IMessageBoxCreator interfaces. The Presenter class is completely unaware of any of the actual user interface plumbing, i.e. not one single reference to the System.Windows.Forms namespace.

In this example I used constructor injection to attach the IMessageBoxCreator. The next example is mostly the same, but I use StructureMap instead to locate the IMessageBoxCreator and take advantage of StructureMap's built in support for NMock.




using System;
using System.Windows.Forms;
using NMock;
using NUnit.Framework;
using StructureMap;

namespace SampleCode.HumbleDialogBox2
{
[PluginFamily("Default")]
public interface IMessageBoxCreator
{
bool AskYesNoQuestion(string title, string message);
}

[Pluggable("Default")]
public class MessageBoxCreator : IMessageBoxCreator
{
public bool AskYesNoQuestion(string title, string message)
{
DialogResult result = MessageBox.Show(message, title, MessageBoxButtons.OKCancel);
return result == DialogResult.OK;
}
}

public interface IView
{
void Close();
bool IsDirty();
}

public class Presenter
{
public const string DIRTY_CLOSE_WARNING = "Changes are pending. "
+ "Ok to continue, cancel to return to the edit screen.";
public const string CLOSE_WARNING_TITLE = "Changes Pending";


private readonly IView _view;

public Presenter(IView view)
{
_view = view;
}

public void Close()
{
bool canClose = true;

if (_view.IsDirty())
{
// Get the IMessageBoxCreator out of StructureMap
IMessageBoxCreator msgBox =
(IMessageBoxCreator)
ObjectFactory.GetInstance(typeof(IMessageBoxCreator));
canClose = msgBox.AskYesNoQuestion
(CLOSE_WARNING_TITLE, DIRTY_CLOSE_WARNING);
}

if (canClose)
{
_view.Close();
}
}
}

[TestFixture]
public class PresenterTester
{
private DynamicMock _viewMock;
private IMock _msgBoxMock;
private Presenter _presenter;

[SetUp]
public void SetUp()
{
_msgBoxMock = ObjectFactory.Mock(typeof(IMessageBoxCreator));
_viewMock = new DynamicMock(typeof(IView));
_presenter = new Presenter((IView) _viewMock.MockInstance);
}

[TearDown]
public void TearDown()
{
ObjectFactory.ResetDefaults();
}

[Test]
public void CloseViewWhenViewIsNotDirty()
{
// Define the expected interaction
_msgBoxMock.ExpectNoCall("AskYesNoQuestion", typeof(string), typeof(string));

_viewMock.ExpectAndReturn("IsDirty", false);
_viewMock.Expect("Close");

// Perform the unit of work
_presenter.Close();

// Verify the interaction
_msgBoxMock.Verify();
_viewMock.Verify();
}


[Test]
public void CloseViewWhenViewIsDirtyAndUserRespondsOk()
{
// Define the expected interaction
_msgBoxMock.ExpectAndReturn(
"AskYesNoQuestion",
true,
Presenter.CLOSE_WARNING_TITLE,
Presenter.DIRTY_CLOSE_WARNING);

_viewMock.ExpectAndReturn("IsDirty", true);
_viewMock.Expect("Close");

// Perform the unit of work
_presenter.Close();

// Verify the interaction
_msgBoxMock.Verify();
_viewMock.Verify();
}


[Test]
public void DoNotCloseViewWhenViewIsDirtyAndUserRespondsCancel()
{
// Define the expected interaction
_msgBoxMock.ExpectAndReturn(
"AskYesNoQuestion",
false,
Presenter.CLOSE_WARNING_TITLE,
Presenter.DIRTY_CLOSE_WARNING);

_viewMock.ExpectAndReturn("IsDirty", true);
_viewMock.ExpectNoCall("Close");

// Perform the unit of work
_presenter.Close();

// Verify the interaction
_msgBoxMock.Verify();
_viewMock.Verify();
}
}
}



Final Thoughts



It is not impossible to write automated unit tests for rich clients, but it's definitely difficult and time consuming. So what can you do? You can take a calculated risk and forgo writing the automated tests for the user interface. The biggest problem with that approach is that a complicated rich user interface can generate a large number of bugs and requires a lot of energy towards manual regression testing (duh). You can test a WinForms application with Luke Maxon's most excellent NUnitForms toolkit, but user interface tests are still more work to setup and execute. A better approach is to simply make as much code as possible independent of the WinForms (or Swing, etc.) engine. I say you still have to test the actual UI forms and controls. However, if they are passive and loosely coupled from the rest of the application your NUnitForms tests can be much simpler.



I left some implementation details out of the example. I've used the MVP pattern pretty extensively on a couple of projects now with mostly good results. Since it's such a hot topic and the book on best practices is literally being written as I type this, I'll try to blog soon on some MVP suggestions and pitfalls.



Comments

Jeremy D. Miller -- The Shade Tree Developer said:

Here's a handful of articles on designing with or for TDD I had originally posted on my...
# July 21, 2005 1:05 PM

idior said:

the view how to tiger the _presenter.Close();
# July 27, 2005 6:12 AM

Matthias Cavigelli said:

> The methods in the .NET framework for
> dialogs are all static, and static
> methods cannot be mocked.

A solution I was thinking about for this, never used though, is to pass in the delegate of MessageBox.Show. The form would invoke the delegate. This delegate could be mocked out for tests. Not a nice solution, but I guess if you just have one static methods to mock out, the delegate solution is shorter. You don't have to create IMessageBoxCreator and its implementation. Did you ever consider anything like that?

Many developers often start with the GUI (Win / Web) designer. Then I double click on a button and quick the handler for the event is generated and I start implementing it.
I imagine a good way to seperate the code could be never opening the form in code view.
The proposed Interface (IView) could be implemented in the inherited class so that I never would touch the generated class.

Hope I wasn't confusing enough:-)
# August 15, 2005 4:53 PM

Jeremy D. Miller -- The Shade Tree Developer said:

A couple of weeks ago I wrote about using the Inversion of Control (IoC) principle to create classes...
# October 6, 2005 11:58 AM

Jeremy D. Miller -- The Shade Tree Developer said:

Unit Testing Loves Beta Testing And Vice Versa
Phil Haack has a great post up in response to some folks...
# October 21, 2005 12:02 AM

Tewfik Zeghmi said:

ProblemI’ve been stumped by this problem, of truly separating UI presentation logic and UI specific code....
# October 26, 2005 5:10 PM

Tewfik Zeghmi said:

ProblemI’ve been stumped by this problem, of truly separating UI presentation logic and UI specific code....
# October 27, 2005 12:33 AM

Dan Bunea said:



Source code: Download


Lately, I have noticed that the Humble Dialog Box or Model...
# November 28, 2005 3:05 AM

Dan Bunea said:



Source code: Download


Lately, I have noticed that the Humble Dialog Box or Model...
# November 28, 2005 8:29 AM

Jeremy D. Miller -- The Shade Tree Developer said:

I’m in a dry spell for blogging, so here’s a rehash of a presentation I gave internally at work earlier...
# December 19, 2005 7:38 AM

Jeremy D. Miller -- The Shade Tree Developer said:

Author: <a href="blogs/jeremy.miller/">Jeremy D. Miller</a><br>You’ll frequently hear teams say they didn’t write unit tests for a particular area of the code because it was just too hard to test. I think one of the biggest challenges of using TDD is learning strategies for isolating the code that’s hard to test and writing code that is easy to test. One of the primary strategies to extend unit test coverage into those hard to reach places is to use mock objects, stubs, or other fake objects in place of the external resources so that your tests don’t involve the external resources at all.
# December 19, 2005 10:49 AM

Jeremy D. Miller -- The Shade Tree Developer said:

Author: <a href="blogs/jeremy.miller/">Jeremy D. Miller</a><br>You’ll frequently hear teams say they didn’t write unit tests for a particular area of the code because it was just too hard to test. I think one of the biggest challenges of using TDD is learning strategies for isolating the code that’s hard to test and writing code that is easy to test. One of the primary strategies to extend unit test coverage into those hard to reach places is to use mock objects, stubs, or other fake objects i
# December 20, 2005 5:24 PM

Jeremy D. Miller -- The Shade Tree Developer said:

Author: <a href="blogs/jeremy.miller/">Jeremy D. Miller</a><br>You’ll frequently hear teams say they didn’t write unit tests for a particular area of the code because it was just too hard to test. I think one of the biggest challenges of using TDD is learning strategies for isolating the code that’s hard to test and writing code that is easy to test. One of the primary strategies to extend unit test coverage into those hard to reach places is to use mock objects, stubs, or other fake objects i
# January 14, 2006 10:24 AM

Jeremy D. Miller -- The Shade Tree Developer said:

A friend of mine was asking me a while back about ways to apply the Model View Presenter (the “Humble...
# February 1, 2006 11:17 PM

Jeremy D. Miller -- The Shade Tree Developer said:

Author: <a href="blogs/jeremy.miller/">Jeremy D. Miller</a><br />A friend of mine was asking me a while back about ways to apply the Model View Presenter (the “Humble Dialog Box”) pattern to ASP.Net development to promote easier unit testing of the user interface layer. Two weeks and a major case of writer’s block later, I finally finished the post. I wrote a blog post last spring describing the usage of MVP (“The Humble Dialog Box”) with WinForms clients, but web development in general and ASP.Net in specific comes with a different set of challenges.
# February 2, 2006 7:11 PM

Jeremy D. Miller -- The Shade Tree Developer said:

This past Saturday was the Austin Code Camp.  I had a great time and the general consensus was that...
# March 6, 2006 8:32 AM

Billy McCafferty said:

# March 15, 2006 7:58 PM

Billy McCafferty said:

# March 15, 2006 10:10 PM

Jeremy D. Miller -- The Shade Tree Developer said:

If you're in Austin, I'm doing a presentation on StructureMap at the ADNUG meeting on July 10th.
Mea...
# June 29, 2006 7:02 PM

Jeremy D. Miller -- The Shade Tree Developer said:

The slide deck and sample code from my StructureMap talk last night is available from the ADNUG downloads...
# July 11, 2006 10:52 AM

风满袖 said:

这里的MVP不是微软的那个MVP,而是一个设计模式Model-Viewer-Presenter。最早(2000年)由IBM开发出来的一个针对C  和Java的编程模型,它是MVC模式的变种。但我们可以把MVP应用到ASP.NET的应用中,以克服code-behind的各种弊端。
# July 12, 2006 11:52 PM

Vivek said:

I am newbie in StructureMap. The most difficult part that I found in StructureMap is generation of Config file. I am not able to find a single .Net example( Full Source Code) with related Config file. If you have some link please send it to me.

my email is : VivekP@ecoaxisindia.com

# August 30, 2007 2:30 AM

Leave a Comment

(required)  
(optional)
(required)  

Enter the numbers above:
Add

About Jeremy D. Miller

Jeremy began his IT career writing "Shadow IT" applications to automate his engineering documentation, then wandered into software development because it looked like more fun. Jeremy previously worked as a systems architect building mission critical supply chain software for a Fortune 100 company and learned agile development practices as a .Net consultant at ThoughtWorks, one of the pioneers of agile development. Jeremy is the author of the open source StructureMap (http://structuremap.sourceforge.net) tool for Dependency Injection with .Net and the forthcoming StoryTeller (http://storyteller.tigris.org) tool for supercharged FIT testing in .Net. Jeremy's thoughts on just about everything software related can be found on his weblog "The Shade Tree Developer" at http://codebetter.com/blogs/jeremy.miller, part of the popular CodeBetter site. Jeremy is a Microsoft MVP for C#. Check out Devlicio.us!

This Blog

Syndication

News

All opinions expressed here constitute my (Jeremy D. Miller's) personal opinion, and do not necessarily represent the opinion of any other organization or person, including (but not limited to) my fellow employees, my employer, its clients or their agents.

About Me

"Best Of" Compendium

StructureMap (Dependency Injection for .Net)

StoryTeller (Supercharged Fit)

Build your own Cab

TestDriven

MVP