Miles Watson
Posted on January 9, 2021
In this article, I explain what MVVM is and how it can help you create better web/desktop applications. I use C#/WPF in my examples, although you should be able to follow along with basic knowledge of OOP and HTML.
Context
Until a month ago, my experience of creating graphical user interfaces was limited to plain HTML/CSS websites and some basic python applications using appJar. I always felt intimidated by the concept of having to work with GUIs: as a backend-oriented programmer, my way of thinking never seemed to mesh well with the popular frameworks.
Recently, I have had to started working on a desktop client for my decentralized social media. Most people would agree that this is an intimidating project, especially for an inexperienced frontend developer. Initially, I found making progress hard - until I learnt about MVVM.
What is MVVM?
MVVM stands for Model, View, View-Model. These form the three main parts of an MVVM application.
Models
Models contain core algorithms and data structures to be used by the application - the "business logic". These are often written in a "pure" programming language, without using any special libraries.
View-Models
View-models are special classes to represent the state of a view. For example, they have properties to represent displayed text, user-input textboxes, or even other view models. They should also have functions / commands which represent events (such as when a button is clicked).
View models often have some boilerplate code, so that they can integrate with whatever MVVM framework you are using. However, they should work on their own, with no dependency on the view.
Views
Views contain the code that is used by the UI. They are often written with a markup language (e.g. XAML or HTML). The views outline the structure (and styling) of the view, and their content is set with bindings.
Bindings allow content to be linked to a property of the view-model. Changing the property in the view-model will automatically update the shown content in the view. The binding can also work the other way, using a two-way binding. This is useful for textboxes, as you can have the view-model property update on each keystroke (this can be used for real-time input validation).
A Quick Example
To demonstrate the ideas I've talked about, I will quickly guide you through the process of creating an application using MVVM. The application will allow the user to click through a list of people.
I'll be using WPF (Windows Presentation Framework). If you want to follow along, create a new WPF App (.NET)
project in Visual Studio. I've called mine Demo
.
Creating a model
First, we'll create a class to represent a person. It will contain two fields - Name
and Age
.
I've put the class in the Demo.Models
namespace, and saved it as Models/PersonModel.cs
:
public class PersonModel
{
public string Name { get; set; }
public int Age { get; set; }
public PersonModel(string name, int age)
{
Name = name;
Age = age;
}
}
Creating the view model
Next, we will create the View Model. There will be four properties - two strings, and two commands. The strings will represent the name and age of the currently displayed person, whilst the buttons will allow us to click through the people.
Whenever we change a property, we have to alert the framework (so that the view can be updated). In WPF, this is achieved by implementing INotifyPropertyChanged
and then calling NotifyPropertyChanged
whenever a property is set.
I've put the class in the Demo.ViewModels
namespace, and saved it as ViewModels/PeopleViewModel.cs
:
public class PeopleViewModel : INotifyPropertyChanged
{
//// Bindable properties
private string _name;
public string Name
{
get => _name;
private set
{
_name = value;
NotifyPropertyChanged(nameof(Name));
}
}
private string _age;
public string Age
{
get => _age;
private set
{
_age = value;
NotifyPropertyChanged(nameof(Age));
}
}
public ICommand Previous { get; init; }
public ICommand Next { get; init; }
//// Implementation
private PersonModel[] people = new PersonModel[] {
new PersonModel("Alice", 20),
new PersonModel("Bob", 25),
new PersonModel("Charlie", 30)
};
private int index = 0;
public PeopleViewModel()
{
Name = people[index].Name;
Age = people[index].Age.ToString();
Previous = new RelayCommand(() =>
{
if (index > 0)
{
index--;
Name = people[index].Name;
Age = people[index].Age.ToString();
}
});
Next = new RelayCommand(() =>
{
if (index < people.Length - 1)
{
index++;
Name = people[index].Name;
Age = people[index].Age.ToString();
}
});
}
//// Boilerplate code to satisfy INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged(string propertyName)
{
if (PropertyChanged is null) return;
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
In this example, the RelayCommand
class is used to wrap functions as an ICommand
. An explanation of this (and the code for the class) can be found here.
Why isn't the age an integer?
The age property isn't an integer, because it isn't representing the age of the person: it is representing the displayed content. The view will be displaying a string, therefore the property should be of type string.
Creating the view
Now that the view-model has been created, we will create a UserControl
to act as the view. It will have two labels and two buttons - the labels will be bound to the Name and Age strings, and the buttons will bound to the Previous and Next commands.
In WPF, each view has a DataContext
. By including the Demo.ViewModels
namespace, we can set the data context of the user control to be a PersonViewModel
. Then, we can bind to a property X
of the UserControl.DataContext
by using the "{Binding X}"
syntax.
I've saved the user control as Views/PeopleView.xaml
:
<UserControl x:Class="Demo.Views.PeopleView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:viewmodels="clr-namespace:Demo.ViewModels"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<UserControl.DataContext>
<viewmodels:PeopleViewModel />
</UserControl.DataContext>
<StackPanel Background="White">
<Label Content="{Binding Name}" />
<Label Content="{Binding Age}" />
<StackPanel Orientation="Horizontal">
<Button Content="Previous" Command="{Binding Previous}" />
<Label />
<Button Content="Next" Command="{Binding Next}" />
</StackPanel>
</StackPanel>
</UserControl>
What about the code-behind?
The code-behind is rarely used in WPF applications that use MVVM. All the UI logic should be contained in the view-model, not the code-behind. This separation of concerns is what makes MVVM such a versatile architecture!
Finishing up
Now that we've created our view, we can display it in MainWindow.xaml
.
<Window x:Class="Demo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:views="clr-namespace:Demo.Views"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<StackPanel>
<views:PeopleView />
</StackPanel>
</Window>
Upon running, you should see the application working as expected:
Why all the effort?
You may be wondering why we should go through all this effort. There are three main reasons that MVVM is better than the standard "code-behind" approach.
Testability
Because your view-model is entirely decoupled from your view, you can use a testing framework (e.g. MSTest) to check that your interface logic is correct, without the need to manually test the UI yourself.
Modularity
Now that you have created your model and view-model pair, you can can reuse them as a component across your code. For example, you can add a second instance of the component like so:
<StackPanel>
<views:PeopleView />
<views:PeopleView />
</StackPanel>
By nesting components within each other, you can build up complex, modular user interfaces with minimal effort.
You can even create multiple views bound to the same view-model! I've done this by setting the DataContext
of the MainWindow
to be a new PersonViewModel
, and then binding both of the views to it.
<Window x:Class="Demo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:viewmodels="clr-namespace:Demo.ViewModels"
xmlns:views="clr-namespace:Demo.Views"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.DataContext>
<viewmodels:PeopleViewModel />
</Window.DataContext>
<StackPanel>
<views:PeopleView DataContext="{Binding}" />
<views:PeopleView DataContext="{Binding}" />
</StackPanel>
</Window>
Note: You would normally bind the DataContext
property of a view to a property of the current view-model. However, the main window doesn't have a view-model, and so I've bound it straight to Window.DataContext
.
Platform Agnosticism
Splitting your application into the three parts allows you to reuse models and view-models across multiple platforms. This is made easy by using cross-platform frameworks like MvvmCross (which supports Windows, Mac, iOS and Android).
Conclusion
MVVM is an application architecture which allows you to develop testable, modular, and cross-platform applications. It splits code into three parts:
- model
- view-model
- view
The separation of concerns improves code maintainability and readability. Furthermore, the model and view-model can be unit-tested and shared across platforms, allowing for quicker development of cross-platform applications.
Footnote
If you enjoyed reading this, then consider dropping a like or following me:
I'm just starting out, so the support is greatly appreciated!
Disclaimer - I'm a (mostly) self-taught programmer, and I use my blog to share things that I've learnt on my journey to becoming a better developer. I apologise in advance for any inaccuracies I might have made - criticism and corrections are welcome!
Posted on January 9, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.