Here at Firefinch Software we’ve been writing desktop applications in WPF for years. And of course, since we are writing WPF, we are also using MVVM architecture. Otherwise what’s the point of those wonderful data bindings? I’d like to take a deeper look at how we write MVVM.
While MVVM sounds great conceptually, it took us a few years to refine the architecture into a framework that just works. In the old days it felt like a convoluted mechanism that didn’t scale to large systems well. Thousand-line god-view-models anyone? These days however, we have a reusable infrastructure that is intuitive to use and practical for real-world applications. So how did we get here?
We’ve pulled out a few key requirements for solving our issues:
Requirement#1: View models can represent multiple model objects
Typical MVVM examples present a one-to-one approach: one view has one view model, and that view model represents one model object.
Although this is useful to demonstrate the fundamentals of MVVM, real-world applications rarely follow this pattern so neatly. We needed our architecture to allow one view model to represent a composition of multiple model objects, and to notify the view accordingly.
This is not much of a jump. A button on the UI can now trigger a view model to change multiple model objects. But what happens when the model needs to be updated from sources other than the UI?
Requirement #2: View models are alerted to changes to the model, regardless of the source of the changes
A common requirement in many applications is for the model to change from an external stimulus, not just when the user clicks a button. The view still needs to be alerted when this happens though – how do we handle that scenario? We don’t want to clutter the model with the boilerplate code required for implementing INotifyPropertyChanged. Though INotifyPropertyChanged is technically not just for views, we find it hard to believe you’d use it unless you were writing WPF and MVVM.
Command pattern to the rescue. We add behaviour to our basic view model base class which manages the notification cycle for us. In short:
- Each view model tells the command layer which model objects it is watching, and has access to commands via dependency injection
- Each command tells the command layer which model objects it modifies
- The command layer will alert relevant subscribed view models about modified model objects when a command is executed
These commands provide a central mechanism to make changes to the model that can be triggered by non-UI sources. This includes external events, other commands, and even the model itself.
The command pattern also provides additional benefits. Simple audit trails and even undo/redo can be written with this at the core.
However, there is one major question remaining: how do we tell the view model which properties of the model have changed, so that the view can update only the relevant data bindings?
Answer: we don’t.
Requirement #3: View models hold as little state as possible
Maintaining how each view model should react when particular model properties change is unwieldy at best in small applications. In large applications this scales up to a headache, a constant source of mysterious broken bindings.
We decided to try to avoid view models holding state (with the exception of view-only concerns that are not comfortably handled by XAML code), including the translation from model changes into UI updates. Instead, the view model will only be aware that something has changed. In response it will notify the view element to update all its bindings, regardless of what the actual change is. And since the behaviour is so generic it’s clearly destined for the view model base class, away from the logic of our concrete view model classes.
The downside is, of course, performance overhead. This risk can be mitigated with good design: small, cohesive view models and a clear separation of concerns. But for the cases where we are worried about genuine performance hits – for example, updating WPF resources for UI theming, or extremely chatty operations – we needed to have enough flexibility to override and explicitly handle subscriptions and notifications.
With these ideas in mind, all that’s left is to make the architecture something we can actually use.
Requirement #4: A pit of success
Finally, we wanted our augmented MVVM framework to lead us into the pit of success; to naturally do the right thing. It should be more difficult for us to do things the wrong way. A developer on our team should only need to know which model objects they are interested in. To ensure this, we wanted the framework to be opinionated and encourage us to design the software in a particular way.
The above requirements should result in an MVVM architecture that enables us to build user interfaces for large applications that have complex behind-the-scenes interaction with the domain model. Here’s a snapshot of how we might expect a part of our system architecture to look, and what our framework should be able to facilitate with ease:
I will discuss how we actually implemented this in my next blog post.