I had a lot of requests after both of my posts about transaction with the Model-View-ViewModel (MVVM) pattern and Yet Another View Locator Pattern to provide a sample project. You asked, I listened, and here it is.
I'll walk through how I built the example. Most of the code is taken verbatim from my two previous posts. To make it easier to use the transactions, I created a transaction command:
public class TransactionCommand<T> : ICommand where T : BaseINPC { private readonly TransactionModel<T> _transaction; private readonly bool _forCommit; public TransactionCommand(TransactionModel<T> transaction, bool forCommit) { _transaction = transaction; _forCommit = forCommit; transaction.Value.PropertyChanged += (o, e) => { if (CanExecuteChanged != null) { CanExecuteChanged(this, EventArgs.Empty); } }; } public bool CanExecute(object parameter) { return _transaction.Value.IsDirty; } public void Execute(object parameter) { string message = _forCommit ? "Are you sure you wish to save your changes?" : "Are you sure you wish to cancel? All changes will be lost."; var result = MessageBox.Show(message, "Please confirm", MessageBoxButton.OKCancel); if (result.Equals(MessageBoxResult.OK)) { if (_forCommit) { _transaction.Value.IsDirty = false; _transaction.Commit(); } else { _transaction.Rollback(); } } } public event EventHandler CanExecuteChanged; }
Normally I'd separate the dialog box but this is a "quick and dirty" example so I left it as it is. "If you're not for us, you're against us." Well, not entirely, but this command takes in the object it "listens" to and is either for commit, or for rollback. It will automatically look at the dirty flag on the target model and only enable when set, and then shows the appropriate confirmation before committing, rolling back, or canceling the operation.
With that handy command in place, I can now create a base view model that is typed to a model we will wrap with transactions. It looks like this:
public class ViewModelBase<T> : BaseINPC where T : BaseINPC { private TransactionModel<T> _transaction; public T EditableObject { get { return _transaction.Value; } set { RaisePropertyChanged(() => EditableObject); _Init(value); } } private void _Init(T instance) { _transaction = new TransactionModel<T>(instance); ConfirmCommand = new TransactionCommand<T>(_transaction, true); CancelCommand = new TransactionCommand<T>(_transaction, false); RaisePropertyChanged(()=>ConfirmCommand); RaisePropertyChanged(()=>CancelCommand); } public ICommand ConfirmCommand { get; set; } public ICommand CancelCommand { get; set; } }
The view model exposes an EditableObject
property. When set, it automatically wraps this in a transaction and binds the appropriate confirm and cancel commands.
The typed view model creates a dummy contact record (this is where you would use a service or some other mechanism to fetch the records you will edit):
[ViewModelForType("Contact")] public class ContactViewModel : ViewModelBase<Contact> { public ContactViewModel() { var contact = new Contact { FirstName = "Jeremy", LastName = "Likness", Email = "[email protected]", PhoneNumber = "555-1212" }; contact.IsDirty = false; EditableObject = contact; } }
Yes, the phone number is fake. Notice that I type it to my contact entity and then the only work I need to do is set the EditableObject. By tagging the view model, I can route it to the view which has a corresponding tag. This is what the routing engine looks like. Note I am dealing really with the one circumstance of the contact view, so I hard coded the example to the first item in the list. For a larger application, I'd likely use an ItemsControl
with a navigation interface that performs the binding as the views come into focus.
[Export] public class ViewManager : IPartImportsSatisfiedNotification { [ImportMany(AllowRecomposition = true)] public Lazy<UserControl, IViewForTypeCapabilities>[] Views { get; set; } [ImportMany(AllowRecomposition = true)] public Lazy<BaseINPC, IViewModelForTypeCapabilities>[] ViewModels { get; set; } [Import] public ContentControl MainRegion { get; set; } public void OnImportsSatisfied() { if (Views.Length > 0) { var view = Views[0].Value; MainRegion.Content = view; var viewModel = (from vm in ViewModels where vm.Metadata.TypeName.Equals(Views[0].Metadata.TypeName) select vm.Value) .FirstOrDefault(); view.DataContext = viewModel; } } }
That's it! With the MEF glue to pull this all together, you get the application below. Notice how you can confirm and then change text and rollback to the original and how it is all handled by the framework - the developer's only concern is simply binding to EditableObject
. (Also note I did not wire any validations, but those can easily fire for each field and still not interfere with the transaction model).
Enjoy!
Remember, you can download the full source here.