MVVM – ViewModel (part 1)

(W)Rappers Delight

In this post, I’ll continue with my introduction to MVVM, and the refactoring of the demonstration StaffManager application, with my first look at the ViewModel layer.

Anyone who’s already looked at MVVM will be aware of the concept of ViewModels as the grand controller and data provider for a View, or occasionally a complicated user control. I’ll be dealing with that type of ViewModel in the next post in this series. At this point I’m going to break away from the usual convention, by introducing a second type of ViewModel – as a wrapper around an individual Model object. I’ll refer to these wrapper ViewModels as WVMs to avoid the text becoming too verbose.

In my discussion about Model classes in the last post, I touched on non-data properties. These are transient properties that represent a Model item’s current state whilst the application is running, but which won’t be persisted to the data-store. The ideal home for these is the WVM. The Person class had FullName and IsSelected properties, which will be moved to the PersonViewModel class. These WVMs can also hold references to other WVMs, either as single items or collections. So each PersonViewModel will also link to a DepartmentViewModel, and equally each DepartmentViewModel will have a list of PersonViewModels. The only reference to departments in the Person Model is now the DepartmentId, which is set whenever PersonViewModel.Department is updated.

A useful aide-memoire is to consider a Model class as being the data, the whole data, and nothing but the data – anything else goes into its corresponding WVM class.

As with the Model classes, I tend to break these WVMs out into a separate library project, so they they can be used throughout a solution. Again, there are several layers of WVM base classes. The full reasoning behind this will be explained in future posts. To ensure type safety, the base class smViewModelBase<T> is generic – each WVM is tied to a specific Model type. Unlike the Model library, I don’t use Fody.PropertyChanged with any ViewModels classes, as there is often further activity required when their properties are set.

Each WVM exposes its Model for direct data binding. Defining data binding now requires a little extra care – some will target properties of the WVM itelf, and some those of its Model.

As the Model already implements INotifyPropertyChanged, creating properties in the WVMs that are just a facade on top of the Model …

public class PersonViewModel : smViewModelBase<Person>
{
    public string FirstName
    {
        get { return Model.FirstName; }
        set {
                Model.FirstName = value;
                RaisePropertyChanged(nameof(FirstName));
            }
    }
    ...
}

… serves no purpose, other than to bloat the codebase.

PersonViewModel

This uses AddModelPropertyDependency() from the base class to keep the DisplayName property in sync with the Model.

    public class PersonViewModel : smViewModelBase<Person>, IComparable<PersonViewModel>
    {
        public PersonViewModel()
        {
            AddModelPropertyDependency(nameof(Model.FirstName), nameof(DisplayName));
            AddModelPropertyDependency(nameof(Model.LastName), nameof(DisplayName));
        }

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

        private bool _isSelected;

        public bool IsSelected
        {
            get { return _isSelected; }
            set { Set(nameof(IsSelected), ref _isSelected, value); }
        }

        private DepartmentViewModel _departmentVm;

        public DepartmentViewModel DepartmentVm
        {
            get { return _departmentVm; }
            set
            {
                var oldDepartmentVm = DepartmentVm;

                if (Set(nameof(DepartmentVm), ref _departmentVm, value))
                {
                    // stop a potential circular loop
                    if (oldDepartmentVm != null && oldDepartmentVm.HasPersonVm(this))
                        oldDepartmentVm.RemovePerson(this);

                    Model.DepartmentId = value?.Model?.Id ?? 0;

                    // stop a potential circular loop
                    if (!DepartmentVm.HasPersonVm(this))
                        DepartmentVm.AddPerson(this);
                }
            }
        }

        // sort any PersonViewModel without a Department last in the list
        private int SortingDepartmentId => DepartmentVm?.Model?.Id ?? int.MaxValue;

        public int CompareTo(PersonViewModel other)
        {
            var result = SortingDepartmentId.CompareTo(other.SortingDepartmentId);
            if (result == 0)
                result = other.Model.IsManager.CompareTo(Model.IsManager); // sort true before false
            if (result == 0)
                result = string.Compare(Model.LastName, other.Model.LastName, StringComparison.InvariantCultureIgnoreCase);
            return result;
        }
    }

DepartmentViewModel

