A picture of me

Welcome, my name is Paul Stovell. I live in Brisbane and work on Octopus Deploy, an automated deployment tool for .NET applications.

Prior to founding Octopus Deploy, I worked for an investment bank in London building WPF applications, and before that I worked for Readify, an Australian .NET consulting firm. I also worked on a number of open source projects and was an active user group presenter. I was a Microsoft MVP for WPF from 2006 to 2013.

In 2001, Microsoft published a paper called Inductive User Interface Guidelines. If you have not read this paper, I would suggest doing so. It is largely technology agnostic, and I know that many of my colleagues at Readify consider it to be a classic. In this article I want to look at building inductive user interfaces with WPF.

PS: If you are interested in building WPF Navigation applications, check out Magellan.

Navigation Applications

Navigation applications are applications that are composed of many "pages", and often feel similar to web applications, although not always. A navigation application usually provides some kind of shell in which the pages are hosted, with custom chrome or widgets around it.

The pages of a navigation application can usually be broken down into two categories:

  1. Standalone, free pages.
  2. Pages that make up a step in a process.

For example, an online banking application may have a number of standalone pages, such as:

  • The Welcome page
  • The Account Balance page
  • The Help page

These pages are often read-only, or can be navigated to at any time, and don't really form part of a single "task" that the user performs.

Then there are many tasks the user can perform:

  • Applying for a credit card
  • Paying a bill
  • Requesting to increase their transaction limits

Note that these tasks may comprise of subtasks, which can make our navigation model rather complex. Let's dissect each of these sections and discuss how WPF handles them.

The Shell

The Shell is usually the main window of your application, and it's where your pages are hosted. In WPF, you have two options:

  1. If you are building a XAML Browser Application, or XBAP, the shell will normally be the web browser that is hosting your application. It is possible to create your own shell within the browser window, though it may confuse users.
  2. If you are building a standalone application, you will need to build your own shell.

Building your own shell is as simple as creating a standard WPF Window in a normal WPF application, and using a WPF Frame element. The Frame's job is to declare a section of the window that your pages will be hosted in, and to provide all of the services around navigation. Here is an example:

<Window 
    x:Class="NavigationSample.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Window1" Height="300" Width="300"
    >
    <DockPanel>
        <Frame x:Name="_mainFrame" />
    </DockPanel>
</Window>

From code, you can tell the frame to navigate, like so:

_mainFrame.Navigate(new Page1());

Which just so happens to be a helpful shortcut to:

_mainFrame.NavigationService.Navigate(new Page1());

NavigationService

Navigation comprises many functions: going back, going forward, going to a new page, refreshing a page, and so on. Objects in the WPF Navigation ecosystem, like the Frame class, as well as the Page class which I'll describe shortly, can access this functionality by a NavigationService property, which is, surprisingly of type NavigationService. For example:

_mainFrame.NavigationService.GoBack(); 
_mainFrame.NavigationService.GoForward(); 
_mainFrame.NavigationService.Refresh();

This same object is made available to every page hosted within the frame, so pages can signal that they wish to navigate backwards or forwards. When navigating, the Navigate method will accept just about anything you want:

_mainFrame.NavigationService.Navigate(new Uri("http://www.google.com/"));
_mainFrame.NavigationService.Navigate(new Uri("Page1.xaml", UriKind.Relative));
_mainFrame.NavigationService.Navigate(new Page1());
_mainFrame.NavigationService.Navigate(new Button());
_mainFrame.NavigationService.Navigate("Hello world");

In the first case, it will load a full web browser control with Google within the frame. In the second case, it will resolve a WPF page by looking up the XAML Uri in the application's resources and display the page. In the third case, it will show the same XAML page from a direct object reference. In the fourth, it will display a Button object direct to the screen, without a page around it. And in the last case, it will show a string enclosed within a TextBlock. Frames can show just about anything.

Frames make their content public through the Content dependency property, which makes it easy to bind to. When navigating to a page, for instance, the Content property will give you a Page object. Showing the title of the current page in your shell becomes very trivial:

