MVVM – Getting Started
And Away We Go!
Welcome to the start of this blog series on MVVM. I won’t make Douglas Adams’s mistake of announcing in advance how many parts there will be – I’ll just keep going as I see fit, and hopefully you’ll stick around.
Over the course of this series, I’ll be creating a demo application, StaffManager, a utility for organising the employees & departments at a small company. All of the code samples for the posts are available on Github, along with my own personal C# / WPF library. The code is written in Visual Studio 2015, for .Net framework version 4.6.2 has been updated to Visual Studio 2017, and .net framework 4.7.2. The demo applications are provided purely as proof of concept (spike applications) – there’s no unit testing or other features you might expect to find in a full production project.
So here’s the first version of the application. The code is pretty similar in style to what I’ve seen presented in other introductory articles. It compiles, it functions – so long as you click the buttons in the right order, and other than the one MessageBox() call, it’s MVVM pure. It doesn’t even have any dependencies on third party libraries.
The Model
A simple poco class, that implements INotifyPropetyChanged, with properties for Id, FirstName, LastName, Department, IsManager, and IsSelected, plus a calculated DisplayName. Person implements IComparable to enable sorting.
public class Person : INotifyPropertyChanged, IComparable<Person> { public event PropertyChangedEventHandler PropertyChanged; private void RaisePropertyChanged(string propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } private int _id; public int Id { get => _id; set { _id = value; RaisePropertyChanged("Id"); } } private string _firstName; public string FirstName { get => _firstName; set { _firstName = value; RaisePropertyChanged("FirstName"); RaisePropertyChanged("DisplayName"); } } private string _lastName; public string LastName { get => _lastName; set { _lastName = value; RaisePropertyChanged("LastName"); RaisePropertyChanged("DisplayName"); } } public string DisplayName => FirstName + " " + LastName; private string _department; public string Department { get => _department; set { _department = value; RaisePropertyChanged("Department"); } } private bool _isManager; public bool IsManager { get => _isManager; set { _isManager = value; RaisePropertyChanged("IsManager"); } } private bool _isSelected; public bool IsSelected { get => _isSelected; set { _isSelected = value; RaisePropertyChanged("IsSelected"); } } public int CompareTo(Person other) { var result = string.Compare(Department, other.Department, StringComparison.InvariantCultureIgnoreCase); if (result == 0) result = other.IsManager.CompareTo(IsManager); // sort true before false if (result == 0) result = string.Compare(LastName, other.LastName, StringComparison.InvariantCultureIgnoreCase); return result; } }
The ViewModel
Another plain C# class, again implementing INotifyPropertyChanged, that populates and maintains a list of people models, and implement the three commands for managing them.
public class MainViewModel : INotifyPropertyChanged { public MainViewModel() { LoadDataCommand = new Command(OnLoadData); AddPersonCommand = new Command(OnAddPerson); DeletePersonCommand = new Command(OnDeletePerson); ListSelectedPeopleCommand = new Command(OnListSelectedPeople); } public event PropertyChangedEventHandler PropertyChanged; private void RaisePropertyChanged(string propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } public ICommand LoadDataCommand { get; } private void OnLoadData() { var people = new List<Person> { new Person {Id = 1, Department = "I.T.", FirstName = "Alan", LastName = "Jones", IsManager = false}, new Person {Id = 2, Department = "I.T.", FirstName = "Joseph", LastName = "Preston", IsManager = true}, new Person {Id = 3, Department = "IT", FirstName = "Stella", LastName = "Mcbride", IsManager = false}, new Person {Id = 4, Department = "IT", FirstName = "Branden", LastName = "Owens", IsManager = false}, new Person {Id = 5, Department = "I.T.", FirstName = "Leonard", LastName = "Marquez", IsManager = false}, new Person {Id = 6, Department = "I.T.", FirstName = "Colin", LastName = "Brady", IsManager = false}, new Person {Id = 7, Department = "Accounts", FirstName = "Callum", LastName = "Roberts", IsManager = true}, new Person {Id = 8, Department = "Accounts", FirstName = "Jillian", LastName = "Scott", IsManager = false}, new Person {Id = 9, Department = "Accounts", FirstName = "Calvin", LastName = "Moran", IsManager = false}, new Person {Id = 10, Department = "Sales", FirstName = "Harlan", LastName = "Reid", IsManager = false}, new Person {Id = 11, Department = "Sales", FirstName = "Felix", LastName = "Schroeder", IsManager = false}, new Person {Id = 12, Department = "Sales", FirstName = "Joseph", LastName = "Smith", IsManager = true}, new Person {Id = 13, Department = "Sales", FirstName = "Jasmine", LastName = "Emerson", IsManager = false}, new Person {Id = 14, Department = "Sales", FirstName = "Lucas", LastName = "Edwards", IsManager = false}, new Person {Id = 15, Department = "Sales", FirstName = "David", LastName = "Baxter", IsManager = false}, new Person {Id = 16, Department = "Logistics", FirstName = "Kane", LastName = "Foreman", IsManager = false}, new Person {Id = 17, Department = "Logistics", FirstName = "Laurel", LastName = "Curtis", IsManager = false}, new Person {Id = 18, Department = "Logistics", FirstName = "Lucy", LastName = "Tanner", IsManager = true}, new Person {Id = 19, Department = "Logistics", FirstName = "Christian", LastName = "Pittman", IsManager = false}, new Person {Id = 20, Department = "Logistics", FirstName = "Patricia", LastName = "Wilkinson", IsManager = false} }; people.Sort(); People = people; SelectedPerson = people.FirstOrDefault(); } public ICommand AddPersonCommand { get; } private void OnAddPerson() { var people = People; var newPerson = new Person(); people.Add(newPerson); People = null; People = people; SelectedPerson = newPerson; } public ICommand DeletePersonCommand { get; } private void OnDeletePerson() { var people = People; var personToDelete = SelectedPerson; people.Remove(personToDelete); People = null; People = people; SelectedPerson = null; } public ICommand ListSelectedPeopleCommand { get; } private void OnListSelectedPeople() { var selectedpeople = People.Where(p => p.IsSelected).ToList(); var message = selectedpeople.Any() ? "The following people are selected\r\n " + string.Join("\r\n ", selectedpeople.Select(p=>p.DisplayName)) : "No people are selected"; MessageBox.Show(message); } private List<Person> _people; public List<Person> People { get => _people; set { _people = value; RaisePropertyChanged("People"); } } private Person _selectedPerson; public Person SelectedPerson { get => _selectedPerson; set { _selectedPerson = value; RaisePropertyChanged("SelectedPerson"); } } }
The View
A standard WPF window, with a DataGrid to display the list of people, a set of controls to allow editing of the selected person, and buttons to call the ViewModel commands.
<Window x:Class="StaffManager.MainView" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:StaffManager" Title="MVVM - Getting Started" MinWidth="500" SizeToContent="WidthAndHeight" TextOptions.TextFormattingMode="Display" TextOptions.TextRenderingMode="ClearType" UseLayoutRounding="True"> <Window.DataContext> <local:MainViewModel /> </Window.DataContext> <Grid Margin="8"> <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> </Grid.RowDefinitions> <DataGrid Grid.Row="0" Height="200" HorizontalAlignment="Stretch" AutoGenerateColumns="False" ItemsSource="{Binding People}" SelectedItem="{Binding SelectedPerson, Mode=TwoWay}"> <DataGrid.Columns> <DataGridCheckBoxColumn Binding="{Binding IsSelected, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Header="Select" IsReadOnly="False" /> <DataGridTextColumn Binding="{Binding Id}" Header="Id" IsReadOnly="True" /> <DataGridTextColumn Binding="{Binding DisplayName}" Header="Name" IsReadOnly="True" /> <DataGridTextColumn Binding="{Binding Department}" Header="Department" IsReadOnly="True" /> <DataGridCheckBoxColumn Binding="{Binding IsManager}" Header="Is Manager" IsReadOnly="True" /> </DataGrid.Columns> </DataGrid> <StackPanel Grid.Row="1" Margin="0,8" Orientation="Horizontal"> <Button Padding="8,2" Command="{Binding LoadDataCommand}" Content="Load Data" /> <Button Margin="8,0,0,0" Padding="8,2" Command="{Binding AddPersonCommand}" Content="Add Person" /> <Button Margin="8,0,0,0" Padding="8,2" Command="{Binding DeletePersonCommand}" Content="Delete Person" /> <Button Margin="8,0,0,0" Padding="8,2" Command="{Binding ListSelectedPeopleCommand}" Content="List Selected People" /> </StackPanel> <Grid Grid.Row="2" DataContext="{Binding SelectedPerson}"> <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto" /> <ColumnDefinition Width="*" /> </Grid.ColumnDefinitions> <TextBlock Grid.Row="0" Grid.Column="0" Margin="0,2,8,2" VerticalAlignment="Center" FontWeight="Bold" Text="Id" /> <TextBlock Grid.Row="0" Grid.Column="1" Margin="0,2" HorizontalAlignment="Left" VerticalAlignment="Center" Text="{Binding Id}" /> <TextBlock Grid.Row="1" Grid.Column="0" Margin="0,2,8,2" VerticalAlignment="Center" FontWeight="Bold" Text="First Name" /> <TextBox Grid.Row="1" Grid.Column="1" Width="200" Margin="0,2" HorizontalAlignment="Left" VerticalAlignment="Center" Text="{Binding FirstName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" /> <TextBlock Grid.Row="2" Grid.Column="0" Margin="0,2,8,2" VerticalAlignment="Center" FontWeight="Bold" Text="Last Name" /> <TextBox Grid.Row="2" Grid.Column="1" Width="200" Margin="0,2" HorizontalAlignment="Left" VerticalAlignment="Center" Text="{Binding LastName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" /> <TextBlock Grid.Row="3" Grid.Column="0" Margin="0,2,8,2" VerticalAlignment="Center" FontWeight="Bold" Text="Department" /> <TextBox Grid.Row="3" Grid.Column="1" Width="200" Margin="0,2" HorizontalAlignment="Left" VerticalAlignment="Center" Text="{Binding Department, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" /> <TextBlock Grid.Row="4" Grid.Column="0" Margin="0,2,8,2" VerticalAlignment="Center" FontWeight="Bold" Text="Is Manager" /> <CheckBox Grid.Row="4" Grid.Column="1" HorizontalAlignment="Left" VerticalAlignment="Center" IsChecked="{Binding IsManager, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" /> </Grid> </Grid> </Window>
The only extra class is a very basic ICommand implementation.
public class Command: ICommand { private readonly Action _execute; public Command(Action execute) { _execute = execute; } public bool CanExecute(object parameter) { return true; } public void Execute(object parameter) { _execute?.Invoke(); } public event EventHandler CanExecuteChanged; }
So, although the application runs, the project structure and design do leave a lot to be desired. Over the next few posts I’ll be refactoring it into my particular style, creating a cleaner code-base and adding to the application functionality.
In my next post, I’ll begin the clean up by looking at the Model layer.
Don’t forget that all of the code samples for this blog are available on Github, along with my own personal C# / WPF library.
If you find this article useful, or you have any other feedback, please leave a comment below.
What license is your code published under? Can I use your framework as a basis for my application(s)?
Hi Terry
The code is released under the MIT licence. Feel free to use it in any way, including commercial applications, so long as the licence and copyright notice is included.
Thanks.
I feel like an idiot, it shows the MIT license right on GitHub.
I have two questions.
The first has to do with TreeView. Any thoughts on drag and drop with your code?
I like your attention to separation of concerns for MVVM, but the proof of it’s reusing the viewmodels with a different UI. Had you thought about a UWP or Xamarin demo?
I realize that the two questions are a bit at odds with each other…
I’ve focused on WPF in this blog, purely because that’s still how I’m earning my living, and therefore most comfortable. The post relating to Model and ViewModel could be directly applied to UWP and Xamarin applications, and although those relating to UI elements might take a little tweaking they should still offer a good base for those platforms too.
I’ll be writing a post on generic drag and drop for MVVM shortly, I’ll try to include an example of how to apply that to TreeView controls.
Thanks much.