Thursday, April 22, 2010

Transactions with MVVM

One objection to MVVM I often hear is that it doesn't manage transactions well. I'm not talking about database transactions or "true atomic" transactions, but those short-lived transactions that happen in the UI. With data-binding, updates happen immediately. If I pass a validation, the field is updated.

Many applications, however, don't work this way. While edits and validations exist for the fields, you may often have business logic that has dependencies on multiple entries in the form. If these "composite" validations fail, you want to rollback to the original value. How do we do this with MVVM?

Again, the problem isn't with MVVM, but rather with the implementation. One common approach I've seen is to simply keep a copy of the old data, then swap it out as needed. This works well, but when you have large data models or even a list of items, can get a little ugly. So what can we do?

First, let's build a few basic base classes that most MVVM solutions contain. We want to handle INotifyPropertyChanged, so we'll borrow the elegant code I learned from Rob Eisenberg in his Build your Own MVVM Framework presentation at Mix 2010. I'll also add to that some automatic "dirty" checks (i.e. sets the dirty flag when a property is modified).

public abstract class BaseModel : INotifyPropertyChanged 
{
    public const string IS_DIRTY = "IsDirty";
    public const string IGNORE_DIRTY = "IgnoreDirty"; 

    private bool _isDirty = false; 

    protected BaseModel()
    {
        PropertyChanged += BaseModel_PropertyChanged;
    }

    void BaseModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        if (!_isDirty && !e.PropertyName.Equals(IS_DIRTY))
        {
            IsDirty = true; 
        }
    }

    public bool IsDirty
    {
        get { return _isDirty; }
        set
        {
            if (!_isDirty.Equals(value))
            {
                _isDirty = value;
                RaisePropertyChanged(() => IsDirty); 
            }
        }
    }

    public void NotifyOfPropertyChange(string propertyName)
    {
        var handler = PropertyChanged;

        if (handler != null)
        {
            handler(this, new PropertyChangedEventArgs(propertyName)));
        }
    }

    public void RaisePropertyChanged<TProperty>(Expression<Func<TProperty>> property)
    {
        if (PropertyChanged == null)
        {
            return; 
        }

        var lambda = (LambdaExpression)property;

        MemberExpression memberExpression;

        if (lambda.Body is UnaryExpression)
        {
            var unaryExpression = (UnaryExpression)lambda.Body;
            memberExpression = (MemberExpression)unaryExpression.Operand;
        }
        else memberExpression = (MemberExpression)lambda.Body;

        NotifyOfPropertyChange(memberExpression.Member.Name);
    }

    public event PropertyChangedEventHandler PropertyChanged;
}

Anything that inherits from this has strongly typed property changed events. Furthermore, whenever a property is assigned, it automatically sets the dirty flag for you.

Next, we don't want to do the work of making a deep copy every time we need to save something off, do we? I didn't think so. Let's use an extension method to make it easy to copy anything that derives from the base model. We're going to assume that this contains really just basic value types or simple objects, or other objects that inherit from base model that we can recursively copy:

public static class ModelExtensions
{
    public static T Copy<T>(this T source) where T : BaseModel 
    {
        var clone = Activator.CreateInstance(source.GetType()) as BaseModel;
        return (T)source.Copy(clone);           
    }

    public static T Copy<T>(this T source, T clone) where T : BaseModel
    {
        const string ITEM = "Item";
        const string COUNT = "Count";
        const string ADD = "Add";
            
       
            foreach (PropertyInfo curPropInfo in source.GetType().GetProperties())
            {
                if (curPropInfo.GetGetMethod() != null
                    && (curPropInfo.GetSetMethod() != null))
                {
                    if (!curPropInfo.Name.Equals(ITEM))
                    {
                        object getValue = curPropInfo.GetGetMethod()
                            .Invoke(source, new object[] { });

                        if (getValue != null && getValue is BaseModel)
                        {
                            var baseModel = getValue as BaseModel;
                            getValue = baseModel.Copy();
                        }

                        curPropInfo.GetSetMethod().Invoke(clone, new[] { getValue });                                   
                    }
                    else
                    {
                        var numberofItemInColleciton =
                            (int)
                            curPropInfo.ReflectedType.GetProperty(COUNT)
                                .GetGetMethod().Invoke(source, new object[] { });

                        for (int i = 0; i < numberofItemInColleciton; i++)
                        {
                            object getValue = curPropInfo.GetGetMethod().Invoke(source, new object[] { i });

                            if (getValue != null && getValue is BaseModel)
                                getValue = ((BaseModel)getValue).Copy();

                            curPropInfo.ReflectedType.GetMethod(ADD).Invoke(clone,
                                                                                    new[] { getValue });                                
                        }
                    }
                }
            }        

        return (T)clone;
    }
}    

Depending on how you handle the model, you may want to turn off the dirty flag after the clone operation.

OK, so let's get to the transactions. First, we'll create a simple class that wraps any model in a transaction. What we do is bind to the Value property, and then through our commands or other mechanisms, call Commit and Rollback as needed to apply or reverse changes.

