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.