<Window 
    x:Class="NavigationSample.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    <strong>Title="{Binding Path=Content.Title, ElementName=_mainFrame}"</strong>
    Height="300" Width="300"
    >    
    <DockPanel>
        <Frame x:Name="_mainFrame" />
    </DockPanel>
</Window>

The NavigationService also provides a number of events you can subscribe to, if you want to control the navigation process:

  • Navigating, when the frame is about to navigate. Set Cancel to true to stop.
  • Navigated, when navigation has finished but before it is rendered
  • NavigationFailed, when something goes wrong
  • NavigationProgress, when chunks of a remote navigation call are being downloaded. Nice for progress bars.
  • NavigationStopped, when the StopLoading method is called (like clicking "Stop" in IE) or a new Navigate request is made during downloading
  • LoadCompleted, when the page has been rendered

Customizing the Chrome

The Frame control ships with a standard UI that provides back and forward buttons once you have navigated to a second page. Out of the box, it looks very similar to IE 7.0, although it doesn't take into account your current Windows theme:

A WPF frame with the standard chrome

Fortunately, just like every other WPF control, how the frame looks is completely up to you. Just apply a ControlTemplate and use a ContentPresenter to show the content of the page:

<ControlTemplate TargetType="Frame">
    <DockPanel Margin="7">
        <StackPanel 
            Margin="7"
            Orientation="Horizontal"
            DockPanel.Dock="Top"
            >
            <Button 
                Content="Avast! Go back!" 
                Command="{x:Static NavigationCommands.BrowseBack}" 
                IsEnabled="{TemplateBinding CanGoBack}" 
                />
            <Button 
                Content="Forward you dogs!" 
                Command="{x:Static NavigationCommands.BrowseForward}" 
                IsEnabled="{TemplateBinding CanGoForward}" 
                />
        </StackPanel>

        <Border 
            BorderBrush="Green"
            Margin="7"
            BorderThickness="7"
            Padding="7"
            CornerRadius="7"
            Background="White"
            >
            <ContentPresenter />
        </Border>
    </DockPanel>
</ControlTemplate>

WPF Frame with a custom ControlTemplate

In that example, TemplateBindings are used because the Frame (being templated) exposes CanGoBack and CanGoForward properties. The NavigationCommands are a set of static routed UI commands that the frame will automatically intercept - there's no need for C# event handlers to call NavigationService.GoBack().

The NavigationService and Frame object expose many other properties and events; these are just the tip of the iceberg, and the ones you'll customize most often when building inductive UI's with WPF.

Note: WPF includes a class called NavigationWindow, which is essentially a Window which also doubles as a Frame, by implementing most of the same interfaces. It sounds useful at first, but most of the time you need more control over the Window, so I've never had any need to use this class. I am just pointing it out for the sake of completeness, though your mileage may vary.

Pages

Pages are the cornerstone of navigation applications, and are the primary surface users interact with. Similar to Windows or UserControls, a Page is a XAML markup file with code behind. If you are working with XBAP's, the Page is the root of the application.

Just like a Window or UserControl, a page can only have one child, which you'll need to set to a layout panel to do anything fancy. You can do whatever you like in a WPF Page - 3D, animation, rotation; it's just like any other control.

Using the Navigation Service from a Page

Each Page has a reference to the NavigationService object provided by its container - usually a Frame. Note that the Page and its containing Frame will share the same reference to the NavigationService, so the Page won't be able to reference it during it's constructor.

The Page can make use of the NavigationService's Navigating event to stop users navigating away from the page, by setting the Cancel property on the event arguments. This is useful if the user has unsaved changes on the page, and you want to give them a chance to save them. Don't worry about unsubscribing from the event either - WPF will detect when you navigate away from the page, and automatically removes all event handlers, to avoid any memory leaks (since the NavigationService object lives a lot longer than any particular page would).

Since the page is hosted within the Frame in the Visual Tree, you can also make use of some of the NavigationCommands routed commands, since the parent Frame will automatically handle them:

