Model, View, ViewModel (MVVM)

  1. Overview
    1. MVVM is a design pattern often used in UWP apps that makes use of data binding. Composed of three parts:
      1. Model - Same as MVC: the data, state, and business logic
      2. View - UI; binds the observable variables and actions exposed by the ViewModel to the UI
      3. ViewModel - Wraps the Model and prepares observable data needed by the View; manipulates the Model due to actions in the View; not tied to the View

      MVVM diagram

    2. Example
      1. The Model implements Theater and Movie classes for managing a theater's movies
      2. The ViewModel exposes properties that wrap the Theater and Movie classes and are bound to the View
      3. The user enters a new movie into the View, and the View adds the new movie to the ViewModel
      4. Changes to the ViewModel cause the ViewModel to update the Theater class
      5. Changes to the ViewModels are automatically propogated to the View because of data binding, so the new movie is automatically shown in the UI
    3. Advantages:
      1. Separation of concerns
      2. MVVM is easier to test than MVC because ViewModel can be tested independent of the View, but Controller cannot be
      3. Reduces the amount of code necessary to connect the View and Model
    4. Disadvantages:
      1. Overkill for simple projects
      2. Because Model data is duplicated in the ViewModel, apps with lots of data may consume considerable amounts of memory
    5. Minimal MVVM UWP example on Microsoft blog (my example is even more minimal)
    6. See Microsoft article Data binding and MVVM for more information
  2. Models
    1. Classes that know nothing of the ViewModels or Views
    2. Usually stored in a project directory called "Models"
    3. Movie
      public class Movie
      {
      	public string Title { get; set; }
      	public string Rating { get; set; }
      }
      
    4. Theater
      public class Theater
      {
      	public string Name { get; set; }
      	public List<Movie> Movies { get; set; }
      
      	public Theater()
      	{
      		Name = "Rialto";
      		
      		// Load some initial movies
      		Movies = new List<Movie>
      		{
      			new Movie { Title = "Wonder Woman", Rating = "PG-13" },
      			new Movie { Title = "Spider-Man", Rating = "PG-13" },
      			new Movie { Title = "Justice League", Rating = "PG-13" }
      		};
      	}
      }
      
  3. ViewModels
    1. Implement INotifyPropertyChanged so the View is notified when the Model changes
    2. Usually stored in a project directory called "ViewModels"
    3. Encapsulates Models
    4. MovieViewModel
      public class MovieViewModel : INotifyPropertyChanged
      {
      	public event PropertyChangedEventHandler PropertyChanged;
      
      	private Movie movie;
      
      	public MovieViewModel()
      	{
      		this.movie = new Movie();
      	}
      
      	public string Title
      	{
      		get { return movie.Title; }
      
      		set
      		{
      			movie.Title = value;
      			NotifyPropertyChanged();
      		}
      	}
      
      	public string Rating 
      	{
      		get	{ return movie.Rating; }
      
      		set
      		{
      			movie.Rating = value;
      			NotifyPropertyChanged();
      		}
      	}
      
      	private void NotifyPropertyChanged([CallerMemberName] String property = "")
          {
      		// Notify any controls bound to the ViewModel that the property changed
      		PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property));
          }
      }
      
    5. TheaterViewModel uses ObservableCollection that provides notifications to ListView when items are added or removed
      public class TheaterViewModel : INotifyPropertyChanged
      {
      	public event PropertyChangedEventHandler PropertyChanged;
      
      	private Theater theater;
      
      	public ObservableCollection<MovieViewModel> Movies { get; set; }
      
      	public string Name
      	{
      		get { return theater.Name; }
      		set
      		{
      			theater.Name = value;
      			OnPropertyChanged(this, new PropertyChangedEventArgs("Name"));
      		}
      	}
      
      	public TheaterViewModel()
      	{
      		this.theater = new Theater();
      
      		Movies = new ObservableCollection<MovieViewModel>();
      
      		// Create ViewModels for each Movie
      		foreach (var movie in theater.Movies)
      		{
      			var newMovie = new MovieViewModel { Title = movie.Title, Rating = movie.Rating };
      			newMovie.PropertyChanged += OnPropertyChanged;
      			Movies.Add(newMovie);
      		}
      	}
      
      	private void OnPropertyChanged(object sender, PropertyChangedEventArgs e)
      	{
      		// Theater name or MovieViewModel changed, so let UI know 
      		PropertyChanged?.Invoke(sender, e);
      	}
      }
      
  4. View
    1. XAML uses x:Bind markup extension, which is converted to code at compile time and sets a property to the specified markup
    2. MainPage.xaml
      <Page
          x:Class="UwpMovieMvvm.MainPage"
          xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
          xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      	xmlns:local="using:UwpMovieMvvm.ViewModels"
          xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
          xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
          mc:Ignorable="d">
      
          <StackPanel Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
      		<TextBox x:Name="titleTextBox" Text="{x:Bind Movie.Title, Mode=TwoWay}" />
      		<TextBox x:Name="ratingTextBox" Text="{x:Bind Movie.Rating, Mode=TwoWay}" />
      		<Button x:Name="addButton" Content="Add Movie" Click="addButton_Click" />
      				
      		<ListView x:Name="movieListView" Height="155" Width="200" HorizontalAlignment="Left"
      						  ItemsSource="{x:Bind Theater.Movies, Mode=OneWay}">
      			<ListView.ItemTemplate>
      				<DataTemplate x:DataType="local:MovieViewModel">
      					<StackPanel>
      						<TextBlock Text="{x:Bind Title, Mode=OneWay}" FontWeight="Bold"/>
      						<TextBlock Text="{x:Bind Rating, Mode=OneWay}"/>
      					</StackPanel>
      				</DataTemplate>
      			</ListView.ItemTemplate>
      		</ListView>				
      
      		<Button x:Name="deleteButton" Content="Delete Movie" Click="deleteButton_Click" />
          </StackPanel>
      </Page>
      
    3. Code-behind creates public properties for ViewModels so XAML can bind to them
      public sealed partial class MainPage : Page
      {
      	// XAML binds to these properties
      	public TheaterViewModel Theater { get; set; }
      	public MovieViewModel Movie { get; set; }
      
      	public MainPage()
      	{
      		this.InitializeComponent();
      
              // Create ViewModels
              Movie = new MovieViewModel();
              Theater = new TheaterViewModel();
      	}
      
      	private void addButton_Click(object sender, RoutedEventArgs e)
      	{
              // Add new movie to list
              Theater.Movies.Add(new MovieViewModel { 
                  Title = Movie.Title, Rating = Movie.Rating });
      	}
      	
      	private void deleteButton_Click(object sender, RoutedEventArgs e)
      	{
      		// Delete selected movie from ObservableCollection to update UI
      		if (movieListView.SelectedIndex > -1)
      		{
      			Theater.Movies.RemoveAt(movieListView.SelectedIndex);		
      		}
      	}
      }
      
    4. Note that adding and removing movies does not affect Theater model!
      private void addButton_Click(object sender, RoutedEventArgs e)
      {
      	// TODO: Create AddMovie() to add to Movies and to Theater model's movie list 		
      	Theater.AddMovie(new MovieViewModel
      	{
      		Title = Movie.Title,
      		Rating = Movie.Rating
      	});
      }
      
      private void deleteButton_Click(object sender, RoutedEventArgs e)
      {
      	// TODO: Create DeleteMovie() to remove from Movies and remove from Theater model's movie list
      	if (movieListView.SelectedIndex > -1)
      	{
      		Theater.DeleteMovie(movieListView.SelectedIndex);	
      	}
      }