Click here to Skip to main content
15,867,686 members
Articles / Programming Languages / C#

Generic implementation of IEditableObject via TypeDescriptor and Reflection

Rate me:
Please Sign up or sign in to vote.
4.75/5 (18 votes)
24 Jun 2009Public Domain5 min read 82.4K   941   48   21
A demonstration of how to create an IEditableObject wrapper for any object, and a detailed look at the concepts and patterns used.

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

DataGrids 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 PropertyDescriptors 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:

C#
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 PropertyDescriptors that mimic the properties of the object we want to wrap. Let's consider how we will create the PropertyDescriptors 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:

C#
/// <summary>
/// Provides internal methods for creating property descriptors.
/// This class should not be used directly.
/// </summary>
internal class InternalPropertyDescriptorFactory : TypeConverter
{
    
    // ... public static methods for creating instances here ...
    
    
    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!

C#
/// <summary>
/// Provides internal methods for creating property descriptors.
/// This class should not be used directly.
/// </summary>
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);
            }
        }
    }
}
    
    
/// <summary>
/// Provides methods for easily creating property descriptors.
/// </summary>
public static class PropertyDescriptorFactory
{
    /// <summary>
    /// Creates a custom property descriptor.
    /// </summary>
    /// <typeparam name="TComponent">The component type.</typeparam>
    /// <typeparam name="TProperty">The parameter type.</typeparam>
    /// <param name="name">The name of the property.</param>
    /// <param name="getter">A function that takes
    /// a component and gets this property's value.</param>
    /// <param name="setter">An action that takes
    /// a component and sets this property's value.</param>
    /// <returns>A customer property descriptor.</returns>
    public static PropertyDescriptor CreatePropertyDescriptor<TComponent, 
           TProperty>(string name, Func<TComponent, TProperty> getter, 
           Action<TComponent, TProperty> setter)
    {
        return InternalPropertyDescriptorFactory.CreatePropertyDescriptor<TComponent, 
               TProperty>(name, getter, setter);
    }

    /// <summary>
    /// Creates a custom read-only property descriptor.
    /// </summary>
    /// <typeparam name="TComponent">The component type.</typeparam>
    /// <typeparam name="TProperty">The parameter type.</typeparam>
    /// <param name="name">The name of the read-only property.</param>
    /// <param name="getter">A function that takes
    /// a component and gets this property's value.</param>
    /// <returns>A customer property descriptor.</returns>
    public static PropertyDescriptor CreatePropertyDescriptor<TComponent, 
           TProperty>(string name, Func<TComponent, TProperty> getter)
    {
        return InternalPropertyDescriptorFactory.CreatePropertyDescriptor<TComponent, 
                                  TProperty>(name, getter);
    }

    /// <summary>
    /// Creates a custom property descriptor.
    /// </summary>
    /// <param name="name">The name of the property.</param>
    /// <param name="componentType">A System.Type that represents
    /// the type of component to which this property descriptor binds.</param>
    /// <param name="propertyType">A System.Type that
    ///       represents the data type for this property.</param>
    /// <param name="getter">A function that takes
    ///       a component and gets this property's value.</param>
    /// <param name="setter">An action that takes
    ///       a component and sets this property's value.</param>
    /// <returns>A customer property descriptor.</returns>
    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);
    }

    /// <summary>
    /// Creates a custom read-only property descriptor.
    /// </summary>
    /// <param name="name">The name of the read-only property.</param>
    /// <param name="componentType">A System.Type that represents
    ///           the type of component to which this property descriptor binds.</param>
    /// <param name="propertyType">A System.Type
    ///           that represents the data type for this property.</param>
    /// <param name="getter">A function that takes
    ///           a component and gets this property's value.</param>
    /// <returns>A customer property descriptor.</returns>
    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 PropertyDescriptors with our PropertyDescriptorFactory, and then we will make the PropertyDescriptors accessible through TypeDescriptor by implementing ICustomTypeDescriptor.