<Page 
    x:Class="NavigationSample.Page2"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Page2"
    >
    <Grid>
        <Button Command="{x:Static NavigationCommands.BrowseBack}">Cancel</Button>
    </Grid>
</Page>

In addition, you can of course use the NavigationService to perform any other kind of navigation, such as navigating to a new page or going backwards.

Hyperlinks

One additional feature you can use when building pages is the Hyperlink control, and the automatic navigation it provides. Hyperlink is an Inline, so it has to be used within a control like a TextBlock or FlowDocument paragraph. You can use it in other WPF applications, but automatic navigation only works within navigation applications. Here is an example:

<Page 
    x:Class="NavigationSample.Page1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Page1"
    >
    <Grid>
        <TextBlock>
            <Hyperlink NavigateUri="Page2.xaml">Go to page 2</Hyperlink>
        </TextBlock>
    </Grid>
</Page>

Passing data between pages

There are two ways you can pass information between pages using WPF. First, I'll show you the way *not *to do it, then I'll show the preferred way to do it :)

Although it's not obvious, you can pass query string data to a page, and extract it from the path. For example, your hyperlink could pass a value in the URI:

<TextBlock>
    <Hyperlink NavigateUri="Page2.xaml?Message=Hello">Go to page 2</Hyperlink>
</TextBlock>

When the page is loaded, it can extract the parameters via NavigationService.CurrentSource, which returns a Uri object. It can then examine the Uri to pull apart the values. However, I strongly recommend against this approach except in the most dire of circumstances.

A much better approach involves using the overload for NavigationService.Navigate that takes an object for the parameter. You can initialize the object yourself, for example:

Customer selectedCustomer = (Customer)listBox.SelectedItem;
this.NavigationService.Navigate(new CustomerDetailsPage(selectedCustomer));

This assumes the page constructor receives a Customer object as a parameter. This allows you to pass much richer information between pages, and without having to parse strings.

Page Lifecycles

Many people have been confused about how long pages live for and how Back/Forward navigation works. The simplest way to demonstrate this is to create a couple of simple pages, Page1 and Page2, which contain hyperlinks to each other. I am going to write the same tracing code in each page:

public Page1()
{
    InitializeComponent();
    Trace.WriteLine("Page 1 constructed");
}

private void Button_Click(object sender, RoutedEventArgs e)
{
    Trace.WriteLine("Page 1 navigating to page 2");
    this.NavigationService.Navigate(new Uri("Page2.xaml", UriKind.Relative));
}

private void Page_Loaded(object sender, RoutedEventArgs e)
{
    Trace.WriteLine("Page 1 loaded");
}

private void Page_Unloaded(object sender, RoutedEventArgs e)
{
    Trace.WriteLine("Page 1 unloaded");
}

~Page1()
{
    Trace.WriteLine("Page 1 destroyed");
}

I've also handled the Back and Forward buttons myself, so I can trace when they are clicked, and I've added a button to force the garbage collector to run. Here's what happens:

Page 1 navigating to page 2
Page 2 constructed
Page 1 unloaded
Page 2 loaded
Garbage collector runs
Page 1 destroyed
Clicked Back
Page 1 constructed
Page 2 unloaded
Page 1 loaded
Garbage collector runs
Page 2 destroyed

So, the navigation system works as you expect - the page is unloaded, and available for the garbage collector. However, it does become more complicated. Suppose your page required some kind of parameter data to be passed to it:

public Page1(DateTime date)
{
    InitializeComponent();
    Trace.WriteLine("Page 1 constructed " + date.ToString());
}

private void Button_Click(object sender, RoutedEventArgs e)
{
    Trace.WriteLine("Page 1 navigating to page 2");
    this.NavigationService.Navigate(new Page2(DateTime.Now));
}

When navigating, if you click "Back", WPF can't possibly know what values to pass to the constructor; therefore it must keep the page alive. Here's the trace output:

Page 1 navigating to page 2
Page 2 constructed 5/06/2008 12:23:19 PM
Page 1 unloaded
Page 2 loaded
Garbage collector runs
Clicked Back
Page 2 unloaded
Page 1 loaded

