IUIs and Web-Style Navigation in Windows Forms, Part 2

 

Michael Weinhardt
www.mikedub.net

September 10, 2004

Summary: Michael Weinhardt continues his exploration of inductive user interfaces by building one using a custom navigation framework for .NET 1.0 and .NET 1.1 Windows Forms applications. (14 printed pages)

Download the winforms09212004_sample.msi file.

Where Were We?

In last month's installment, we saw how return on investment (ROI) is an important consideration to make with regard to an application's user experience. Higher ROI is generally achieved as a result of performing tasks more frequently and, consequently, being forced to relearn less the next time they are performed. Lower ROI, on the other hand, results from less frequent tasks that require users to spend more time relearning them. To demonstrate, we identified the Northwind database's Regions table as potentially supporting the following infrequently used tasks:

  • Adding a new region
  • Renaming an existing region
  • Deleting an existing region
  • Combining one region with another
  • Moving territories from one region to another

We then discussed two types of UI designs—deductive and inductive. Generally speaking, the former puts the onus on the user to manage and learn a task, while the latter takes on the onus to guide the user through a task. The latter consequently turns out to be a great UI design choice for infrequently used tasks, particularly if they incorporate web-style navigation support. Jan Miksovsky's (Microsoft, Longhorn User Experience Team) work in the IUI space includes the development of a navigation library, Microsoft.Samples.Windows.Forms.Navigation, that allows Windows Forms developers to more easily develop web-style IUIs. We began using this library by creating a customized Northwind Region Manager explorer-style host simply by deriving from Microsoft.Samples.Windows.Forms.Navigation.Explorer. The resulting RegionExplorer is shown in Figure 1.

Figure 1. Stylized RegionExplorer browser host

In this installment, we'll continue utilizing the Navigation library to convert RegionExplorer from an empty shell into a fully functional Northwind Region Manager, illustrated in Figure 2.

Figure 2. Northwind Region Manager

As we saw in part 1, Explorer (and RegionExplorer, by way of derivation) is a lightweight UI built on top of a complete navigation framework. The real work is performed by the Frame type, which handles navigation to and between one or more pages (like a Web applications). The nature of the Navigation library's infrastructure is such that all we need to do to build the Northwind Region Manager is build the pages themselves and leave the underlying internals to take care of the rest. We'll need to build one page for each step in each of the designated tasks, as well as the home page.

Building the Home Page

Because the home page is the first page that will be navigated to, it seems a natural place to start. Whereas Windows Forms derive from System.Windows.Forms.Form, our IUIs pages will derive from Microsoft.Samples.Windows.Forms.Navigation.Page:

public class HomePage : 
  Microsoft.Samples.Windows.Forms.Navigation.Page {...}

Because Page itself derives from System.Windows.Forms.UserControl, instead of manually creating and coding up a class, we can create a new page using the designer by following these steps (making sure the Navigation library is referenced by your project):

  1. In Visual Studio .NET, open Solution Explorer
  2. Right-click on the appropriate project.
  3. Click Add | Add Inherited Control.
  4. In the Templates list, select Inherited User Control.
  5. Enter a suitable name, like HomePage.cs.
  6. Click Open, which opens the Inheritance Picker dialog.
  7. Select Page as the base implementation you'd like to inherit.
  8. Inheriting from UserControl also means that your page can be visually and programmatically designed, just like a form. Figure 3 shows the home page with appropriate controls.

Figure 3. HomePage, derived from Page, in the Windows Forms Designer

The controls I've used to build the page include:

  • A PictureBox for the enhanced region image
  • A Label for the page title
  • A ListBox to list currently active regions
  • One LinkLabel to browse the Help page, and to browse to the first page of each of the identified tasks

The LinkLabel is used instead of traditional command buttons to keep the Web-style experience as familiar and consistent as possible with a Web application. The actual LinkLabel I've used is provided by the Navigation library, which derives from System.Windows.Forms.LinkLabel and is enhanced to ensure active link appearance (underlined and colored with the ActiveLinkColor) when the mouse both hovers over it and clicks it, the latter only being supported by the LinkLabel control.