C#
public class EditableAdapter<T> : IEditableObject, 
             ICustomTypeDescriptor, INotifyPropertyChanged
{
    /// <summary>
    /// The wrapped object.
    /// </summary>
    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;
            // Need this copy of property for use in the closure

            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;
            // Need this copy of property for use in the closure

            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:

C#
SomeObject obj = new SomeObject();
var editable = new EditableAdapter<SomeObject>();
editable.BeginEdit();

// ... change editable's properties ...

editable.CancelEdit(); // or editable.EndEdit();

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

License

This article, along with any associated source code and files, is licensed under A Public Domain dedication


Written By
Software Developer
United States United States
Ninja coder extraordinaire.

Comments and Discussions

 
QuestionBindingSource and IEditableObject Pin
Simon Bridge10-Feb-13 11:54
Simon Bridge10-Feb-13 11:54 
Great article, thanks for sharing.

Have you noticed that when BindingSource uses an entity that implements IEditableObject, it doesn't always call EndEdit or CancelEdit for each record it called BeginEdit on? This is especially true when you close the form.... maybe I'm not gracefully ending something... but what?
GeneralMy vote of 5 Pin
Joakim O'Nils30-Nov-10 15:20
Joakim O'Nils30-Nov-10 15:20 
General.NET 4.0 [modified] Pin
Member 285066622-Nov-10 22:38
Member 285066622-Nov-10 22:38 
GeneralMy vote of 5 Pin
Santiago Santos Cortizo28-Jul-10 22:06
professionalSantiago Santos Cortizo28-Jul-10 22:06 
GeneralImplement IChangeTracking Pin
Kazna4ey17-Apr-10 11:39
Kazna4ey17-Apr-10 11:39 
GeneralRe: Implement IChangeTracking Pin
joe blowhead18-Nov-10 8:31
joe blowhead18-Nov-10 8:31 
GeneralReflection for properties Pin
Paul B.30-Jun-09 6:55
Paul B.30-Jun-09 6:55 
GeneralRe: Reflection for properties Pin
Charles Strahan1-Jul-09 16:28
Charles Strahan1-Jul-09 16:28 
Questiondeep copy? Pin
lalalalalalaalalala27-Jun-09 5:36
lalalalalalaalalala27-Jun-09 5:36 
AnswerRe: deep copy? Pin
Charles Strahan27-Jun-09 10:05
Charles Strahan27-Jun-09 10:05 
GeneralRe: deep copy? Pin
Member 380236030-Jun-09 8:34
Member 380236030-Jun-09 8:34 
GeneralRe: deep copy? Pin
Bishoy Demian30-Jun-09 9:52
Bishoy Demian30-Jun-09 9:52 
GeneralRe: deep copy? Pin
Charles Strahan1-Jul-09 16:46
Charles Strahan1-Jul-09 16:46 
GeneralProperties vs Fields Pin
Steve Hansen25-Jun-09 7:35
Steve Hansen25-Jun-09 7:35 
GeneralRe: Properties vs Fields Pin
Charles Strahan25-Jun-09 19:22
Charles Strahan25-Jun-09 19:22 
GeneralRe: Properties vs Fields Pin
lalalalalalaalalala27-Jun-09 5:34
lalalalalalaalalala27-Jun-09 5:34 
GeneralTip Pin
tonyt25-Jun-09 0:04
tonyt25-Jun-09 0:04 
GeneralRe: Tip Pin
Charles Strahan25-Jun-09 19:17
Charles Strahan25-Jun-09 19:17 
GeneralRe: Tip Pin
Steve Hansen25-Jun-09 21:50
Steve Hansen25-Jun-09 21:50 
GeneralRe: Tip Pin
Charles Strahan26-Jun-09 18:20
Charles Strahan26-Jun-09 18:20 
GeneralRe: Tip Pin
Steve Hansen26-Jun-09 21:56
Steve Hansen26-Jun-09 21:56 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.