public class TransactionModel<T> where T: BaseModel 
{
    private T _src;
    private T _editable;

    public TransactionModel(T src)
    {
        _editable = src;
        _src = _editable.Copy();
    }

    public T Value
    {
        get { return _editable; }
        set
        {
            _editable = value;
            _src = _editable.Copy();
        }
    }

    public void Commit()
    {
        _editable.Copy(_src);
    }

    public void Rollback()
    {
        _src.Copy(_editable); 
    }

    public override bool Equals(object obj)
    {
        return obj is TransactionModel<T> && ((TransactionModel<T>)obj).Value.Equals(Value);
    }

    public override int GetHashCode()
    {
        return Value.GetHashCode();
    }
}

Note how we take in the initial property and copy it. The commit and rollbacks do not create new objects but transfer properties to and from as needed. If a new value is assigned, we automatically generate the copy and are ready to continue with the transaction.

With this scenario, you can simply wrap your model in a TransactionMode and bind to the Value property. All other operations can reference the original object, it will simply update as needed based on the commits and/or rollbacks.

While this is quite useful, what if you have a large list of items and want to either commit or roll the entire list back? No problem!

public class TransactionCollection<T> where T: BaseModel 
{
    private List<TransactionModel<T>> _transactions; 

    public TransactionCollection(IEnumerable<T> source)
    {
        Collection = new ObservableCollection<T>(source);
        Collection.CollectionChanged += Collection_CollectionChanged;

        _transactions = new List<TransactionModel<T>>(Collection.Count);

        foreach(var t in source)
        {
            _transactions.Add(new TransactionModel<T>(t))
        }
    }

    void Collection_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        if (e.OldItems != null)
        {
            foreach (var item in e.OldItems)
            {
                var transaction = (from t in _transactions where t.Value.Equals(item) select t).FirstOrDefault();
                if (transaction != null)
                {
                    _transactions.Remove(transaction);
                }
            }
        }

        if (e.NewItems != null)
        {
            foreach (var item in e.NewItems)
            {
                _transactions.Add(new TransactionModel<T>((T)item));
            }
        }
    }

    public ObservableCollection<T> Collection { get; private set; }

    public void Commit()
    {
        foreach(var t in _transactions)
        {
           t.Commit(); 
        }
    }

    public void Rollback()
    {
        foreach(var t in _transactions)
        {
           t.Rollback(); 
        }
    }
}

With this helper class, you can bind to the collection with a data grid, list box, or any other control and all that control "sees" is a collection of your objects. However, the wrapper "listens" to items being added/removed and automatically wraps them in a transaction, so you can then commit or rollback the entire list.

As you can see, requiring the ability to rollback is not only fine with MVVM, but also rather easy when you have the right helper classes in place.

Jeremy Likness

14 comments:

  1. Brilliant and very helpful. Thanks for sharing.

    ReplyDelete
  2. The problem starts when you use Linq-to-SQL or any other tracking ORM, each property changed will set the underlying object as Changed, so you change a property and then rollback, the DataContext will still flag your entity as changed.

    ReplyDelete
  3. Nice, but what about IEditableCollectionView and IEditableObject ?

    ReplyDelete
  4. Good point! The caveat should been when dealing with POCO, not when plugging into some data layer service that (presumably) should offer these types of actions anyway.

    ReplyDelete
  5. For the IEditable, you can certainly use those and obviously must if you are depending on those for different plumbing pieces. They provide contracts but not implementation, so you could refactor the implementation to satisfy the interface as well (internally use the TransactionModel when begin edit, etc are called).

    ReplyDelete
  6. This is great. Very helpful. Thanks Jeremy.
    What is IgnoreDirtyFlag class? You don't seem to provide an implementation for it.

    ReplyDelete
  7. Heh, fixed the code but was a implementation of a pattern that I forgot to refactor "out" of the example. Basically, I had an ignore dirty flag attribute that would let you manipulate the object without firing the property changed, and the using would set it then clear it on IDispose.

    ReplyDelete
  8. Hi Jeremy, do you have a sample project for this? I'd like to explore it if possible...

    ReplyDelete
  9. Not currently, but thinking of whipping one up. Stay tuned ...

    ReplyDelete
  10. +1 for the sample project. That would be very useful.

    ReplyDelete
  11. Good news ... I've completed the sample project. I just need to upload and blog it, so look for it this coming week.

    ReplyDelete
  12. The Extensions for Copy doesnt seem to work well with Lists and ObservableCollection. It doesnt seem to create a copy. It creates and copy but the values in the copy also get updated when the source is edited.

    ReplyDelete
  13. hey really helpful. if you will share sample code of implementation then it would be easy to integrate

    ReplyDelete
  14. const string ITEM = "Item";
    const string COUNT = "Count";
    const string ADD = "Add";

    Hi, Jeremy, this is inspiring. However, what are the above three lines used for? Why do you want to handle a property called 'Item'? Is this a common property (I don't think it is)?

    ReplyDelete