Running the Home Page

Once created, you'll need to instruct your application on which page to load into RegionExplorer upon execution. This turns out to be a case of creating an instance of the RegionExplorer host and passing it an instance of the home page. The base Microsoft.Samples.Windows.Forms.Navigation.Explorer class happens to implement a constructor overload designed to assist us in this very endeavor:

namespace Microsoft.Samples.Windows.Forms.Navigation {
  public class Explorer : System.Windows.Forms.Form {
    public Explorer() {...}
    public Explorer(Page homePage) : this() {...}
  }
}

RegionExplorer also acquires the same capability because, as a derivation, it must re-implement its base class's constructors if overloaded:

namespace winforms08172004_RegionManagerSample {
  public class RegionExplorer : Explorer {
    public RegionExplorer() : base() {...}
    public RegionExplorer(Page homePage) : base(homePage) {...}   
  }
}

With this in place, opening RegionExplorer with HomePage when the application starts is simply a matter of taking advantage of the constructor overload from within the application's static entry point:

public class EntryPoint {
  [STAThread]
  public static void Main() {     
    // Create home page and pass it to explorer as the home page
    Page homePage = new HomePage();
    RegionExplorer ex = new RegionExplorer(homePage);
    // Set RegionExplorer title bar text
    ex.ApplicationName = "Northwind Region Manager";
    // Run application
    Application.Run(ex);
  }
} 

Figure 4 shows HomePage running within a hidden Frame, itself hosted by RegionExplorer.

Figure 4. RegionExplorer hosting HomePage (within a hidden Frame)

Note All pages will be shown within the Frame as Figure 4 highlights.

A home page is typically a base of operations from which the real tasks are initiated. Users initiate tasks by clicking the appropriate link label and being navigated to the appropriate page. When a task is either completed successfully or cancelled, users are typically navigated back to the home page (or whichever page they initiated a task from). Microsoft.Samples.Windows.Forms.Navigation.Page offers a several methods that, through your derived page classes, allow you to programmatically support forward, back, and home navigation.

We'll begin by looking at the most simple example of navigation, which is navigating to another page such as a help page, show in Figure 5.

Figure 5. Static Help page

Page navigation can most easily be implemented by call Page.Go from your derived page. HomePage does this to navigate to the help page:

public class HomePage : Microsoft.Samples.Windows.Forms.Navigation.Page {
  ...
void helpLink_LinkClicked(
  object sender, LinkLabelLinkClickedEventArgs e) {
    this.Go(typeof(HelpPage));
  }
  ...
}

You might by wondering why we are passing a type instead on an actual object instance to Go. This method call implicitly requests the Navigation library to create and treat the type being passed as a static page. Just as in Web applications, the Navigation library makes the distinction between static and dynamic pages. Static pages, like our simple help page, look the same from one browse to the next. Because their content doesn't change, there's no need to repeatedly create new instances of them and sacrifice application performance. The Navigation library supports this by creating a single instance of the page type you specified and adding it to an internally-managed list of static pages. Internally, Go creates a new instance of the type specified, adds it to the internal list, and browses to it. Subsequent calls to Go open the existing instance held in the internal list.

On the other hand, some pages will have content that differs from one navigation to the next, such as RenameRegionPage, which displays the name of the region being renamed, shown in Figure 6.

Figure 6. Dynamic Rename Region page

The Navigation library caters for dynamic pages with an overload of the Go method that you pass a page instance to:

public class HomePage : Microsoft.Samples.Windows.Forms.Navigation.Page {
  ...
void renameRegionLink_LinkClicked(
  object sender, LinkLabelLinkClickedEventArgs e) {
    this.Go(new RenameRegionPage(GetSelectedRegionID(),
                                 GetSelectedRegionName()));
  }
  ...
}

In this sample, we are not only creating our own page instance, we are also passing it data that is unique to this instance. Just as with static pages, the Navigation library retains and shows this instance as users navigate back or forwards through history. And, because each dynamic page instance retains appropriate state data, you can be assured that it performs the same task it was originally instantiated to do.

