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

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:
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
DataGridViewuntil 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.
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!
Discussion
Martin Nyborg
Very nice - can this be done in SL3?
Michael Tayloe
I was interested in your approach but I very quickly ran into a roadblock. The problem is that IEO doesn't play well with validation of any sort. Both IEO and IDataErrorInfo were originally designed for datasets and the interfaces are designed as such. Why these 2 interfaces have become popular for business objects is beyond me because they are both grossly inadequate. Nevertheless they are commonly used in both WinForms and WPF nowadays so I figure I should at least give them a shot. That's when the issues start to creep in.
IEO makes the assumption that it is perfectly valid for an object to be in an invalid state. It assumes that there is some later step to actually commit the changes and do final validation. But the EO class you defined will replace the real object with the (potentially bad) data when EndEdit is called. We could extend EO to implement IDEI but then we'd have to add an implementation for each property that needs to do validation (which defeats the purpose of the class you wrote). It would also mean we'd have to replicate the validation logic in two places (the original business object and the EO-derived type) which is never good.
Taking the demo you gave as an example I tried to implement validation via IDEI and it just won't work correctly. While I can get the edit dialog in the form to validate it only does so if I replicate validation code and if I augument the EO class to not call EndEdit when there are errors. For a dialog that is OK but for controls that are IEO aware it won't work because they'll just call EndEdit.
Do you have any suggestions on how to integrate IEO and IDEI w/o replication of validation code and w/o all the extra work involved? I've yet to see any real-world example from MS or others that combine the two correctly. They always solve one side of the equation or the other but never both.
xxx
Interesting approach - didn't even know that you can play with type such way. BTW, there is bug in your code: after cancel edit wrapped type contains changed value. Am I misused something? static void Main(string[] args) { var role = new Roles {Id = 5, Name = "Vardas"}; var wrappedRole = new EditableAdapter(role); wrappedRole.PropertyChanged += role_PropertyChanged; Console.WriteLine("Start..."); wrappedRole.BeginEdit(); Console.WriteLine("Setting new value..."); wrappedRole.WrappedInstance.Name = "Vardas2"; wrappedRole.EndEdit(); Console.WriteLine("Committed changes..."); Console.WriteLine("New value: {0}", wrappedRole.WrappedInstance.Name); Console.WriteLine("New value at original object: {0}", role.Name);
Paul, just a note to add one little fix (found because my adapted object implement IDataErrorInfo) is to ignore indexed properties...
On lines 31 and 44 of TypeMetaDataRepository add additional condition of: !property.GetIndexParameters().Any()
Hope that helps someone!
Andrew Weaver
Hi Paul,
I would like to follow up with Michael Tayloe's post regarding IDataErrorInfo being mimicked from the adapted business object. Would you have any thoughts on how this might be achieved given that IDataErrorInfo is typically based on a validation of the current (rather than pending) state of the object???
A suggestion I was given was that instead of applying updates on EndEdit and cancel just resets pending changes, you instead apply updates as they occur and cancel undoes changes made. Then the IDataErrorInfo implementation on the adapted object can simply be mimicked.
Thoughts?
Andrew.
Dison
Excellent. I have tried this to create a sample project, but there seems to be one improvement: The sample project has the UI, but all the validations are done in the entity: partial void OnValidate(System.Data.Linq.ChangeAction action) { }
So you can see that, if the editableEntity.EndEdit() is not called, the values will not be saved to the entity, so the validation will still use the current values; but if the EndEdit() is called, once it cannot pass the validation, if the user tries to use CancelEdit(), but the values will not change back to its original state.
Could you please put more efforts to make it support multi times undo, or to support the IDataErrorInfo as Andrew suggested? Thank you in advance.