Notice that although the pages are loaded and unloaded many times, it is the same page instance. This means that:

  • If you navigate using a URI, WPF will create the page by invoking the constructor each time. The navigation history keeps the URI, not the object.
  • If you navigate passing an object directly, WPF will keep the object alive.

This gives you a few things to consider:

  • Over time, that can amount to some serious memory usage.
  • The Loaded and Unloaded events are called each time the page is shown or disappears, so use them as chances to clear all the data you can in order to minimize memory.
  • The URI navigation mode explained above *can *be useful for these reasons, but don't abuse it :)

Lastly, each page provides a KeepAlive boolean property, which is false by default. However, it only applies when using URI navigation, which explains why many people have tried changing it only to see no difference. Back/forward simply couldn't work if objects were not kept alive, since the navigation service has no way to construct them. An option I'd like to see is being able to pass a lambda or delegate which is used to reload the page.

PageFunctions

WPF Pages serve to allow us to build standalone pages within our application - pages like Welcome, Help, or View Account Balances. However, when it comes to completing tasks, they fall short in a number of ways. Consider the scenario for paying a bill:

A simple navigation workflow

Once the task is complete, you need to return to the Home page. Should the Confirm page be written to Navigate directly to the Home page when a button is clicked? What if the Pay Bill task was triggered from another page? And if the user clicks "Back" upon arriving back at the Home page, should they get to cancel the payment after they already confirmed it? And what if we don't want to allow them to go back?

Now consider a more sophisticated example. Maybe the biller they want to pay doesn't appear in their list of existing billers. You might have another task to define a biller - something like this:

Workflow with a sub-workflow

This causes us to think carefully about our navigation model:

  • Will the Verify and Confirm page be hard-coded to transition to Transaction details?
  • When they get to the Transaction Details page, what will clicking "Back" in the shell do?
    • If it went to Verify and Confirm, we'd be in trouble, since the biller has already been saved

What we are really doing here is interrupting one task to start another - like taking a detour - and then returning to our previous task. After we return, we can then complete the original task. Once a task returns, its navigation journal should be cleared, and it should be as if the detour never happened. This creates a couple of rules:

  • Within a task, you can click back or forward as much as you like, until it returns.
  • When a task is completed, the browser history "stack" is unwound to where it was when the task began

It's helpful to picture this as code:

PaidBill PayBill()
{
    var biller = SelectBiller();
    EnterTransactionDetails(biller);
    return Confirm();
}
Biller SelectBiller()
{
    if (BillerAlreadyExists)
        return SelectExisting();
    else
        return CreateNew();
}

Where each function represents a task that can branch out, and then return to the original task.

Introducing PageFunctions

Now you can begin to sense where Page Functions get their name. WPF Page Functions are a special type of Page that inherit from a PageFunction<T> class. They are standard Page objects, but with one difference: they provide a Return event which makes it possible to unwind the navigation journal.

Returning

Just like our pseudo code above, and like any other kind of function, when a PageFunction returns it needs to declare a type of object that is being returned, so that the page which triggered the PageFunction can get data back from it. Here's what a the code behind might look like:

public partial class VerifyAndConfirmNewBillerPage : PageFunction<Biller>
{
    public VerifyAndConfirmNewBillerPage(Biller newBiller)
    {
        InitializeComponent();
        this.DataContext = newBiller;
    }

    private void ConfirmButton_Click(object sender, RoutedEventArgs e)
    {
        this.OnReturn(new ReturnEventArgs<Biller>((Biller)this.DataContext));
    }
}

Notice that the class inherits from a generic type. When the user clicks "confirm" on this page, it will raise a Return event to whatever page instantiated it, and returns the Biller that was created.

The page that came before this one would need to subscribe to the event in order to expect it. Here is how that would look:

public partial class DefineNewBillerPage : PageFunction<Biller>
{
    public DefineNewBillerPage()
    {
        InitializeComponent();
        this.DataContext = new Biller();
    }