The DepartmentViewModel maintains a collection of all the people that belong to this department.

    public class DepartmentViewModel: smViewModelBase<Department>
    {
        private readonly HashSet<PersonViewModel> _peopleVmSet = new HashSet<PersonViewModel>();

        public IEnumerable<PersonViewModel> PeopleVms
        {
            get
            {
                var result = _peopleVmSet.ToList();
                result.Sort();
                return result;
            }
        }

        public bool HasPersonVm(PersonViewModel personVm)
        {
            return _peopleVmSet.Contains(personVm);
        }

        public void AddPerson(PersonViewModel personVm)
        {
            if (_peopleVmSet.Add(personVm))
                RaisePropertyChanged(nameof(PeopleVms));

            // stop a potential circular loop
            if (personVm.DepartmentVm != this)
                personVm.DepartmentVm = this;
        }

        public bool RemovePerson(PersonViewModel personVm)
        {
            var result = _peopleVmSet.Remove(personVm);

            if (result)
                RaisePropertyChanged(nameof(PeopleVms));

            // stop a potential circular loop
            if (personVm.DepartmentVm == this)
                personVm.DepartmentVm = null;

            return result;
        }
    }

Note that the DepartmentViewModel does not directly expose its collection of people. This prevents a new person being added or removed, except via the AddPerson() or RemovePerson() methods, thus ensuring that people and departments stay in sync.

In the MainViewModel, the OnLoadData() now handles creation of the WVM classes from the Models.

    private void OnLoadData()
    {
        var departments = new List<Department>
        {
            new Department {Id = 1, Description = "I.T."},
            new Department {Id = 2, Description = "Accounts"},
            new Department {Id = 3, Description = "Sales"},
            new Department {Id = 4, Description = "Logistics"}
        };

        Departments = departments.Select(d => new DepartmentViewModel {Model = d}).ToList();

        var people = new List<Person>
        {
            new Person {Id = 1, DepartmentId = 1, FirstName = "Alan", LastName = "Jones", IsManager = false},
            new Person {Id = 2, DepartmentId = 1, FirstName = "Joseph", LastName = "Preston", IsManager = true},
            new Person {Id = 3, DepartmentId = 1, FirstName = "Stella", LastName = "Mcbride", IsManager = false},
            new Person {Id = 4, DepartmentId = 1, FirstName = "Branden", LastName = "Owens", IsManager = false},
            new Person {Id = 5, DepartmentId = 1, FirstName = "Leonard", LastName = "Marquez", IsManager = false},
            new Person {Id = 6, DepartmentId = 1, FirstName = "Colin", LastName = "Brady", IsManager = false},
            new Person {Id = 7, DepartmentId = 2, FirstName = "Callum", LastName = "Roberts", IsManager = true},
            new Person {Id = 8, DepartmentId = 2, FirstName = "Jillian", LastName = "Scott", IsManager = false},
            new Person {Id = 9, DepartmentId = 2, FirstName = "Calvin", LastName = "Moran", IsManager = false},
            new Person {Id = 10, DepartmentId = 3, FirstName = "Harlan", LastName = "Reid", IsManager = false},
            new Person {Id = 11, DepartmentId = 3, FirstName = "Felix", LastName = "Schroeder", IsManager = false},
            new Person {Id = 12, DepartmentId = 3, FirstName = "Joseph", LastName = "Smith", IsManager = true},
            new Person {Id = 13, DepartmentId = 3, FirstName = "Jasmine", LastName = "Emerson", IsManager = false},
            new Person {Id = 14, DepartmentId = 3, FirstName = "Lucas", LastName = "Edwards", IsManager = false},
            new Person {Id = 15, DepartmentId = 3, FirstName = "David", LastName = "Baxter", IsManager = false},
            new Person {Id = 16, DepartmentId = 4, FirstName = "Kane", LastName = "Foreman", IsManager = false},
            new Person {Id = 17, DepartmentId = 4, FirstName = "Laurel", LastName = "Curtis", IsManager = false},
            new Person {Id = 18, DepartmentId = 4, FirstName = "Lucy", LastName = "Tanner", IsManager = true},
            new Person {Id = 19, DepartmentId = 4, FirstName = "Christian", LastName = "Pittman", IsManager = false},
            new Person {Id = 20, DepartmentId = 4, FirstName = "Patricia", LastName = "Wilkinson", IsManager = false}
        };

        People = people.Select(p => new PersonViewModel {Model = p}).ToList();

        foreach (var personVm in People)
        {
            var departmentVm = Departments.FirstOrDefault(d => d.Model.Id == personVm.Model.DepartmentId);
            departmentVm?.AddPerson(personVm);
        }

        People.Sort();
        CurrentPerson = People.FirstOrDefault();
    }

In my next post, I’ll look at refactoring the main ViewModel.

Don’t forget that all of the code samples for this blog are available on Github, along with my own personal C# / WPF library.

Leave a Reply

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