I've been spending a lot of time learning about WPF recently and its chief design pattern: MVVM. It is partly work-related, as we've been doing some major backend refactoring and could take the advantage to change some of the front-end too. We've been gradually adopting WPF because apparently, Blazor and co. are still unproven to be basing an entire system on them.
My primary focus is on managing complexity, coupling, and increasing cohesion. On the face of it, WPF is a surprisingly nice technology if not a little verbose. Unfortunately, there appears to be a lot of... misinformation, and... bad advice on the internet, which kinda put me off and prompted this series so I can note/organize my thoughts around this technology.
Our main goal for this 'series' is to minimize the complexity of our models/ViewModels, coupling, and increase cohesion. We'll have a look at how to deal with detecting changes, component communication, validation (using FluentValidation :D) component child-parent relationship, dependency injection, and styling.
What goes into MVVM?
MVVM stands for Model, View, and ViewModel and WPF doesn't require you to use MVVM. In fact, when a new XAML component is created, you only get a code-behind file, very much like a WinForm. However, WPF was built with the MVVM pattern in mind. It's pretty obvious that if you apply it right it can lead to amazing improvements in managing complexity.
View - The XAML file. By default, it links to the code-behind and allows to write UI using... well XML. However, it can go a step further that allows for one-way or two-way binding to a ViewModel.
Model - This is where it appears to be a lot of... wired or misguided information on the internet. Some say that model is the domain model, some that it comes from EF, some it comes from the service. That may be true, of course, but the point of a (domain) model is to model objects in the real world. It feels like it should be a (more-less) plain POCO object, with setters and getters.
ViewModel -- What? This is where things start to become confusing and somewhat controversial...
What problem is MVVM trying to solve?
Design patterns are meant to solve common problems in a neat way. MVVM is a design pattern, so what problem exactly is MVVM trying to solve? Why can't we simply use View and Model (e.g. AngularJS does that with MVwhatever)).
Well... As far as I can tell, there appear to be two at least two good reasons.
WPF can't auto-detect changes
WPF can't auto-detect changes to a model. In WPF, an event (NotifyPropertyChanged
) has to be raised to tell the view that a property has changed. The domain model should remain independent of the underlying implementation. If you were to implement this event in your (domain) model, you'd be coupling it to WPF implementation making your app significantly less cohesive. It can also be super tricky to do if for example, you don't have access to the source code.
This is where ViewModel comes to the rescue. It acts like a 'highway': to set your model and fire NotifyPropertyChanged
, thereby keeping your model independent of WPF. However, for some reason models are not used like that, in fact, I've never actually seen it in any online examples. Pretty much all of them use fields to store the data and not models.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
// what appears to be the 'correct way'TM of doing MVVM public class Model { public string Name { get; set; } } public class ViewModel { private Model _model = new(); public string Name { get => _model.Name; set => { _model.Name = value; NotifyPropertyChanged(); } } } // examples on the internet and what most of us seem to be doing, which is a little wired. public class ViewModel { private string _name; public string Name { get => _name; set { _name = value; NotifyPropertyChanged(); } } } |
Most modern frameworks in 2022, Blazor and Angular amongst them, are able to auto-detect when the state changes and reflect it in the view. It would explain is why we don't see MVVM more often these days.
No logic in the view
XAML files don't exactly lend themselves particularly well to writing logic. XAML is verbose enough as it is. Its sole job is to evaluate binding for a particular property on a particular control and act accordingly. However, in the online community, there are two schools of thought: those that believe that ViewModel should be decoupled from the view (almost like a model) and those that don't care.
To illustrate this, imagine, you want to conditionally hide and show a component in the view. In the decouplers/purist camp, in your ViewModel, you would model the Hide/Show property as a boolean (e.g. IsMyControlHidden
), then used Binding and a BooleanToVisibilityEnumConverter
in the View to hide and show the control.
1 2 3 4 5 6 7 |
//ViewModel public bool IsMyControlHidden => SomeCondition; //View <Control Visibility="{Binding Path=IsMyControlHidden, Converter={StaticResource BoolToVisibilityConverter}}"/> |
In the other camp, you wouldn't bother with the converter, you'd use Visibility enum directly in the ViewModel.
1 2 3 4 5 6 7 |
//ViewModel public Visibility MyControlVisibility => SomeCondition ? Visibility.Collapsed : Visibility.Visible; //View <Control Visibility="{Binding Path=MyControlVisibility}"/> |
I am not entirely sure where the 'decouplers' approach comes from. I can't really see any major advantages, but I do see quite a few disadvantages. Maybe if you are using fields instead of the Model, then it is a little bit unclear what's the role of the ViewModel in WPF; then some of that responsibility is shifted on to the converters. Whatever the reasons, the 'non-purist' seems better for me for a couple of reasons:
- the less code, the better - the purist approach involves writing an (uncessary) converter
- in my example boolean true maps to collapsed.. what if my collegue mistakes evaluating true for visible
- visiblity enum has 3 states actaully: hidden, collapsed, visible, how are you planning to implement it using a boolean? are you planning on implementing another converter?
- using the visibility enum doesn't make the code any more coupled and it is equally as easy to unit test as the boolean
Conclusion
It does feel like WPF was well ahead of its time. By comparison, AngularJS which works on very similar principles (of binding) way was released 4 years! later.