In my previous post I discussed the requirements of an MVVM architecture that is appropriate for use in real-world applications. For our purposes, this means one that is convenient to use even when scaled to large applications with complex behind-the-scenes interactions.
After a number of iterations we arrived at an implementation that met these requirements. The resulting architecture has an instinctive quality about it; we are guided towards injected model objects and the commands for changing them as soon as a view model inherits our ViewModelBase
.
It’s also very small! The implementation is centred around three main classes:
1) ModelChangeNotifier
This class handles the model change subscriptions for the application. View models will provide the ModelChangeNotifier
with model objects and actions that perform UI notifications. Commands will tell the ModelChangeNotifier
when a model object has changed, in turn triggering all associated UI notifications. Weak references are used so that the objects can be garbage collected if all other references no longer exist. If view models are not application-lifetime it may be necessary to trim these unused subscriptions to avoid performance issues.
public class ModelChangeNotifier { private readonly List<(WeakReference watchedModelRef, Action actionWhenChanged)> modelChangeSubscriptions = new List<(WeakReference watchedModelRef, Action actionWhenChanged)>(); public void Subscribe(object watchedModel, Action actionWhenChanged) { var watchedModelRef = new WeakReference(watchedModel); var subscription = (watchedModelRef: watchedModelRef, actionWhenChanged: actionWhenChanged); this.modelChangeSubscriptions.Add(subscription); } public void Notify(IEnumerable<object> changedModels) { foreach (var changedModel in changedModels) { this.Notify(changedModel); } } public void Notify(object changedModel) { foreach (var modelChangeSubscription in this.modelChangeSubscriptions) { var watchedModel = modelChangeSubscription.watchedModelRef.Target; if (changedModel == watchedModel) { modelChangeSubscription.actionWhenChanged(); } } } }
In order for this to work, there must only be a single shared instance of ModelChangeNotifier
used throughout the application. We handle this through dependency injection.
2) ViewModelBase
This is the base class for all our view models. A view model will pass to it all the model objects it wants to watch. The ViewModelBase
will use the ModelChangeNotifier
to subscribe each of these objects to an INotifyPropertyChanged
implementation that updates the UI.
public abstract class ViewModelBase : INotifyPropertyChanged { protected ModelChangeNotifier ModelChangeNotifier { get; } protected ViewModelBase(ModelChangeNotifier modelChangeNotifier, params object[] watchedModels) { this.ModelChangeNotifier = modelChangeNotifier; foreach (var watchedModel in watchedModels) { this.ModelChangeNotifier.Subscribe(watchedModel, this.NotifyAllPropertyBindings); } } protected virtual void NotifyAllPropertyBindings() { var properties = this.GetType().GetProperties(); foreach (var property in properties) { this.NotifyPropertyChanged(property.Name); } } // ... implementation of NotifyPropertyChanged // ... either using an MVVM framework or own implementation }
3) ModelChangeCommand
This is the base class for all our commands. A command will pass to it all model objects that it will modify when executed. The base ModelChangeCommand
will use the ModelChangeNotifier
to call the UI notification actions once the changes have been made. (Error handling has been omitted from the code snippet for clarity.)
public abstract class ModelChangeCommand<T> { private readonly ModelChangeNotifier modelChangeNotifier; private readonly object[] changedModels; protected ModelChangeCommand(ModelChangeNotifier modelChangeNotifier, params object[] changedModels) { this.modelChangeNotifier = modelChangeNotifier; this.changedModels = changedModels; } protected abstract void Action(T parameter); public void ExecuteAndNotify(T parameter) { var task = this.Execute(parameter); this.Notify(); } public async Task ExecuteAndNotifyAsync(T parameter) { await Task.Run(() => this.Execute(parameter)) .ContinueWith(task => this.Notify(), TaskContinuationOptions.OnlyOnRanToCompletion); } private Task Execute(T parameter) { this.Action(parameter); return Task.CompletedTask; } private Task Notify() { this.modelChangeNotifier.Notify(this.changedModels); return Task.CompletedTask; } }
Example usage
Here is an example of how we use these view model and commands.
We have a model object – in this case, Foo
.
public class Foo { public int Number { get; private set; } public void Update(int number) { this.Number = number; } }
We write a command that modifies Foo
. It also tells the base ModelChangeCommand
that Foo
will be changed in order to notify any watchers.
public class UpdateFooCommand : ModelChangeCommand<int> { private readonly Foo foo; public UpdateFooCommand(ModelChangeNotifier modelChangeNotifier, Foo foo) : base(modelChangeNotifier, foo) { this.foo = foo; } protected override void Action(int parameter) { this.foo.Update(parameter); } }
Our FooViewModel
can execute the command to update Foo
. And since FooViewModel
allows views to bind to properties that depend on the state of Foo
, it tells the ViewModelBase
to watch Foo
for changes. Note that this view model is also presenting data about Bar
and therefore watching for changes in the same way as Foo
. If it also needed to make changes to Bar
, a command would be required.
public class FooViewModel : ViewModelBase { private readonly Foo foo; private readonly Bar bar; private readonly Random random = new Random(); private readonly UpdateFooCommand updateFooCommand; // notifications about changes to these model objects are handled via ViewModelBase public string FooString => $"Foo is {this.foo.Number}"; public string BarString => $"Bar is {this.bar.Number}"; // view-only concerns must still handle notifications explicitly private bool isUpdating; public bool IsUpdating { get { return this.isUpdating; } set { this.isUpdating = value; this.NotifyPropertyChanged(nameof(this.IsUpdating)); } } public FooViewModel(Foo foo, Bar bar, UpdateFooCommand updateFooCommand, ModelChangeNotifier modelChangeNotifier) : base(modelChangeNotifier, foo, bar) { this.foo = foo; this.bar = bar; this.updateFooCommand = updateFooCommand; } // called by the view public async void UpdateFooAsync() { this.IsUpdating = true; var number = this.random.Next(); await this.updateFooCommand.ExecuteAndNotifyAsync(number); this.IsUpdating = false; } }
This has significantly improved our productivity. No more worrying about MVVM boilerplate on new projects – it’s just there, ready to be plugged in with no fuss. No more headscratching over broken UI bindings – either the view model or command has not highlighted the model object it cares about. No more wondering how a rogue developer has handled external events through obscure mechanisms – we are all using the same conventions enforced by an opinionated framework.