Isolate Your UI Code Before It Invades Your Business Layer
Mark Seemann
This article discusses:
| This article uses the following technologies: C#, NUnit |
Contents
Many applications today have complex user interface requirements with many different screens and complex logic dictating their interaction. Most developers know that they should design their apps in layers, but my experience has shown me that in reality it is much more difficult than anticipated to isolate the user interface from the rest of the application.
The User Interface Process Application Block (UIP) from the Microsoft Patterns & Practices team can help you isolate your UI, but why stop there? With UIP you can now write unit tests of all (or most) of your UI logic, and the UI finally becomes the thin layer on top of the app that it was always meant to be.
Using a simple example, I will show you how to use UIP to create a multiscreen application and how to write unit tests for the user interface logic. The example starts out as a Windows
® Forms application, but the actual UI contains so little code that it subsequently becomes very easy to create a second, ASP.NET user interface for the application. I'll also briefly discuss how this works.
Thick User Interfaces
Ideally, user interfaces should only be thin shells on top of the next layer in an application. UI should really only do two things: display information in a useful fashion, and pass user input as quickly as possible to some code that knows what to do with it. In reality, however, user interface code usually includes a lot of logic which determines where to go next, how to communicate user selections to the next screen, where to get data, and so on. This is particularly prevalent in Web applications, where there's no obvious, object-oriented way to pass data between two Web pages. Instead, Web applications typically include lots of code which transforms page variables into form fields or query strings, as well as code which reads, validates, and transforms these fields and query strings to input. Typically, you will see liberal use of code like this:
This sort of code is bad for several reasons. You can probably see a lot of the problems yourself, so I'll focus on the main ones: maintenance and testability.
string searchPhrase = Request.QueryString["SearchPhrase"]; searchPhrase = Server.UrlDecode(searchPhrase); int page = 1; try { page = int.Parse(Request.QueryString["SearchPage"]); } catch{} // Use searchPhrase and page in further code
Such code relates to an application's infrastructure and has nothing to do with the actual task at hand. Each page probably has its own distinct set of input variables it needs to handle, so it's difficult to extract this infrastructure code into a common library. The code gets harder to read, and when you need to modify a form field, you need to modify all the pages that handle the field. The application quickly becomes hard to maintain and extend.
It is typically more difficult to perform automated tests on code residing in the user interface, and the more logic contained there, the greater the need for automated testing. With Web applications, automated user interface test tools exist, but such tests are typically more brittle than unit tests. With Windows-based applications, such test tools are less prevalent and, although managed Windows applications don't have the same need for infrastructure code as Web applications, user interface logic can quickly become bloated just the same.
Implementing Model View Controller
As with most problems in object-oriented development, the solution is to add a new level of indirection. The key solution is to insert a new controlling layer between the user interface and the domain model. There's a basic design pattern called Model View Controller which describes this approach.
Instead of letting a Web page or Windows form communicate directly with the domain model, a controller manages the communication between the two other layers. In good, layered fashion, the domain model is completely unaware of both controller and view. The controller uses the domain model, but is unaware of the view and the view uses the controller but is unaware of the domain model. Layers are unaware of their consumers, but may raise events to notify them of changes.
Instead of implementing Model View Controller yourself, you can use the UIP Application Block. This reusable code module provides a sophisticated implementation of Model View Controller and is available at
User Interface Process (UIP) Application Block.
With UIP, you can create controllers for many different scenarios. In the example I'm about to show you, I use UIP in a fashion that closely follows the Application Controller design pattern, as described in Martin Fowler's
Patterns of Enterprise Application Architecture (Addison-Wesley, 2002).
A Trip Around the Solar System
Figure 1
Booking Application UI Flow
In this age of emerging space tourism, let's imagine a travel company selling tickets to various destinations in our solar system. They need a booking application for their salespeople to record customer travel requests. As an example of how to use UIP, I'll show you how to create and unit-test a simple booking application. The workflow of the booking application is shown in
Figure 1. In its initial state, the application is just waiting for a new booking process to begin. When it does, customer information is recorded as the first step. Then a travel destination is selected. As the third step, a summary screen is shown with the trip details so far. From the trip details screen, it is possible to add more travel destinations, book the trip, or cancel the whole operation. While looking at the sample application, I will focus on the controller, since creating domain logic and user interfaces should be familiar to most of you.
Creating the Controller
The Controller is not your business logic layer, but rather it is a thin adapter over the business logic layer which basically intercepts calls from the user interface in order to implement abstraction and state management. You may as well know up front that creating and configuring the controller is a bit complicated, but it's well worth the effort. The first thing to do is to create a controller class and set up the application's configuration file. Creating the controller is as easy as the following:
public class BookingController : ControllerBase { public BookingController(Navigator context) : base(context) {} }
BookingController inherits from Microsoft.ApplicationBlocks.UIProcess.ControllerBase and implements a single constructor, which takes a Navigator object as the only parameter. In UIP, the Navigator object manages the control flow of the user interface process. Depending on your needs, different navigators are available in UIP to direct the interaction of views. In the booking sample application, there are well-defined transitions between each view, so I'll use a navigator object called a Graph Navigator.
The Graph Navigator uses the application's configuration file to determine which views are available and which transitions are legal (the navigation graph reduces the user interaction workflow to an explicit finite state machine encoded in the configuration XML).
Figure 2 shows the navigationGraph section of the UIP configuration. Notice how each view and transition in
Figure 1 is defined in the configuration. Together with other configuration data, UIP uses this information to control transitions between views.
Figure 2 navigationGraph of UIP
<navigationGraph iViewManager="WindowsFormViewManager" name="BookingManager" state="State" statePersist="MemoryPersistState" startView="Start"> <node view="Start"> <navigateTo navigateValue="createNewTrip" view="PassengerInformation" /> </node> <node view="PassengerInformation"> <navigateTo navigateValue="addLegToTrip" view="SelectDestination" /> </node> <node view="SelectDestination"> <navigateTo navigateValue="confirmSelection" view="TripDetails" /> </node> <node view="TripDetails"> <navigateTo navigateValue="addLegToTrip" view="SelectDestination" /> <navigateTo navigateValue="bookTrip" view="TripDetails" /> <navigateTo navigateValue="cancel" view="Start" /> </node> </navigationGraph>
As a controller manages communication between views, it needs some way to communicate the state between one view and the next. UIP does this by containing a state object in each controller. You can use the default State class, which is basically a serializable dictionary object with a few more properties, but to make development a little easier, I usually prefer to create a strongly typed state class instead. Notice that you can achieve the same functionality with the default State class, so the main purpose of a strongly typed state class is to enhance code readability and maintainability.
Creating a strongly typed state class for UIP isn't difficult at all, and it's described well in the documentation, so I won't go into details. Just think about the state class as a strongly typed property bag with the following public API:
If you are interested in learning more about how the BookingState class is implemented, it's available in the download that accompanies this article, available at the
MSDN
®
Magazine Web site.
public class BookingState : State { public event StateChangedEventHandler StateChanged; public void Reset(); public string FirstName { get; set; } public bool IsBooked { get; set; } public string LastName { get; set; } public StringCollection SelectedDestinations { get; } public TravelClass TravelClass { get; set; } ... }
You need to instruct UIP to use the BookingState and BookingController classes. You do this in the application configuration file:
This code causes UIP to populate BookingController's State property with an instance of the BookingState class. Although I could access the property and cast it to BookingState, this doesn't make my code any more readable, so for convenience I've created a property on the BookingController class which makes it easier for me to access my strongly typed state class:
<objectTypes> ... <state name="State" type="BookingUip.BookingState, BookingUip, Version=null, Culture=neutral, PublicKeyToken=null" /> <controller name="BookingController" type="BookingUip.BookingController, BookingUip, Version=null, Culture=neutral, PublicKeyToken=null" /> ... </objectTypes>
public BookingState BookingState { get { return (BookingState)this.State; } }
Finally, I'm ready to begin creating the real logic of BookingController. To start, I'll create a method to begin a new booking process. This corresponds to making the first, "Create New Trip" transition in
Figure 1:
As you can see, it's pretty simple stuff. Since it's a new booking process, it's always a good idea to reset the internal state. After modifying the state in UIP, you should always call the Save method. For this application, I'm using an in-memory state. If I decide to change to a persistable state, it's necessary to call the Save method to persist the new state to the underlying storage.
public void CreateNewTrip() { this.BookingState.Reset(); this.BookingState.Save(); this.Navigate("createNewTrip"); }
The last line of code instructs UIP to perform a navigation action. Note that I'm not telling it which view it should navigate to, but rather which transition to perform. If you take a look at
Figure 2 again, you will see that the createNewTrip transition indeed takes me from the Start view to the PassengerInformation view.
Preparing a Unit Test Project
Obviously I'll need to add more methods to the controller, but before doing that, I want to create a unit test for the CreateNewTrip method. Like creating a controller, it takes a bit of effort to set up the unit test project. Currently, I'm using NUnit 2.2 (available at
www.nunit.org) for unit testing, and I always create a separate project for unit tests. After adding the appropriate references to the new project, you need to create a configuration file to hold all the UIP configuration entries.
Since the unit test project is a library project, Visual Studio
® .NET isn't going to copy the App.config file from the project folder to the output folder or rename it properly. For that reason, it's necessary to instruct Visual Studio to do so manually. This can be done by adding the following post-build event to the unit test project:
This will copy and rename the App.config file to the output folder. My unit test project is called BookingUipUnitTests, so in debug mode the file will by copied to /bin/debug/BookingUipUnitTests.dll.config. NUnit will then automatically pick up and use the configuration file just like any other app config file.
copy "$(ProjectDir)App.config" "$(TargetDir)$(TargetFileName).config"
Although it's possible to reuse most of the configuration settings from the real application that's being developed, it's necessary to modify a few settings to make it work under unit test conditions. Since the whole idea of unit testing is to test an isolated library with as few dependencies as possible, it's necessary to remove any references to the UI project from the configuration file.
The UIP section of the configuration file has a section where you define how each view is implemented. In the real presentation layer application, this section is used to specify which Windows Forms or Web pages correspond to which views. UIP then uses this information to activate and show the correct view based on the configured navigation graph.Stub View and View Manager
In my article, I create a test view as a Windows Form which doesn't really do anything, as a stub for testing. While this method works well, it may not be the best way. A better approach would be to use a dedicated test stub view.
In UIP, views are classes that implement the IView interface. The application block comes with three view classes (WebFormView, WindowsFormView, and WindowsFormControlView), but new implementations can be created. Views are managed by corresponding view managers, which are classes implementing the IViewManager interface.
For unit-testing purposes, a stub view and corresponding view manager can be created. The purpose of creating these stubs would be to do as little as possible, but still provide stubs for UIP to invoke when necessary. As such, the stub view manager, shown in
Figure A, is simple.
As you can tell, StubViewManager doesn't do much besides implement the IViewManager interface. Creating a stub view is even simpler, as shown in
Figure B.
When using the stub view and view manager classes for unit testing, the configuration file for the testing project should be modified to reflect the changes. Instead of the WindowsFormViewManager used in the article, the StubViewManager class is used, as shown here:
<iViewManager name="StubViewManager" type="BookingUipUnitTests.StubViewManager, BookingUipUnitTests, Version=null, Culture=neutral, PublicKeyToken=null" />
Correspondingly, the views should now be defined with the StubView class, as shown in
Figure C, instead of the TestForm class used in the article.
Because the stub view manager never activates the stub views, execution time is better than with the test form, which is still being loaded and initialized during each test. An informal test where I ran each configuration ten times indicates that using the stub views and view manager result in 30 percent better performance, so if execution time during unit testing is an issue, this approach can be used to speed things up a bit.
Figure C Configuring Views as Stub Views
<views> <view name="Start" type="BookingUipUnitTests.StubView, BookingUipUnitTests, Version=null, Culture=neutral, PublicKeyToken=null" controller="BookingController" /> <view name="PassengerInformation" type="BookingUipUnitTests.StubView, BookingUipUnitTests, Version=null, Culture=neutral, PublicKeyToken=null" controller="BookingController" /> <view name="SelectDestination" type="BookingUipUnitTests.StubView, BookingUipUnitTests, Version=null, Culture=neutral, PublicKeyToken=null" controller="BookingController" /> <view name="TripDetails" type="BookingUipUnitTests.StubView, BookingUipUnitTests, Version=null, Culture=neutral, PublicKeyToken=null" controller="BookingController" /> </views>
Figure B Creating a Stub View
public class StubView : IView { public void Enable(bool enabled) {} public ControllerBase Controller { get { return null; } } public string ViewName { get { return null; } } public Navigator Navigator { get { return null; } } public Guid TaskId { get { return Guid.Empty; } } public void Initialize( TaskArgumentsHolder args, ViewSettings settings) {} }
Figure A Stub View Manager
public class StubViewManager : IViewManager { private Hashtable tasks_; private readonly static object placeHolder_ = new object(); public StubViewManager() { this.tasks_ = new Hashtable(); } // Not necessary for all ViewManagers public int GetActiveViewCount() { return 0; } // Not necessary for all ViewManagers public void StoreProperty(Guid taskId, string name, object value) {} // Required for web applications only public bool IsRequestCurrentView(IView view, string stateViewName) { return true; } public void ActivateView(string previousView, string view, Navigator context, TaskArgumentsHolder args) { Guid taskId = context.CurrentState.TaskId; this.tasks_[taskId] = StubViewManager.placeHolder_; } void Microsoft.ApplicationBlocks.UIProcess.IViewManager. ActivateView(string previousView, string view, Navigator context) { this.ActivateView(previousView, view, context, null); } public Guid[] GetCurrentTasks() { Guid[] returnValue = new Guid[this.tasks_.Keys.Count]; this.tasks_.Keys.CopyTo(returnValue, 0); return returnValue; } // Required for web applications only public string GetViewNameForCurrentRequest(IView currentView) { return null; } }
In the real application, I've defined a Windows Form class for each view, but I don't want to have a dependency on these classes in my unit test. Additionally, in unit testing I can't have a reliance on any UI, as I may want to run my unit tests as a scheduled job. As always, the solution is to create a stub that can be activated instead of the real UI.
To create a stub, I add a new Windows Form called TestForm to my unit test project. This form contains absolutely no functionality; the only change I make is setting its WindowState property to Minimized so I don't have to watch it flicker on and off when I execute the tests.
With this form, I can now define the views section of the configuration file to use only the stub form. As you can see in
Figure 3, all four views are defined to use the same TestForm class. Now, when the tests execute, UIP will read this configuration file and each time it needs to activate a view, it will create, show, and dispose an instance of TestForm, but it will happen so quickly that you'll hardly notice it. If you find that this approach is a bit of a hack, see the sidebar on how to implement a test stub view.
Figure 3 Defining the Views
<views> <view name="Start" type="BookingUipUnitTests.TestForm, BookingUipUnitTests, Version=null, Culture=neutral, PublicKeyToken=null" controller="BookingController" /> <view name="PassengerInformation" type="BookingUipUnitTests.TestForm, BookingUipUnitTests, Version=null, Culture=neutral, PublicKeyToken=null" controller="BookingController" /> <view name="SelectDestination" type="BookingUipUnitTests.TestForm, BookingUipUnitTests, Version=null, Culture=neutral, PublicKeyToken=null" controller="BookingController" /> <view name="TripDetails" type="BookingUipUnitTests.TestForm, BookingUipUnitTests, Version=null, Culture=neutral, PublicKeyToken=null" controller="BookingController" /> </views>
Unit-Testing the Controller
So far I've only set up and configured the unit test project, but I haven't written any test code. With the entire configuration in place, I can finally create a skeleton class to hold my unit tests, as illustrated in
Figure 4. At this point, I've only created an initialization method and a few helper properties.
Figure 4 Unit Test Class
[TestFixture] public class BookingControllerTests { private BookingController controller_; [SetUp] public void CreateController() { GraphNavigator gn = new GraphNavigator("BookingManager"); this.controller_ = new BookingController(gn); } private GraphNavigator Navigator { get { return (GraphNavigator)this.controller_.Navigator; } } private BookingState State { get { return (BookingState)this.controller_.State; } } }
The CreateController method ensures that I always have an initialized BookingController object available. The controller object is initialized with the graph navigator specified in
Figure 2, as the name of the graph is supplied to the GraphNavigator constructor. The two private properties are just helpers to make the rest of my test code a bit more readable.
To test the CreateNewTrip method, I write the test method in
Figure 5. The first thing I need to do is initialize the associated Navigator object in some way. This can be done in more than one way, but since it's the initiation of a new booking process that I'm testing, I can call the StartTask method on the GraphNavigator. This instructs UIP to begin a new navigation task as defined in the configuration file, and since the configured navigation graph specifies that the start view is the view called Start, this will be the initial view after calling the StartTask method.
Figure 5 Testing CreateNewTrip
[Test] public void InitiateNewTripCreation() { this.Navigator.StartTask(); this.controller_.CreateNewTrip(); StringCollection selectedDestinations = this.controller_.GetSelectedDestinations(); Assert.AreEqual(0, selectedDestinations.Count); bool isBooked = this.controller_.IsTripBooked; Assert.AreEqual(false, isBooked); string newViewName = this.Navigator.CurrentState.CurrentView; Assert.AreEqual("PassengerInformation", newViewName); }
Next I invoke the CreateNewTrip method and validate that the controller has the expected values. The last assertion in the test verifies that after calling CreateNewTrip, the controller has transitioned to the PassengerInformation view, as is also specified in the configuration file. This is basically all it takes to unit-test a method on a controller.
Testing Inside the UI Flow
The first test I showed you demonstrated how to test the controller at the beginning of a navigation task. Things work a little differently when you want to test how the controller behaves in the middle of a UI flow, such as when the user adds a destination to the list of destinations from the Select Destination view.
The controller's method to add a destination is shown here:
public void ConfirmDestinationSelection(string selectedDestination) { this.BookingState.SelectedDestinations.Add(selectedDestination); this.BookingState.Save(); this.Navigate("confirmSelection"); }
It simply adds the new destination to the internal list of destinations, saves the state, and instructs UIP to perform the confirmSelection view transition. To test this method, I write the unit test in
Figure 6. Before invoking the ConfirmDestinationSelection method, I need to put the controller in a state corresponding to the Select Destination view. To do so, I need to tell UIP which view is the current view, and I need to initialize relevant parts of the internal state object.
Figure 6 ConfirmDestinationSelection Method
[Test] public void AddDestination() { this.Navigator.CurrentState.CurrentView = "SelectDestination"; this.State.SelectedDestinations.Add("Earth"); this.controller_.ConfirmDestinationSelection("The Moon"); StringCollection selectedDestinations = this.controller_.GetSelectedDestinations(); Assert.AreEqual(2, selectedDestinations.Count); Assert.AreEqual("Earth", selectedDestinations[0]); Assert.AreEqual("The Moon", selectedDestinations[1]); string newViewName = this.Navigator.CurrentState.CurrentView; Assert.AreEqual("TripDetails", newViewName); }
According to my test scenario I'm now in the middle of the booking process, so I can't initialize the GraphNavigator by calling the StartTask method, as that would begin a new booking process with the Start view. Luckily I can access the Navigator's state, and directly manipulate it by telling it the name of the current view.
Although I don't really have to, I decide that I want to test a scenario in which, in the initial state, the Earth destination has already been added, as this allows me to test that a new destination isn't going to override any destinations that were already selected.
After setting up the initial state of my test, I invoke the ConfirmDestinationSelection method on the controller, which should cause two things to happen. The new destination should have been added to the internal list of destinations, and the view should have changed to the TripDetails view. If these conditions are true, no assertions will fail, and the test will pass.
Raising and Testing Events
According to good object-oriented design, a component should not know anything about its client, but should instead raise events whenever something important happens. In UIP, the controller's State object raises the StateChanged event whenever something happens to change the state. The BookingState class, which inherits from State, implements the same behavior, because every time a property is set, the StateChanged event is raised. Generally, the booking app is not going to care about these events, so it will just ignore them, but for the Trip Details view, I'd like to know if the state changed as a result of a booking, so controls in the view can be updated. Booking a trip is done by the BookTrip method:
public void BookTrip() { /* Should really write complete trip information to * domain model layer, but this is omitted for clarity. */ this.BookingState.IsBooked = true; this.BookingState.Save(); this.Navigate("bookTrip"); }
Nothing much is actually happening here, but since I set the IsBooked property, the StateChanged event ought to be raised. To test this, I write the unit test in
Figure 7. Before invoking the BookTrip method, I set the eventWasRaised_ member variable to false and subscribe to the StateChanged event. When BookTrip is called, this ought to invoke the event handler, where I validate that the event data is as expected. If everything checks out, I set the eventWasRaised_ member variable to true. When the event handler is done executing, control returns to the test method where further checks are performed to validate that the event was, in fact, raised, and that the IsTripBooked property is now true.
Figure 7 Testing the BookTrip Method
// Member variable... private bool eventWasRaised_; [Test] public void BookTrip() { this.Navigator.CurrentState.CurrentView = "TripDetails"; this.eventWasRaised_ = false; this.State.StateChanged += new BookingUip.BookingState.StateChangedEventHandler (BookingControllerTests_StateChanged); this.controller_.BookTrip(); bool booked = this.controller_.IsTripBooked; Assert.AreEqual(true, booked); Assert.AreEqual(true, this.eventWasRaised_); string newViewName = this.Navigator.CurrentState.CurrentView; Assert.AreEqual("TripDetails", newViewName); } private void BookingControllerTests_StateChanged(object sender, StateChangedEventArgs e) { Assert.AreEqual("IsBooked", e.Key); Assert.AreEqual(true, this.State.IsBooked); this.eventWasRaised_ = true; }
Thin User Interfaces
I'll spare you the details of how to implement and unit-test the rest of the BookingController, but if you're interested, the complete controller is available in the sample code download.
Creating the UI for the booking application now becomes quite easy, since most code in the UI just delegates the work to the BookingController. A view in UIP should implement the IView interface, but fortunately, the application block comes with implementations of IView for Windows Forms and Web applications. To create the Start view, for example, I just need to inherit from WindowsFormView:
Since WindowsFormView inherits from the Form class that you'd usually subclass, you're still creating a Windows Form application.
public class StartForm : WindowsFormView { /* Implementation... */ }
The Start view only contains a single button which, when pressed, will start the booking process, so implementing the Start form is simple. The WindowsFormView class that StartForm is inheriting from contains a Controller property to which the real work can be delegated. In StartForm, I can implement the event handler of its single button like this:
Clicking the button will now delegate the real work to the CreateNewTrip method, which will perform its work and subsequently let UIP manage the transition to a new view, as defined in the method. Since this example may seem laughably simple, let's look at the most complex view in the entire application, the Trip Details view, shown in
Figure 8. This view should be able to load existing data, handle three different buttons, and enable and disable some of these buttons according to state. The implementation of this view is shown in
Figure 9.
private void createNewTripButton__Click( object sender, System.EventArgs e) { ((BookingController)this.Controller).CreateNewTrip(); }
Figure 9 Implementation of TripDetailsForm
public class TripDetailsForm : WindowsFormView { // Auto-generated code goes here... private void TripDetailsForm_Load(object sender, System.EventArgs e) { this.firstNameLabel_.Text = this.BookingController.PassengerFirstName; this.lastNameLabel_.Text = this.BookingController.PassengerLastName; this.classLabel_.Text = this.BookingController.TravelClass.ToString(); this.destinationsListBox_.DataSource = this.BookingController.GetSelectedDestinations(); this.BookingController.BookingState.StateChanged += new BookingUip.BookingState.StateChangedEventHandler( BookingState_StateChanged); this.SetButtonState(); } private void addLegToTripButton__Click( object sender, System.EventArgs e) { this.BookingController.SelectDestination(); } private void bookButton__Click(object sender, System.EventArgs e) { this.BookingController.BookTrip(); } private void cancelButton__Click(object sender, System.EventArgs e) { this.BookingController.Cancel(); } private void BookingState_StateChanged (object sender, StateChangedEventArgs e) { if(e.Key == "IsBooked") this.SetButtonState(); } private void SetButtonState() { bool disabled = this.BookingController.IsTripBooked; this.bookButton_.Enabled = !disabled; this.addLegToTripButton_.Enabled = !disabled; } private BookingController BookingController { get { return (BookingController)this.Controller; } } }
Figure 8
The Trip Details Form
Loading the existing data and handling the button click events is trivial, since it just delegates all logic to the BookingController. Managing the state of the buttons is a bit more complex, but still easy to implement. All three buttons should be enabled as long as the trip has not been booked. When the trip is booked, the Add Leg to Trip and Book buttons should be disabled, as the trip can't be edited once it's booked. For that reason, I add an event handler to the StateChanged event so that the view will always be able to react to changes in the underlying state of the controller. If the event is raised, the code investigates whether the event is related to the IsBooked property, and if that is the case, calls the SetButtonState method, which enables or disables the buttons as required.
Conclusion
Using UIP provides several benefits. It pulls the complex part of the user interface code into a library that can be unit-tested, it abstracts away the need to think about communication between views, and it makes it very easy to create different view implementations for the same application. I began the creation of the sample booking program as a Windows Forms application, but when it was complete, I recreated it as an ASP.NET application, and it turned out to be basically the same, small amount of code I had to write.
It took me a couple of hours to create the ASP.NET application, and since UIP abstracts away all communication between views, there are no query strings or hidden form fields involved between the pages. Although UIP may at first seem like a complex and daunting piece of technology, it takes less than a day to learn and get started, and I'm sure that you will agree it is well worth the initial investment in time and effort.
For more information on topics discussed in this article, you should visit NUnit at
www.nunit.org and take a look at User Interface Process Application Block, version 2 at
User Interface Process (UIP) Application Block.