    private void SubmitButton_Click(object sender, RoutedEventArgs e)
    {
        var nextPage = new VerifyAndConfirmNewBillerPage((Biller)this.DataContext);
        nextPage.Return += new ReturnEventHandler<Biller>(BillerVerified);
        this.NavigationService.Navigate(nextPage);
    }

    private void BillerVerified(object sender, ReturnEventArgs<Biller> e) 
    {
        this.OnReturn(e);
    }
}

When the user enters information about their new biller and click "Submit", we navigate to the Verify and Confirm page, and we subscribe to its Return event. On that page, they'll hit Confirm, which will Return control back to our page. Our page will then also return - this is similar to when you make a function call at the last line of an existing function, in that the innermost function's stack is cleared, it returns to the calling function, and then that function returns.

Back at the Select Existing Biller page, here's how that code behind would appear:

public partial class SelectExistingBillerPage : PageFunction<PaidBill>
{
    public SelectExisingBillerPage()
    {
        InitializeComponent();
        this.DataContext = GetAllExisingBillers();
    }

    private void AddNewButton_Click(object sender, RoutedEventArgs e)
    {
        var nextPage = new DefineNewBillerPage();
        nextPage.Return += new ReturnEventHandler<Biller>(NewBillerAdded);
        this.NavigationService.Navigate(nextPage);
    }

    private void NewBillerAdded(object sender, ReturnEventArgs<Biller> e) 
    {
        // A biller has been added - we can pluck it from the event args
        var nextPage = new EnterTransactionDetailsPage(e.Result);
        this.NavigationService.Navigate(nextPage);
    }

    private void SelectExisingButton_Click(object sender, RoutedEventArgs e)
    {
        var nextPage = new EnterTransactionDetailsPage((Biller)listBox1.SelectedItem);
        this.NavigationService.Navigate(nextPage);
    }
}

Note how in the return event handler our page navigates to the Transaction Details page? Thus, our navigation is actually:

Workflow with a sub-workflow using Return events

This model allows pages to complete a task, return to whoever called them, and have the previous task continue without any additional knowledge. To users, it would appear as a detour, where in reality the pages operate just like function calls.

Since one page returns to the caller, it might be worrying that the previous page will quickly flash up before navigating. Fortunately, this is not the case, as WPF waits for the Return event handler to execute before loading or rendering the previous page. If the Return event handler from the calling page asks to navigate somewhere else, WPF would perform the navigation without showing the page.

Markup

PageFunctions do inherit from the Page base class, and apart from the concept of returning, they operate just like any other Page. Since the base class is generic, the XAML accompanying the PageFunction does look slight different:

<PageFunction
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:<strong>domain</strong>="clr-namespace:DomainModel" 
    x:Class="NavigationSample.DefineNewBillerPage"
    <strong>x:TypeArguments="domain:Biller"</strong>
    Title="Define a new Biller"
    >
    <Grid>
        <... />
    </Grid>
</PageFunction>

PageFunction Lifecycles

When using PageFunctions, you need a reference to the object in order to subscribe to the event and for the handler to be invoked, and so navigation with PageFunctions always involves the PageFunction object being left alive in memory; similar to normal pages. Thus, KeepAlive generally has no effect on PageFunctions.

Fortunately, once a PageFunction has returned, it is removed from the navigation journal and can be garbage collected. This means that although the pages that make up a task will consume memory, once the task is finished, the pages are destroyed. In this way, a largely task-focused UI can use the rich navigation model without degrading performance.

Summary

Navigation applications are made up of many pages, which can be categorized as either standalone pages, or pages that contribute towards a step in a user's task. Standalone pages are usually represented as Page objects, and steps within a task are represented as PageFunctions. Pages of any kind can be hosted with a WPF Window using the Frame control, or within an XBAP application hosted by the browser.

The NavigationService object shared between a Page and it's host provides many hooks into the navigation system. Page lifecycles can complicate things, but are logical when you think about them. Pay special attention to the lifetime of your pages and find ways to reduce their memory footprint when not in view (remove all data during the Unloaded event, and restore it during the Loaded event, for example). As always, use memory profilers to detect major leaks.