You might be wondering why the previous code sample doesn't retain a reference to the new RenameRegionPage instance. You'd see this in typical Windows Forms applications when main forms use dialogs to gather data that are actually processed by the main form when accepted by a user. RegionManager eschews this technique because the free-form navigation supported by Web-style applications leads to possibilities where, in fact, several instances of the same task page can be accessible through navigation history, as shown in Figure 7.

Figure 7. Multiple RenameRegionPage instances in navigation history

This is a cumulative result of various combinations of navigation, such as clicking links and navigating through history, without explicitly ending any tasks by clicking either the OK or Cancel buttons for example. Leaving a task incomplete is entirely valid and users should be able to navigate back to those pages to complete the desired tasks. After all, this is a great feature of Web-style navigation. However, in the face of multiple page instances, it quickly becomes difficult for the home page to remember which page instance the user is navigating back from. As such, it is easier to employ a fire-and-forget approach that relies on page instances to manage and respond to changes in their own state as appropriate, like renaming a region. This is necessarily different to the more traditional main form/dialog pattern generally found in Windows Forms applications that relies on dialogs to gather state changes and the main form to respond to them.

Explicitly Ending a Task

When users do finally explicitly end a task, the responsibility for navigating them appropriately is up to you. If your IUI has only a single task, of which Wizards are great examples, it makes sense to completely finish the task. However, if your IUI is more like RegionExplorer and supports multiple tasks, navigating users back to the home page to begin other tasks is more suitable. As you might expect, Microsoft.Samples.Windows.Forms.Navigation.Page also offers the ability to navigate back to the home page:

public class RenameRegionPage : 
  Microsoft.Samples.Windows.Forms.Navigation.Page {
  ...
void renameRegionButton_Click(object sender, EventArgs e) {
    ...
    // Rename region 
    ...
    this.GoHome();
    ...
  }  
  void cancelButton_Click(object sender, EventArgs e) {
    this.GoHome();
  }
  ...
}

GoHome instructs the host Frame to load the home page, the page instance stored in its HomePage property. Explorer also calls its Frame's GoHome method when the Home navigation toolbar button is clicked. Note that while the home page is a page instance you pass to Explorer’s constructor, it is really a singleton because the same instance is exposed with the HomePage property. It is the only page that is neither static nor dynamic.

One of the benefits of combining Web-style navigation with the IUI concept is it allows users to navigate backwards and forwards through the current task until appropriately complete, or backwards and forwards through several tasks. Such functionality is natively supported by Frame and exposed using the Page type through the GoForward and GoBack methods. Calls to either of these functions instruct the host Frame instance to navigate to the next or previous Page in its backwards and forwards navigation stacks or histories. Explorer calls the Frame versions directly when either Back or Forwards navigation toolbar buttons are clicked:

public class Explorer : System.Windows.Forms.Form {
  ...
  void goBack_Click(object sender, EventArgs e) {
    frame.GoBack();
  }
  void goForward_Click(object sender, EventArgs e) {
    frame.GoForward();
  }
  ...
}

Both GoBack and GoForward have an overload that allows a caller to specify a number of pages to move back or forwards through the navigation history. The benefits of navigation are plainly obvious to anyone who has used a Web browser, and the experience is comparable, as shown in Figure 8.

Figure 8. Navigating back to the home page

While RegionExplorer doesn't need to call either because it derives from Explorer, you may need to consider this when you build a completely new explorer from scratch.

Removing a Task Page on Task End

While there is a benefit associated with back and forwards navigation support, there is also a potential issue that stems from the fact that you are navigating back to page instances that may have been built dynamically to reflect specific piece or set of data. For example, if a user deletes region "1," then browses back to a rename region page created for region "1" and tries to rename it, a SQL Server exception will occur since region "1" obviously doesn't exist. The simplest way to avoid confusing users and causing exceptions is to prevent them from performing a task they shouldn't be able to perform. As it turns out, the Page type implements a CanEnter method that you can override to report whether or not this page instance can be re-browsed to:

public class RenameRegionPage : 
  Microsoft.Samples.Windows.Forms.Navigation.Page {
  ...
  bool _completed = false;
  ...
public override bool CanEnter(ViewState viewState) {
    return !_completed;
  }
void renameRegionButton_Click(
  object sender, 
  System.EventArgs e) {
    ...
    // Prevent further renaming
    _completed = true;
    ...
  }
  ...
}

When you browse back to this page, but before it is loaded into the host Frame, the host Frame itself calls CanEnter. If CanEnter returns a value of true, the host Frame completes the page navigation and displays it. If CanEnter returns false, however, the host frame removes the page instance from the navigation history completely and continues trying to navigate back until it finds a page instance that can be loaded.

Page Template

With great navigation support comes great responsibility. One such responsibility is to ensure a consistent user experience across your pages, which helps to provide a warm and happy context for your users. In Windows Forms, you can use visual inheritance to ensure all forms have the same general look and feel by creating a base form with the common visual elements and having subsequent forms derive from it to inherit them. Because Page derives from UserControl, we can take advantage visual inheritance by creating a page template with the elements that are common to the Northwind Region Manager IUI, including the region image, the page title and the help link. This involves creating PageTemplate in the same manner we used to create the HomePage and dropping on and configuring the appropriate controls to leave us with Figure 9.

Figure 9. Northwind Region Manager page template

It's pretty easy to determine that the only designable area, that is the only area where developers can actually place controls and configure them, is the area on the bottom right. We can enforce these rules with visual inheritance by setting the access modifiers on both the region image PictureBox and the Panel that contains both page title and help link to Private, thereby preventing derivations from altering them. Of course, developers should be able to actually change the page title Label's text from page to page. Allowing them to do so involves creating an override of the Text property in PageTemplate:

public override String Text {
  set {
    base.Text = value;
    if (pageTitle == null) {
      pageTitle.Hide();
    }
    else {
      pageTitle.Text = value;
      pageTitle.Show();
    }
  }
}

PageTemplate also implements a ShowHelp property that allows developers to set whether the help link is shown or not (it wouldn't make sense to show it on a help page, for instance):

bool ShowHelp {
  get { return this.helpLink.Visible; }
  set { this.helpLink.Visible = value; }
}

Subsequent application pages, such as the HomePage, should now derive from PageTemplate:

public class HomePage : PageTemplate {...}

The designer behaves a little differently when a visually inherited form is open, by using special icons to signify derived visual elements and preventing property changes of any controls set with Private modifiers, illustrated by Figure 10.

Figure 10. Visually inherited HomePage

Each page in the Northwind Region Manager derives from PageTemplate and I'll leave it up to you to further explore this aspect of the implementation.

Where Are We?

In this installment, we utilized Jan Miksovsky's Navigation library to build the Northwind Region Manager as a Web-style inductive user interface. We saw how to derive from Page to build your custom pages, as well as using a page template to reduce development effort and ensure a consistent look and feel across the application. We then covered a variety of scenarios enabled by the Navigation library. Please take time to explore the Navigation library's sample, especially as it covers scenarios beyond the scope of this installment, including the use of view state.

Sample Installation

If you do not have SQL Server or the SQL Server version of the Northwind database on which the sample is based, the sample includes a readme file (MSDE2000NorthwindDbInstallReadMe.html) that outlines how you can install the free Microsoft SQL Server Desktop Engine 2000 and Northwind database, as well as how updating the sample solution to accordingly. Note that the Northwind Region Manager does not support deleting the entire set of regions as doing so would introduce functionality beyond the scope of this article.

Acknowledgements

Thanks to Chris Sells for introducing me to Jan and his work, for asking me to write about it, and for continuing to not let me off the writing hook.

References

Michael Weinhardt is currently working full-time on various .NET writing commitments that include co-authoring Windows Forms Programming in C#, 2nd Edition (Addison Wesley) with Chris Sells and writing this column. Michael loves .NET in general and Windows Forms specifically. He is also often accused of overrating the quality of 80s music, a period he believes to be the most progressive in modern history. Visit www.mikedub.net for further information.