IEditableObject Adapter for WPF and Windows Forms

(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!

A picture of me

Welcome, my name is Paul Stovell. I live in Brisbane and work on Octopus Deploy 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.