Click here to Skip to main content
Click here to Skip to main content

Generic implementation of IEditableObject via TypeDescriptor and Reflection

By , 24 Jun 2009
 

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:

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:

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

/// <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.

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:

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

About the Author

Charles Strahan
Software Developer
United States United States
Member
Ninja coder extraordinaire.

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
QuestionBindingSource and IEditableObjectmemberSimon 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 5memberJoakim O'Nils30 Nov '10 - 15:20 
5 Super good
General.NET 4.0 [modified]memberMember 285066622 Nov '10 - 22:38 
Does this work if the target framework is .NET 4.0, with the stock DataGrid (non-toolkit)?

modified on Tuesday, November 23, 2010 5:15 AM

GeneralMy vote of 5memberSantiago Santos Cortizo28 Jul '10 - 22:06 
Very useful to make LINQ to SQL objects work with CancelEdit(). Thanks.
GeneralImplement IChangeTrackingmemberKazna4ey17 Apr '10 - 11:39 
Hi,
I haven't read the whole article, but it looks like you have done a nice job!
 
1 tip though: I would implement IChangeTracking interface - this way it will be possible to update in the database only those objects that were actually changed.
GeneralRe: Implement IChangeTrackingmemberjoe blowhead18 Nov '10 - 8:31 
Do know of any sample source code and WPF project that i can download on the internet that implements the IChangeTracking interface. I would like to see how to code for it.
You can email me directly at
steve_44@inbox.com
thanks
GeneralReflection for propertiesmemberPaul B.30 Jun '09 - 6:55 
I think using reflection creates more problems than it solves here. On something like this I would specify a generic interface such as ICopyable and then let each implementation of the object how it wanted to handle saving a copy. Then you could use reflection if you wanted to or something else for all the people that want fields, etc. but I'd probably just go with a this.MemberwiseClone() as T
GeneralRe: Reflection for propertiesmemberCharles Strahan1 Jul '09 - 16:28 
I agree that reflection can be more trouble than it's worth. In my particular case, I wanted to easily add this functionality to many, many types, and I wanted the least amount of friction (IOW, I'm lazy pragmatic). The best design would be to require an interface as you suggest. For my project, I knew that all of the objects that I needed to wrap we're pretty dumb - just a bunch of properties that could be easily fetched via reflection - I figured that reflection was a more pragmatic approach for my purposes.
 
I do, however, see one potential pitfall with cloning the object as you suggest - your code might reference the original object. I suppose that wouldn't be an issue if the rest of the code always gets the current Target, but that sounds prone to accidents. What are your thoughts on this?
 
Cheers,
-Charles
Questiondeep copy?memberlalalalalalaalalala27 Jun '09 - 5:36 
I'm just looking at the code for the first time now, and I'm curious how it handles collections of data. It seems that it does a shallow copy and I'm thinking that there should be a way to do deep copies. Maybe through another attribute and/or a parameter on the constructor.
AnswerRe: deep copy?memberCharles Strahan27 Jun '09 - 10:05 
I think I see what you're saying here. Say for example, we wrap a target object with the EditableAdapter, and the target itself has some properties that are objects consisting of other properties. With the current code, we only backup references of the target's members, and we don't go any further down the tree. A data binding could route through all of the references, causing those data changes to not roll back on CancelEdit().
 
I think the best solution to this problem (or any related problems) is to remove the memento creation from the EditableAdapter itself. Instead, we should encapsulate all of that logic in another factory, which could be passed in during the construction of the EditableAdapter.
 
This solution came to mind shortly chatting with Steve Hansen regarding Properties vs. Fields... Perhaps we could use two generic interfaces - IMemento<T> and IMementoFactory<T>.
 
What are your thoughts on this? I'd like to make this project as useful as possible - while it won't fit all situations, I think it could be handy in a lot of cases.
 
Thanks for your feedback and interest,
-Charles

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

Permalink | Advertise | Privacy | Mobile
Web03 | 2.6.130516.1 | Last Updated 24 Jun 2009
Article Copyright 2009 by Charles Strahan
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid