Firefinch Deep Dive MVVM 2

Deep Dive: MVVM in Real-World Applications, Part 2

Posted on Posted in Uncategorised

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.