MVVM Cookbook

Credit: fcamuso

Summary

  • Start a WPF framework project
  • Start editing the MainWindow XAML file
      <StackPanel>
          <Button Content="Aggiungi"/>
          <ListBox />
      </StackPanel>
    
  • Remove the startup URI in the App.xaml and override the startup method. Set the DataContext to MainWindowViewModel (for Binding):
    public partial class App : Application
      {
          protected override void OnStartup(StartupEventArgs e)
          {
              base.OnStartup(e);
    
              this.MainWindow = new MainWindow();
              this.MainWindow.DataContext = new ViewModels.MainWindowViewModel();
              this.MainWindow.Show();
          }
      }
    
  • Add a folder called ViewModels with 3 classes: BaseView, RelayCommand and a MainWindowViewModel class that will control the MainWindow view.
  • BaseView derived from INotifyPropertyChanged. This will let the Listbox to listen for changes in its ItemSource:
    using System.ComponentModel;
    using System.Runtime.CompilerServices;
    
      public abstract class BaseViewModel : INotifyPropertyChanged
      {
          public event PropertyChangedEventHandler PropertyChanged;
          protected void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
          {
              PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
          }
          protected bool IsDesignMode
          {
              get { return DesignerProperties.GetIsInDesignMode(new System.Windows.DependencyObject()); }
          }
      }
    
  • RelayCommand derived from ICommand:
using System;
using System.Windows.Input;

class RelayCommand : ICommand
    {
        public event EventHandler CanExecuteChanged;

        private Action<object> executeMethod;
        private Predicate<object> canExecuteMethod;

        public RelayCommand(Action<object> Execute, Predicate<object> CanExecute)
        {
            executeMethod = Execute;
            canExecuteMethod = CanExecute;
        }

        public RelayCommand(Action<object> Execute) : this(Execute, null)
        { }

        public bool CanExecute(object parameter)
        { return (canExecuteMethod == null) ? true : canExecuteMethod.Invoke(parameter); }

        public void Execute(object parameter)
        { executeMethod.Invoke(parameter); }

        public void RaiseCanExecuteChanged()
        {
            CanExecuteChanged?.Invoke(this, EventArgs.Empty);
        }
    }
  • To get the autocompletion add:
    xmlns:vm="clr-namespace:DeleteButtonAdd.ViewModels"
    d:DataContext="{d:DesignInstance vm:MainWindowViewModel, IsDesignTimeCreatable=True}"
    

    Replace DeleteButtonAdd with your Solution name.

  • Clean the solution and rebuild to update the XAML view
  • Create an instance of the RelayCommand in the MainWindowViewModel constructor:
using System.Windows;

    class MainWindowViewModel: BaseViewModel
    {
        public RelayCommand AggiungiClick { get; set; }

        public MainWindowViewModel()
        {
            AggiungiClick = new RelayCommand(AggiungiMethod);            
        }

        private void AggiungiMethod(object param)
        {
            MessageBox.Show("Ciao");
        }
    }
  • Bind the button Click command to the RelayCommand public property
<Button Content="Aggiungi" Command="{Binding Path=AggiungiClick}"/>
  • Add an ObservableCollection
using System.Collections.ObjectModel;

    private ObservableCollection<string> _txtSaluto;
    
    public ObservableCollection<string>  TextSaluto
        {
            get { return _txtSaluto; }
            set
            {
                _txtSaluto = value;
                NotifyPropertyChanged();//no need to specify the property. At runtime the compiler will use TextSaluto
            }
        }
  • Create a new instance of the ObservableCollection in the Button action method:
    private void AggiungiMethod(object param)
        {
            TextSaluto = new ObservableCollection<string>(); //will be overwritten every time the button is pressed.  
            TextSaluto.Add("ciao");
        }

or in the MainWindowViewModel constructor:

public MainWindowViewModel()
        {
            TextSaluto = new ObservableCollection<string>(); //will keep all the values
            AggiungiClick = new RelayCommand(AggiungiMethod);
        }
  • Bind the ListBox content to the ObservableCollection:
<ListBox ItemsSource="{Binding Path=TextSaluto}"/>

WPF & MVVM in C# ITA - 03: Hello World