Navigation applications make it easy to build user interfaces that are task-focused and geared for ease-of-use over productivity. This style of UI is very difficult to achieve using Windows Forms or other UI systems. Even on the web, which shares similar navigation styles, concepts such as journal unwinding, returning to calling pages and remembering state between pages can be notoriously difficult to implement. Entire frameworks in like Struts in Java exist to enable just these scenarios.

Windows Presentation Foundation is the next generation framework for building desktop Windows applications. I have been working with WPF since 2006, and I have written a number of articles on the subject.

Articles

Change Management

Often we want a way to tell whether the user has made changes to data in the UI. Here are some different strategies for dealing with that:

General Tips

Samples

Presentations

The screenshot below is of a sample WPF application that deals with editing a collection of items:

The editable collection

A few features:

  • You can make a batch of changes to the screen and hit "Save".
  • If you deleted items, you can click "Show deleted items" to make them visible again, then click Undo to rollback the deletion
  • The list on the left is bound to the same collection as the list on the right - the changes aren't written to the source until you click Save
  • If you change an item, it highlights in bold
  • If you change an item, you can undo the changes via the Undo button
  • If you add items to the original collection, they will appear on screen (the Remote Update button)

The object representing an individual row is just a simple POCO object with INotifyPropertyChanged support - it has no HasChanges property or similar. Likewise, the collection is just an ObservableCollection<T>.

This is all implemented with a mix of BindableLINQ and the Editable Object Adapter.

Download

Note: This sample uses the Xceed WPF DataGrid control. If you have a license, you can use your license key when running the sample (just set it in App.xaml.cs). Alternatively, you can download a 45 day trial from the Xceed website.

(This page deals with editing a single object - for collections, see IEditableObject Adapter for Collections)

When building data-entry screens, it's nice to be able to tell whether the user has made changes - for example, to prompt if they have unsaved changes, or to highlight changes in the title of the application.

Commonly this is implemented using the memento pattern, or recursively subscribing to PropertyChanged events. Unfortunately this can be time-consuming and repetitive code to write. In this post, I will present another option: I'll make use of CustomTypeDescriptors to add an IEditableObject implementation with a HasChanges property to an existing class.

IEditableObject

IEditableObject is an interface that's been around since the early days of .NET. By implementing it, you can make changes to a data-bound object, and then choose to commit them or roll them back (usually via Save/Cancel buttons). Under the hood, we usually implement this via the memento design pattern.

IEditableObject requires you to implement the following methods:

  • BeginEdit()
  • CancelEdit()
  • EndEdit()

BeginEdit can be called manually, or in the case of the DataGridView, it will be called automatically if implemented. This is the part where you normally take your "snapshot" of the object's state. CancelEdit is then called to rollback the changes, and EndEdit to accept them. Note that BeginEdit can be called multiple times (some weirdness with the DataGridView), so you should only honor the first call.

A common problem with editing data-bound objects is that when you edit the UI, changes are pushed to the underling properties on the object. If that object is visible elsewhere - such as in a list in another Window - you'll see the changes reflected there too, even if the user hasn't "committed" their changes yet. That can be confusing to users. This approach prevents this.

CustomTypeDescriptors

The data binding systems in both Windows Forms and WPF don't go through reflection to find your properties, but instead make use of the [TypeDescriptor][4] class. This class allows you to describe how an object looks - the properties it has, the events it has, and the attributes it has. You can't fake interfaces, but you can fake a TypeConverter which converts to the target interface - a clever way to support interfaces the object doesn't expose.

Some examples of how you might use TypeDescriptors are:

  • To add fake properties to an object
  • To add fake events (Changed events, for example)
  • To provide TypeConverters to simulate implementation of interfaces

Note that when you provide a custom TypeDescriptor, it is honored both at runtime and at design time, as you'll see below.

Idea

