Introduction
I recently found myself in need of a way to enable transactional edits within a WPF DataGrid
control for a very large project at work.
I wanted to abstract-out the concept of rolling-back changes so that I wouldn't have to rewrite the same logic every where (I like to keep
my code DRY). Unfortunately, the only result that turned up on Google was
a dead link to one of Paul Stovell's blog posts (bummer).
After giving up on ever finding a solution on the web, I decided that I'd make my own. Feeling pretty happy with the results thereafter,
I decided it wouldn't hurt to give back to the community.
Background
DataGrid
s make transactional edits possible through the use
of the IEditableObject
interface.
Any object that implements this interface can have its changes rollback through the BeginEdit
, CancelEdit
, and EndEdit
methods.
This article will explore the idea of implementing the IEditableObject
in a wrapper for data-bound objects.
Implementation
First, we need an awesome name for our equally awesome wrapper - let's call it EditableAdapter
. We already know that EditableAdapter
will need to implement IEditableObject
,
but we still have some other things to ponder before we can start coding:
- How will we keep a snapshot of the object's state?
- How will our wrapper expose the same properties as the underlying object?
To address the first bullet, we will use a variation of the Memento pattern (we'll use Reflection to capture and restore state).
The simplest solution to the second problem is to use the ICustomTypeDescriptor
interface. By implementing ICustomTypeDescriptor
,
we will be able to expose the same PropertyDescriptor
s
as the wrapped object. If this all sounds crazy, just bear with me - I'll explain all of this shortly.
Now then, let's see the code!
Memento (it's more than just an awesome movie)
We need a way to dynamically save and restore the state of another object. Fortunately for us, the .NET Framework supports this through Reflection.
What we will do is create a Memento
class that gets all of the properties' metadata for the wrapped object (within the context of the Memento
class,
let's call it the originator). The class will look something like this:
class Memento<T>
{
Dictionary<PropertyInfo, object> storedProperties =
new Dictionary<PropertyInfo, object>();
public Memento(T originator)
{
var propertyInfos =
typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Where(p => p.CanRead && p.CanWrite);
foreach (var property in propertyInfos)
{
this.storedProperties[property] = property.GetValue(originator, null);
}
}
public void Restore(T originator)
{
foreach (var pair in this.storedProperties)
{
pair.Key.SetValue(originator, pair.Value, null);
}
}
}
This class simply creates a backup of all the public, readable, and writable properties of the originator
. Oh, and one more thing - it's strongly typed :).
Note: We could bypass the need for Reflection by requiring that all objects wrapped by the EditableAdapter
implement a common interface.
The common interface would contain a method that takes in state and restores the object, and another method that outputs the state of the object.
While that would be more inline with the original Memento pattern, it doesn't afford us the flexibility of using Reflection.
Exposing the same PropertyDescriptors
Instead of using Reflection directly, WPF and Windows Forms enumerate data-bound objects' properties through an intermediary
class - the TypeDescriptor
class.
What we want to do is hijack that system so that we can make the EditableAdapter
appear to expose the same properties.
Hmm... what could make that work? Voodoo, black magic - sorcery, perhaps?
Nope! Just another interface to implement - ICustomTypeDescriptor
.
This interface defines a method called GetProperties
, which is where we will return
PropertyDescriptor
s
that mimic the properties of the object we want to wrap. Let's consider how we will create the
PropertyDescriptor
s
before we get too wrapped-up with the ICustomTypeDescriptor
.
Creating a custom PropertyDescriptor
can be a little tricky, but I have a few tricks up my sleeve that will make it easier. There's an awesome abstract class nested inside of the
TypeConverter
class - the aptly
named SimplePropertyDescriptor
class.
Why is it marked protected
? I have no idea...
Anywho, we want to create instances of TypeConverter.SimplePropertyDescriptor
dynamically for each of the target object's properties. The "dynamic" aspect of this can be easily handled using delegates - combine that with
a PropertyDescriptor
factory,
and you'll be ready for anything. Ninjas, pirates, aliens - you name it.
All joking aside, this is going to be pretty slick. Let's start fleshing this out:
internal class InternalPropertyDescriptorFactory : TypeConverter
{
protected class GenericPropertyDescriptor :
TypeConverter.SimplePropertyDescriptor
{
Func<object, object> getter;
Action<object, object> setter;
public GenericPropertyDescriptor(string name, Type componentType,
Type propertyType, Func<object, object> getter,
Action<object, object> setter)
: base(componentType, name, propertyType)
{
if (getter == null)
{
throw new ArgumentNullException("getter");
}
if (setter == null)
{
throw new ArgumentNullException("setter");
}
this.getter = getter;
this.setter = setter;
}
public GenericPropertyDescriptor(string name, Type componentType,
Type propertyType, Func<object, object> getter)
: base(componentType, name, propertyType)
{
if (getter == null)
{
throw new ArgumentNullException("getter");
}
this.getter = getter;
}
public override bool IsReadOnly
{
get
{
return this.setter == null;
}
}
public override object GetValue(object target)
{
object value = this.getter(target);
return value;
}
public override void SetValue(object target, object value)
{
if (!this.IsReadOnly)
{
object newValue = (object)value;
this.setter(target, newValue);
}
}
}
}
Whew! A quick Q&A is in order, and then we'll move on to the rest of the code involved in this factory.
Q. Why make InternalPropertyDescriptorFactory internal?
A. Because I want to make the public interface all static - I can't do that and
derive from TypeConverter.SimplePropertyDescriptor
.
We'll make a public static class shortly, and we'll call it PropertyDescriptorFactory
.
Q. What are all of those Actions and Funcs for again?
A. We will pass in the functionality for the getting and setting when we create instances of the GenericPropertyDescriptor
.
Q. In cases where we know the type at compile time, wouldn't it make sense to utilize Generics?
A. Definitely! That code is available in the next code listing.
Alright, let's see all of it!
internal class InternalPropertyDescriptorFactory : TypeConverter
{
public static PropertyDescriptor CreatePropertyDescriptor<TComponent,
TProperty>(string name, Func<TComponent, TProperty> getter,
Action<TComponent, TProperty> setter)
{
return new GenericPropertyDescriptor<TComponent,
TProperty>(name, getter, setter);
}
public static PropertyDescriptor CreatePropertyDescriptor<TComponent,
TProperty>(string name, Func<TComponent, TProperty> getter)
{
return new GenericPropertyDescriptor<TComponent,
TProperty>(name, getter);
}
public static PropertyDescriptor CreatePropertyDescriptor(string name,
Type componentType, Type propertyType, Func<object, object> getter,
Action<object, object> setter)
{
return new GenericPropertyDescriptor(name, componentType,
propertyType, getter, setter);
}
public static PropertyDescriptor CreatePropertyDescriptor(string name,
Type componentType, Type propertyType, Func<object, object> getter)
{
return new GenericPropertyDescriptor(name, componentType,
propertyType, getter);
}
protected class GenericPropertyDescriptor<TComponent, TProperty> :
TypeConverter.SimplePropertyDescriptor
{
Func<TComponent, TProperty> getter;
Action<TComponent, TProperty> setter;
public GenericPropertyDescriptor(string name, Func<TComponent,
TProperty> getter, Action<TComponent, TProperty> setter)
: base(typeof(TComponent), name, typeof(TProperty))
{
if (getter == null)
{
throw new ArgumentNullException("getter");
}
if (setter == null)
{
throw new ArgumentNullException("setter");
}
this.getter = getter;
this.setter = setter;
}
public GenericPropertyDescriptor(string name,
Func<TComponent, TProperty> getter)
: base(typeof(TComponent), name, typeof(TProperty))
{
if (getter == null)
{
throw new ArgumentNullException("getter");
}
this.getter = getter;
}
public override bool IsReadOnly
{
get
{
return this.setter == null;
}
}
public override object GetValue(object target)
{
TComponent component = (TComponent)target;
TProperty value = this.getter(component);
return value;
}
public override void SetValue(object target, object value)
{
if (!this.IsReadOnly)
{
TComponent component = (TComponent)target;
TProperty newValue = (TProperty)value;
this.setter(component, newValue);
}
}
}
protected class GenericPropertyDescriptor :
TypeConverter.SimplePropertyDescriptor
{
Func<object, object> getter;
Action<object, object> setter;
public GenericPropertyDescriptor(string name, Type componentType,
Type propertyType, Func<object, object> getter,
Action<object, object> setter)
: base(componentType, name, propertyType)
{
if (getter == null)
{
throw new ArgumentNullException("getter");
}
if (setter == null)
{
throw new ArgumentNullException("setter");
}
this.getter = getter;
this.setter = setter;
}
public GenericPropertyDescriptor(string name, Type componentType,
Type propertyType, Func<object, object> getter)
: base(componentType, name, propertyType)
{
if (getter == null)
{
throw new ArgumentNullException("getter");
}
this.getter = getter;
}
public override bool IsReadOnly
{
get
{
return this.setter == null;
}
}
public override object GetValue(object target)
{
object value = this.getter(target);
return value;
}
public override void SetValue(object target, object value)
{
if (!this.IsReadOnly)
{
object newValue = (object)value;
this.setter(target, newValue);
}
}
}
}
public static class PropertyDescriptorFactory
{
public static PropertyDescriptor CreatePropertyDescriptor<TComponent,
TProperty>(string name, Func<TComponent, TProperty> getter,
Action<TComponent, TProperty> setter)
{
return InternalPropertyDescriptorFactory.CreatePropertyDescriptor<TComponent,
TProperty>(name, getter, setter);
}
public static PropertyDescriptor CreatePropertyDescriptor<TComponent,
TProperty>(string name, Func<TComponent, TProperty> getter)
{
return InternalPropertyDescriptorFactory.CreatePropertyDescriptor<TComponent,
TProperty>(name, getter);
}
public static PropertyDescriptor CreatePropertyDescriptor(string name,
Type componentType, Type propertyType, Func<object,
object> getter, Action<object, object> setter)
{
return InternalPropertyDescriptorFactory.CreatePropertyDescriptor(name,
componentType, propertyType, getter, setter);
}
public static PropertyDescriptor CreatePropertyDescriptor(string name,
Type componentType, Type propertyType, Func<object, object> getter)
{
return InternalPropertyDescriptorFactory.CreatePropertyDescriptor(name,
componentType, propertyType, getter);
}
}
Alright, that wraps up how we will create the
PropertyDescriptor
- now, we can
put it all together in the EditableAdapter
class.
EditableObject
This is where the magic happens. We will backup state with the Memento
, create PropertyDescriptor
s with our PropertyDescriptorFactory
,
and then we will make the PropertyDescriptor
s accessible through
TypeDescriptor
by implementing ICustomTypeDescriptor
.
public class EditableAdapter<T> : IEditableObject,
ICustomTypeDescriptor, INotifyPropertyChanged
{
public T Target { get; set; }
Memento<T> memento;
public EditableAdapter(T target)
{
this.Target = target;
}
#region IEditableObject Members
public void BeginEdit()
{
if (this.memento == null)
{
this.memento = new Memento<T>(this.Target);
}
}
public void CancelEdit()
{
if (this.memento != null)
{
this.memento.Restore(this.Target);
this.memento = null;
}
}
public void EndEdit()
{
this.memento = null;
}
#endregion
#region ICustomTypeDescriptor Members
PropertyDescriptorCollection ICustomTypeDescriptor.GetProperties()
{
IList<PropertyDescriptor> propertyDescriptors =
new List<PropertyDescriptor>();
var readonlyPropertyInfos =
typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Where(p => p.CanRead && !p.CanWrite);
var writablePropertyInfos =
typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Where(p => p.CanRead && p.CanWrite);
foreach (var property in readonlyPropertyInfos)
{
var propertyCopy = property;
var propertyDescriptor = PropertyDescriptorFactory.CreatePropertyDescriptor(
property.Name,
typeof(T),
property.PropertyType,
(component) => propertyCopy.GetValue(
((EditableAdapter<T>)component).Target, null));
propertyDescriptors.Add(propertyDescriptor);
}
foreach (var property in writablePropertyInfos)
{
var propertyCopy = property;
var propertyDescriptor = PropertyDescriptorFactory.CreatePropertyDescriptor(
property.Name,
typeof(T),
property.PropertyType,
(component) => propertyCopy.GetValue(
((EditableAdapter<T>)component).Target, null),
(component, value) => propertyCopy.SetValue(
((EditableAdapter<T>)component).Target, value, null));
propertyDescriptors.Add(propertyDescriptor);
}
return new PropertyDescriptorCollection(propertyDescriptors.ToArray());
}
AttributeCollection ICustomTypeDescriptor.GetAttributes()
{
throw new NotImplementedException();
}
string ICustomTypeDescriptor.GetClassName()
{
throw new NotImplementedException();
}
string ICustomTypeDescriptor.GetComponentName()
{
throw new NotImplementedException();
}
TypeConverter ICustomTypeDescriptor.GetConverter()
{
throw new NotImplementedException();
}
EventDescriptor ICustomTypeDescriptor.GetDefaultEvent()
{
throw new NotImplementedException();
}
PropertyDescriptor ICustomTypeDescriptor.GetDefaultProperty()
{
throw new NotImplementedException();
}
object ICustomTypeDescriptor.GetEditor(Type editorBaseType)
{
throw new NotImplementedException();
}
EventDescriptorCollection ICustomTypeDescriptor.GetEvents(Attribute[] attributes)
{
throw new NotImplementedException();
}
EventDescriptorCollection ICustomTypeDescriptor.GetEvents()
{
throw new NotImplementedException();
}
PropertyDescriptorCollection
ICustomTypeDescriptor.GetProperties(Attribute[] attributes)
{
throw new NotImplementedException();
}
object ICustomTypeDescriptor.GetPropertyOwner(PropertyDescriptor pd)
{
throw new NotImplementedException();
}
#endregion
private void NotifyPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (this.PropertyChanged != null)
{
this.PropertyChanged(this, e);
}
}
#region INotifyPropertyChanged Members
private event PropertyChangedEventHandler PropertyChanged;
event PropertyChangedEventHandler INotifyPropertyChanged.PropertyChanged
{
add
{
if (this.Target is INotifyPropertyChanged)
{
this.PropertyChanged += value;
((INotifyPropertyChanged)this.Target).PropertyChanged +=
this.NotifyPropertyChanged;
}
}
remove
{
if (this.Target is INotifyPropertyChanged)
{
this.PropertyChanged -= value;
((INotifyPropertyChanged)this.Target).PropertyChanged -=
this.NotifyPropertyChanged;
}
}
}
#endregion
}
Using the Code
Using the code is as simple as:
SomeObject obj = new SomeObject();
var editable = new EditableAdapter<SomeObject>();
editable.BeginEdit();
editable.CancelEdit();
Points of interest
Hmm... all of it seems pretty interesting to me. It's amazing what you can do with a dash of abstraction.
One thing that bit me involved C#'s implementation of closures when combined with its implementation of foreach
loops.
You must create a local reference to the iterated value when creating an anonymous delegate in a foreach
loop; otherwise,
all of the delegates will reference the last item in the sequence. I've known this for a while, but it's easy to overlook.
History
- 06/23/09 - First started writing this :)