Create a window with inputs for name and surname and a button to display a greeting.

  • Nest two stack panels for better layout control
  • x: Refers to the xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml” namespace
  • mc:Ignorable=”d” Specifies which XML namespace prefixes encountered in a markup file may be ignored by a XAML processor.
  • Click property will add a button Click event in xaml.cs
  • x:FiledModifier = “private” makes the fields private

WPF & MVVM in C# ITA - 4: Data Service

  • Remove Name text box and change Surname text box to combobox
<ComboBox x:Name="cboxSurname" Width="150" Margin="3,3,10,3"/>
  • Add a class (Person) storing Name and Surname
    public class Person
    {
        public string name { get; set; }
        public string surname { get; set; }

    }
  • Add a (static) data source PeopleService defining a list of Person
      class PeopleService
      {
          private List<Person> _people = new List<Person>();
          public PeopleService()
          {
              _people.Add(new Person() { name = "Mario", surname = "Rossi" });
              _people.Add(new Person() { name = "Peter", surname = "Pan" });
              _people.Add(new Person() { name = "Susan", surname = "Don" });
          }
          public IList<Person> People => _people;
      }
    
  • Combobox is linked to an abstract class (model) not to the data source directly -> easier to substitute the data source
  • Create an instance of PeopleService and assign it to the comboBox.ItemSource. Specify the property to be displayed using DisplayMemberPath:
    PeopleService ps = new PeopleService();
              cboxSurname.ItemsSource = ps.People;
              cboxSurname.DisplayMemberPath = "surname";
    
  • In the Click event, cast the comboBox.SelectedItem to a Person object in order to access its properties (Name and Surname)
    private void SalutaMi()
          {
              if (cboxSurname.SelectedItem != null)
              {
                  Person chosenPerson = cboxSurname.SelectedItem as Person;
                  result.Text = String.Format("Ciao {0} {1}", chosenPerson.name, chosenPerson.surname);
              }
          }
    

  • To make our code independent from a class named PeopleService -> Make MainWindow independent from the Service used

  • Add an interface to the PeopleService class

    public interface IPeopleService
    {
        IList<Person> People { get; } //declare a read only property. 
        //Will be made available in the class implementing the interface
    }
  • In the MainWindow change the field from PeopleService to IPeopleService
  • Add a IPeopleService parameter to the MainWindow constructor -> the Service will then be created outside the MainWindow class
    private Models.IPeopleService peopleService = null; 

    public MainWindow(Models.IPeopleService _peopleService)
    {
        InitializeComponent();       
        peopleService = _peopleService;
        comboSurname.ItemsSource = _peopleService.People;
        comboSurname.DisplayMemberPath = "Surname";
    }
  • MainWindow does not know the nature of the class we are passing, it only needs to respect the IPeopleService interface (does not have to look like something, but it has to behave like something)
  • The PeopleService class must be created in the App class
    private void Application_Startup(object sender, StartupEventArgs e)
    {
        MainWindow mainWindow = new MainWindow(new Models.PeopleService());
        mainWindow.Show();
    }
  • in the App.xaml, replace the StartupURI with
Startup="Application_Startup">
  • You can also remove the Startup property and change the code as:
    protected override void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);
        MainWindow mainWindow = new MainWindow(new Models.PeopleService());
        MainWindow.Show();
    }

WPF & MVVM in C# ITA - 5: Binding & ViewModel

DataContext property

  • Default source of your bindings
  • There’s no default source for the DataContext property
  • Inherited down through the control hierarchy
  • Replaces the MainWindow IPersoneService property
    public MainWindow(Models.IPeopleService _peopleService)
    {
        InitializeComponent();
        DataContext = _peopleService;
    }

In the MainWindow.xaml, add the binding to PeopleService.People and its property Surname:

    <ComboBox x:Name="comboSurname" Width="120" Margin="8,0"
        ItemsSource="{Binding People}" DisplayMemberPath="Surname"/> 
  • To make the best use of Data Binding let’s divide our code in 3 parts:
    • Model class -> Data Source (i.e. PeopleService)
    • ViewModel class -> combines functions that connect Model class and View
    • View -> xaml layout

Create a folder called ViewModels and a class called MainWindowViewModel.cs and copy the reference to the Service from the MainWindow.xaml.cs:

public class MainWindowViewModel
    {
        private Models.IPeopleService _peopleService = null;
        
        public MainWindowViewModel(Models.IPeopleService peopleService)
        {
            _peopleService = peopleService;
        }
        
        public IList<Models.Person> People => _peopleService.People;
        //properties for binding
        public Models.Person ChosenPerson { get; set; } 
        public string TextSaluto;
    }