Instead of implementing IEditableObject on every class, we'll create a class that wraps our entities following the Adapter design pattern. It will implement IEditableObject for us, as well as INotifyPropertyChanged, and use a CustomTypeDescriptor to expose the properties of the object it wraps. A few goals:

  • When properties are changed on the adapter, they aren't committed to the object being wrapped until EndEdit() is called
  • The object should make minimal use of reflection
  • It should be easy to add extra UI-only properties to the object
  • Full designer support (especially important for Windows Forms)
  • Minimal code to use the functionality

Using the Code

We'll call the adapter EditableAdapter<T>. Assuming you have a Contact class generated by LINQ to SQL, here's how you would use the EditableAdapter<T>:

public class EditableContact : EditableAdapter<Contact>
{
    public EditableContact(Contact contact)
        : base(contact)
    {
    }
}

You wouldn't need to write this class at all in WPF, but the Windows Forms designer does not allow us to bind to generic objects. This class does give us the chance to provide additional UI-only properties, however, like this:

public class EditableContact : EditableAdapter<Contact>
{
    public EditableContact(Contact contact)
        : base(contact)
    {
    }

    public string FullName
    {
        get 
        { 
            return string.Format("{0} {1}", 
                this.WrappedInstance.FirstName,
                this.WrappedInstance.LastName);
        }
    }
}

Note that we didn't have to re-implement every property from the Contact class on our adapter class - that will all be done at runtime through the type descriptor.

Now we can use the Windows Forms designer to bind to the object:

The EditableObject adapter in Windows Forms

Note that the properties appear in the Data Sources window, including our custom FullName property, and the HasChanges property which is provided by the EditableAdapter.

The HasChanges property is a bindable boolean value that indicates whether any property values have been changed. We'll bind that up to the Apply and Reset button's Enabled properties - the buttons will only be enabled when you've got pending changes:

Binding the HasChanges property

Finally, wire up the form's code-behind to handle the various buttons:

public ContactEditDialog(EditableContact contact)
{
    InitializeComponent();
    editableContactBindingSource.DataSource = contact;
    contact.BeginEdit();
}

public EditableContact Contact
{
    get { return editableContactBindingSource.DataSource as EditableContact; }
}

private void OKButton_Click(object sender, EventArgs e)
{
    this.Contact.EndEdit();
    this.DialogResult = DialogResult.OK;
}

private void ApplyButton_Click(object sender, EventArgs e)
{
    this.Contact.EndEdit();
    this.Contact.BeginEdit();
}

private void ResetButton_Click(object sender, EventArgs e)
{
    this.Contact.CancelEdit();
}

private void CancelButton_Click(object sender, EventArgs e)
{
    this.Contact.CancelEdit();
    this.DialogResult = DialogResult.Cancel;
}

And you're done. IEditableObject implementations on any class with minimal code :)

Download the sample below and have a look. Notice that:

  • The changes don't appear in the DataGridView until we click Apply.
  • If we edit something the Apply and Reset buttons become enabled, however if we then change it back to what it was, the buttons disable again.
  • If we Cancel or Reset, the changes aren't applied.

Download

Summary

Custom type descriptors are a great way to leverage code. You can build the behavior into a couple of smart adapters or type descriptors/converters, and minimize the code you need to write over and over again. As I mentioned, the above code would work in WPF, and you wouldn't even need to code the wrapper class. Type descriptors can be a little tricky though, so give yourself some time. It's well worth the investment.

CustomTypeDescriptors aren't a solution to every problem, however. They provide a bit of a barrier between the objects and the UI, but they aren't a complete layer of insulation in the way an adapter layer would be. Use at your own risk!

Contact me

Here is an hCard with my contact information. Browser add-ins, such as Operator for Firefox or Oomph for Internet Explorer, should recognise this card and allow you to save or bookmark my contact details. Alternatively, click here for an Outlook vCard.

Paul Stovell

I am in the process of updating the WPF and XAML guidelines to bring them up to date with the latest technology changes.

In the mean time, the old page has been uploaded to the WPF Disciples site.