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;

        protected virtual void RaisePropertyChanged(string propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }

        private int _id;

        public int Id
        {
            get { return _id; }
            set
            {
                _id = value;
                RaisePropertyChanged("Id");
            }
        }

        private string _firstName;

        public string FirstName
        {
            get { return _firstName; }
            set
            {
                _firstName = value;
                RaisePropertyChanged("FirstName");
                RaisePropertyChanged("DisplayName");
            }
        }

        private string _lastName;

        public string LastName
        {
            get { return _lastName; }
            set
            {
                _lastName = value;
                RaisePropertyChanged("LastName");
                RaisePropertyChanged("DisplayName");
            }
        }

        public string DisplayName => FirstName + " " + LastName;

        private string _department;

        public string Department
        {
            get { return _department; }
            set
            {
                _department = value;
                RaisePropertyChanged("Department");
            }
        }

        private bool _isManager;

        public bool IsManager
        {
            get { return _isManager; }
            set
            {
                _isManager = value;
                RaisePropertyChanged("IsManager");
            }
        }

        private bool _isSelected;

        public bool IsSelected
        {
            get { return _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 { return _people; }
            set
            {
                _people = value;
                RaisePropertyChanged("People");
            }
        }

        private Person _selectedPerson;

        public Person SelectedPerson
        {
            get { return _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="Getting Started with Mvvm - Part 1" 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" Width="400" Height="200" AlternatingRowBackground="AliceBlue" AutoGenerateColumns="false" CanUserAddRows="False" CanUserDeleteRows="False" GridLinesVisibility="None" HeadersVisibility="Column" IsReadOnly="false" ItemsSource="{Binding People}" SelectedItem="{Binding SelectedPerson, Mode=TwoWay}" VerticalScrollBarVisibility="Visible">
            <DataGrid.Columns>
                <DataGridCheckBoxColumn Binding="{Binding IsSelected, Mode=TwoWay}" 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 Command="{Binding LoadDataCommand}" Content="Load Data" Padding="8,2" />
            <Button Margin="8,0,0,0" Command="{Binding AddPersonCommand}" Content="Add Person" Padding="8,2" />
            <Button Margin="8,0,0,0" Command="{Binding DeletePersonCommand}" Content="Delete Person" Padding="8,2" />
            <Button Margin="8,0,0,0" Command="{Binding ListSelectedPeopleCommand}" Content="List Selected People" Padding="8,2" />
        </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.

Leave a Reply

Your email address will not be published. Required fields are marked *