Let’s also move the method associated with the button Click:

    public void SalutaMi()
    {
        if (ChosenPerson != null)
        {
            TextSaluto = String.Format("Ciao {0} {1}", ChosenPerson.Name, ChosenPerson.Surname);
        }
    }

Let’s now isolate the MainWindow from the Service:

    //public MainWindow(Models.IPeopleService peopleService)
    public MainWindow(ViewModels.MainWindowViewModel vm) //not great to link it to a specific ViewModel, can be made more generic
    {
        InitializeComponent();
        DataContext = vm;
    }

and bind the comboBox selected item and the text block Text to their respective properties in the MainWindowViewModel:

  <ComboBox x:Name="comboSurname" Width="120" Margin="8,0"
       ItemsSource="{Binding People}" 
       DisplayMemberPath="Surname"
       SelectedItem="{Binding Path=ChosenPerson}"/>
  <TextBlock x:Name="tboxResult"
       x:FieldModifier="private"
       Text="{Binding Path = TextSaluto}"/>

then bind the click function in the xaml.cs:

    private void BtnSalutami_Click(object sender, RoutedEventArgs e)
    {
        (DataContext as ViewModels.MainWindowViewModel).SalutaMi();
    }

and finally update the App.xaml.cs

    protected override void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);
        Models.PeopleService ps = new Models.PeopleService();
        MainWindow mainWindow = new MainWindow(new ViewModels.MainWindowViewModel(ps));
        mainWindow.Show();
    }

We could have imported the HelloWpf.Models and HelloWpf.ViewModels namespaces to simplify the syntax.

If we run the code, the View does not know that the string txtSaluto has been updated -> we need to use INotifyPropertyChanged:

  • Import the namespace System.ComponentModel

  • Make the class MainWindowViewModel inherit from INotifyPropertyChanged

public class MWViewModel : INotifyPropertyChanged
  • Add an event PropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
  • Update the setter of the TextSaluto property with an event invoke
        private string _txtSaluto;
        public string TextSaluto
        {
            get { return _txtSaluto; }
            set
            {
                _txtSaluto = value;
                //PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("TextSaluto")); not good practice to use a string for the property name
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(TextSaluto))); //better to use nameof(Property)
            }
        }

WPF & MVVM in C# ITA - 6: ICommand (RelayCommand Class)

  • Completely isolate the Click function -> move it from MainWindow to the ViewModel (ICommand)

In the MainWindowViewModel class:

  • Collect some components into an abstract class
    public abstract class BaseViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        protected void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); //better than using the string for refactoring
        }
    }
  • Make MainWindowViewModel implement BaseViewModel and change the function in the TextSaluto property setter:
          public string TextSaluto
          {
              get { return _txtSaluto; }
              set
              {
                  _txtSaluto = value;
                  NotifyPropertyChanged();//no need to specify the property. At runtime the compiler will use TextSaluto
              }
          }
    

if the argument of NotifyPropertyChanged is null the view will refresh all the controllers (for list use Observable collection)

NotifyPropertyChanged(null)
  • Isolate the View from all the events (i.e. Click, Mouse Up and Down can be left there)
    • import System.Windows.Input
    • create a RelayCommand class (in the MainWindowViewModel class)
    • add CanExecuteChanged event
    • add executeMethod action -> defines what happens when the button is clicked
    • addCanExecuteMethod predicate -> contains checks on the action. input object, return true/false
    • methods will be provided externally to the class constructor (delegates)
    public class RelayCommand : ICommand
        {
            public event EventHandler CanExecuteChanged;
            private Action<object> executeMethod;
            private Predicate<object> canExecuteMethod;
        }

Then let’s add two constructors:

    public RelayCommand(Action<object>Execute, Predicate<object> CanExecute)
    {
        canExecuteMethod = CanExecute;
        executeMethod = Execute;
    }

This first one checks the active state of the button. The second one will leave the button always active (clickable).

We then need to public methods to invoke the private members:

public bool CanExecute(object parameter)
        {
            return (canExecuteMethod == null) ? true : canExecuteMethod.Invoke(parameter);
        }

If the button is always clickable, return true. Else (:) return the result of the method invoked.

Then let’s add the Click method:

        public void Execute(object parameter)
        {
            executeMethod.Invoke(parameter);
        }

And the method that will be called whenever the button condition (active/inactive) has to be checked:


        public void RaiseCanExecuteChanged()
        {
            CanExecuteChanged?.Invoke(this, EventArgs.Empty);
        }

Let’s now create a property to hold an instance of the RelayCommand in the MainWindowViewModel:

public ViewModels.RelayCommand SalutaCommand { get; private set; } //private set -> read only

and an instance of the class using two new methods:

        SalutaCommand = new RelayCommand(salutaMethod, salutaExec);
        
        private void salutaMethod(object obj)
        {
            SalutaMi();
        }

        private bool salutaExec(object obj)
        {
            return ChosenPerson != null;
        }

or using lambda syntax:

SalutaCommand = new RelayCommand(param => SalutaMi(), param => ChosenPerson != null);

To check the button status (clickable/not clickable) when something is selected in the comboBox we need to adjust the setter:

    private Models.Person _ChosenPerson = null;

    public Models.Person ChosenPerson
    {
        get { return _ChosenPerson; }

        set
        {
            if (_ChosenPerson == value) return;//avoid to change value if the element chosen in the combobox is the same
            _ChosenPerson = value; //save the value in the private property
            NotifyPropertyChanged(); //if the changes happened outside the View
            SalutaCommand.RaiseCanExecuteChanged();
        }
    }

Let’s finally update the binding in the button in the MainWindow.xaml and remove the Click event in the MainWindow.xaml.cs:

<Button Command="{Binding Path=SalutaCommand}" Content="SalutaMi"/>

WPF & MVVM in C# ITA - 7: AppBase - DesignInstance

Project organization

  • Create a Models folder
  • Create a ViewModels folder
  • Create a Views folder

Move MainWindow.xaml in the Views folder and rename it MainWindowView.xaml

Change the namespace and class to:

x:Class="WpfApp1.Views.MainWindowView"
xmlns:local="clr-namespace:WpfApp1.Views"

And in the code behind:

amespace WpfApp1.Views
{
    public partial class MainWindowView : Window
    {
        public MainWindowView()
        {
            InitializeComponent();
        }
   }
}

Then update the App.xaml.cs to launch the MainWindowView (using the static property MainWindow):

    public partial class App : Application
    {
        protected override void OnStartup(StartupEventArgs e)
        {
            base.OnStartup(e);
            this.MainWindow = new Views.MainWindowView();
            this.MainWindow.Show();
        }
    }

In the ViewModels folder let’s add 3 classes:

  • BaseViewModel
  • MainWindowViewModel
  • RelayCommand

In the BaseViewModel paste the code created before and a new property IsDesignMode:

    public abstract class BaseViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        protected void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
        protected bool IsDesignMode
        {
            get { return DesignerProperties.GetIsInDesignMode(new System.Windows.DependencyObject()); }
        }
    }

Let’s make the MainWindowViewModel implements the BaseViewModel.

class MainWindowViewModel : BaseViewModel

In the App.xaml.cs set the DataContext to and instance of the MainWindowViewModel

    this.MainWindow = new Views.MainWindowView();
        this.MainWindow.DataContext = new ViewModels.MainWindowViewModel();
    this.MainWindow.Show();

In doing this the View does not have any link to the ViewModel.

In order to take advantage of the intellisense

  • comment the ViewModel in the app.xaml.cs

csharp this.MainWindow = new Views.MainWindowView(); //this.MainWindow.DataContext = new ViewModels.MainWindowViewModel(); this.MainWindow.Show();

  • add a namespace reference to ViewModels in the MainWindowView.xaml
xmlns:vm="clr-namespace:WpfApp1.ViewModels"
  • add a new property in the MainWindowViewModel
    public String myTitle { get; set; }
    public MainWindowViewModel()
    {
        myTitle = "My Title";
    }

We have again introduced a link between the View and the ViewModel! Better would be to add a fictitious DataContext only valid during the Design phase:

Reactivate the DataContext in the App.xaml.cs:

this.MainWindow.DataContext = new ViewModels.MainWindowViewModel();

and in the MainWindowView replace the with:

d:DataContext="{d:DesignInstance vm:MainWindowViewModel, IsDesignTimeCreatable=True}"

[WPF & MVVM in C# ITA - 8: Binding applications(https://youtu.be/bJNEQAXvfWM)

Written on April 29, 2019