Into WPF: Complete MVVM Example

Into WPF: Complete MVVM Example

Learning WPF without learning MVVM feels like missing the soul. So what is MVVM? Why learn MVVM? This article uses a simple CRUD example to briefly introduce the basic knowledge of MVVM and how to develop MVVM architecture-based programs. For learning and sharing purposes only, please point out any shortcomings.

Last updated 2/21/2022 1:43 PM
小六公子
12 min read
Category
WPF
Topic
WPF MVVM Framework Prism Series
Tags
.NET WPF MVVM Architecture Design

Learning WPF without learning MVVM feels like missing its soul. So what is MVVM? Why should we learn MVVM? This article uses a simple CRUD (Create, Read, Update, Delete) example to briefly explain the basics of MVVM and how to develop applications using the MVVM architecture. It is only for learning and sharing. If there are any shortcomings, please feel free to point them out.

What is MVVM?

MVVM stands for Model-View-ViewModel. Essentially, it is an improved version of MVC (Model-View-Controller). That is, Model-View-ViewModel. The definitions are as follows:

  • [Model] refers to the data passed from the backend.
  • [View] refers to the page you see.
  • [ViewModel] is the core of the MVVM pattern, acting as a bridge between View and Model. It operates in two directions:
    • One is to transform the [Model] into the [View], i.e., convert the data from the backend into the page you see. The implementation method is: data binding.
    • The other is to transform the [View] into the [Model], i.e., convert the page you see into backend data. The implementation method is: DOM event listening. When both directions are implemented, we call it two-way data binding.

The MVVM schematic is shown below:

Installing the MvvmLight Plugin

Right-click on the project name --> Manage NuGet Packages --> Search for MvvmLight --> Install. As shown below:

When the license acceptance window appears, click [Accept] as shown below:

After MvvmLight is successfully installed, it automatically references the required third-party libraries and generates sample content by default. Some unnecessary items need to be deleted, as shown below:

MVVM Example Screenshot

It mainly implements basic CRUD (Create, Read, Update, Delete) operations on data using MVVM, as shown below:

MVVM Development Steps

  1. Create the Model Layer

In this example, we mainly CRUD student information, so we create a Student model class as shown below:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace WpfApp3.Models
{
    /// <summary>
    /// Student class
    /// </summary>
    public class Student
    {
        /// <summary>
        /// Unique identifier
        /// </summary>
        public int Id { get; set; }

        /// <summary>
        /// Student name
        /// </summary>
        public string Name { get; set; }

        /// <summary>
        /// Age
        /// </summary>
        public int Age { get; set; }

        /// <summary>
        /// Class
        /// </summary>
        public string Classes { get; set; }
    }
}
  1. Create the DAL Layer

To simplify the example, simulate database operations and build basic data as shown below:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using WpfApp3.Models;

namespace WpfApp3.DAL
{
    public class LocalDb
    {
        private List<Student> students;

        public LocalDb() {
            init();
        }

        /// <summary>
        /// Initialize data
        /// </summary>
        private void init() {
            students = new List<Student>();
            for (int i = 0; i < 30; i++)
            {
                students.Add(new Student()
                {
                    Id=i,
                    Name=string.Format("Student{0}",i),
                    Age=new Random(i).Next(0,100),
                    Classes=i%2==0?"Class 1":"Class 2"
                });
            }
        }

        /// <summary>
        /// Query data
        /// </summary>
        /// <returns></returns>
        public List<Student> Query()
        {
            return students;
        }

        /// <summary>
        /// Query by name
        /// </summary>
        /// <param name="name"></param>
        /// <returns></returns>
        public List<Student> QueryByName(string name)
        {
            return students.Where((t) => t.Name.Contains(name)).ToList();//FindAll((t) => t.Name.Contains(name));
        }

        public Student QueryById(int Id)
        {
            var student = students.FirstOrDefault((t) => t.Id == Id);
            if (student != null)
            {
                return new Student() {
                    Id=student.Id,
                    Name=student.Name,
                    Age=student.Age,
                    Classes=student.Classes
                };
            }
            return null;
        }


        /// <summary>
        /// Add student
        /// </summary>
        /// <param name="student"></param>
        public void AddStudent(Student student)
        {
            if (student != null)
            {
                students.Add(student);
            }
        }

        /// <summary>
        /// Delete student
        /// </summary>
        /// <param name="Id"></param>
        public void DelStudent(int Id)
        {
            var student = students.FirstOrDefault((t) => t.Id == Id); //students.Find((t) => t.Id == Id);
            if (student != null)
            {
                students.Remove(student);
            }

        }
    }


}
  1. Create the View Layer

The View layer interacts with the user, displaying data and responding to events. In this example, the View layer mainly consists of a data query display page and an add/edit page.

In the View layer, commands and data are bound.

  • In DataGridTextColumn, the column property to display is bound via Binding="{Binding Id}".
  • On Button, the command to respond to is bound via Command="{Binding AddCommand}".
  • In TextBox, the search condition property is bound via Text="{Binding Search}".

Data display window, as shown below:

<Window
  x:Class="WpfApp3.MainWindow"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
  xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
  xmlns:local="clr-namespace:WpfApp3"
  mc:Ignorable="d"
  Title="MainWindow"
  Height="450"
  Width="800"
>
  <Grid>
    <Grid.RowDefinitions>
      <RowDefinition Height="80"></RowDefinition>
      <RowDefinition Height="*"></RowDefinition>
    </Grid.RowDefinitions>
    <StackPanel
      Orientation="Horizontal"
      Grid.Row="0"
      Margin="5"
      VerticalAlignment="Center"
    >
      <TextBlock Text="Name:" Margin="10" Padding="5"></TextBlock>
      <TextBox
        x:Name="sname"
        Text="{Binding Search}"
        Width="120"
        Margin="10"
        Padding="5"
      ></TextBox>
      <button
        x:Name="btnQuery"
        Content="Search"
        Margin="10"
        Padding="5"
        Width="80"
        Command="{Binding QueryCommand}"
      ></button>
      <button
        x:Name="btnReset"
        Content="Reset"
        Margin="10"
        Padding="5"
        Width="80"
        Command="{Binding ResetCommand}"
      ></button>
      <button
        x:Name="btnAdd"
        Content="Create"
        Margin="10"
        Padding="5"
        Width="80"
        Command="{Binding AddCommand}"
      ></button>
    </StackPanel>
    <DataGrid
      x:Name="dgInfo"
      Grid.Row="1"
      AutoGenerateColumns="False"
      CanUserAddRows="False"
      CanUserSortColumns="False"
      Margin="10"
      ItemsSource="{Binding GridModelList}"
    >
      <DataGrid.Columns>
        <DataGridTextColumn
          Header="Id"
          Width="100"
          Binding="{Binding Id}"
        ></DataGridTextColumn>
        <DataGridTextColumn
          Header="Name"
          Width="100"
          Binding="{Binding Name}"
        ></DataGridTextColumn>
        <DataGridTextColumn
          Header="Age"
          Width="100"
          Binding="{Binding Age}"
        ></DataGridTextColumn>
        <DataGridTextColumn
          Header="Class"
          Width="100"
          Binding="{Binding Classes}"
        ></DataGridTextColumn>
        <DataGridTemplateColumn Header="Actions" Width="*">
          <DataGridTemplateColumn.CellTemplate>
            <DataTemplate>
              <StackPanel
                Orientation="Horizontal"
                VerticalAlignment="Center"
                HorizontalAlignment="Center"
              >
                <button
                  x:Name="edit"
                  Content="Edit"
                  Width="60"
                  Margin="3"
                  Height="25"
                  CommandParameter="{Binding Id}"
                  Command="{Binding DataContext.EditCommand, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=DataGrid}}"
                ></button>
                <button
                  x:Name="delete"
                  Content="Delete"
                  Width="60"
                  Margin="3"
                  Height="25"
                  CommandParameter="{Binding Id}"
                  Command="{Binding DataContext.DeleteCommand, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=DataGrid}}"
                ></button>
              </StackPanel>
            </DataTemplate>
          </DataGridTemplateColumn.CellTemplate>
        </DataGridTemplateColumn>
      </DataGrid.Columns>
    </DataGrid>
  </Grid>
</Window>

Add/Edit page, as shown below:

<Window
  x:Class="WpfApp3.Views.StudentWindow"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
  xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
  xmlns:local="clr-namespace:WpfApp3.Views"
  mc:Ignorable="d"
  Title="StudentWindow"
  Height="440"
  Width="500"
  AllowsTransparency="False"
  WindowStartupLocation="CenterScreen"
  WindowStyle="None"
>
  <Grid>
    <Grid.RowDefinitions>
      <RowDefinition Height="60"></RowDefinition>
      <RowDefinition></RowDefinition>
      <RowDefinition Height="60"></RowDefinition>
    </Grid.RowDefinitions>
    <TextBlock FontSize="30" Margin="10">Edit Student Information</TextBlock>
    <StackPanel Grid.Row="1" Orientation="Vertical">
      <TextBlock FontSize="20" Margin="10" Padding="5">Name</TextBlock>
      <TextBox
        x:Name="txtName"
        FontSize="20"
        Padding="5"
        Text="{Binding Model.Name}"
      ></TextBox>
      <TextBlock FontSize="20" Margin="10" Padding="5">Age</TextBlock>
      <TextBox
        x:Name="txtAge"
        FontSize="20"
        Padding="5"
        Text="{Binding Model.Age}"
      ></TextBox>
      <TextBlock FontSize="20" Margin="10" Padding="5">Class</TextBlock>
      <TextBox
        x:Name="txtClasses"
        FontSize="20"
        Padding="5"
        Text="{Binding Model.Classes}"
      ></TextBox>
    </StackPanel>
    <StackPanel
      Grid.Row="2"
      Orientation="Horizontal"
      HorizontalAlignment="Right"
    >
      <button
        x:Name="btnSave"
        Content="Save"
        Margin="10"
        FontSize="20"
        Width="100"
        Click="btnSave_Click"
      ></button>
      <button
        x:Name="btnCancel"
        Content="Cancel"
        Margin="10"
        FontSize="20"
        Width="100"
        Click="btnCancel_Click"
      ></button>
    </StackPanel>
  </Grid>
</Window>
  1. Create the ViewModel Layer

The ViewModel layer is the core of MVVM, acting as a bridge between the View and Model. The ViewModel needs to inherit from the GalaSoft.MvvmLight.ViewModelBase base class.

In the ViewModel, properties implement data binding, and commands implement user interaction responses. As shown below:

using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Windows;
using WpfApp3.DAL;
using WpfApp3.Models;
using WpfApp3.Views;

namespace WpfApp3.ViewModel
{
    /// <summary>
    ///
    /// </summary>
    public class MainViewModel : ViewModelBase
    {
        #region Properties and Constructor

        private LocalDb localDb;

        private ObservableCollection<Student> gridModelList;

        public ObservableCollection<Student> GridModelList
        {
            get { return gridModelList; }
            set
            {
                gridModelList = value;
                RaisePropertyChanged();
            }
        }

        /// <summary>
        /// Search condition
        /// </summary>
        private string search;

        public string Search
        {
            get { return search; }
            set
            {
                search = value;
                RaisePropertyChanged();
            }
        }


        /// <summary>
        ///
        /// </summary>
        public MainViewModel()
        {
            localDb = new LocalDb();
            QueryCommand = new RelayCommand(this.Query);
            ResetCommand = new RelayCommand(this.Reset);
            EditCommand = new RelayCommand<int>(this.Edit);
            DeleteCommand = new RelayCommand<int>(this.Delete);
            AddCommand = new RelayCommand(this.Add);
        }

        #endregion

        #region Commands

        /// <summary>
        /// Query command
        /// </summary>
        public RelayCommand QueryCommand { get; set; }

        /// <summary>
        /// Reset command
        /// </summary>
        public RelayCommand ResetCommand { get; set; }

        /// <summary>
        /// Edit
        /// </summary>
        public RelayCommand<int> EditCommand { get; set; }

        /// <summary>
        /// Delete
        /// </summary>
        public RelayCommand<int> DeleteCommand { get; set; }

        /// <summary>
        /// Add
        /// </summary>
        public RelayCommand AddCommand { get; set; }

        #endregion

        public void Query()
        {
            List<Student> students;
            if (string.IsNullOrEmpty(search))
            {
                students = localDb.Query();
            }
            else
            {
                students = localDb.QueryByName(search);
            }

            GridModelList = new ObservableCollection<Student>();
            if (students != null)
            {
                students.ForEach((t) =>
                {
                    GridModelList.Add(t);
                });
            }
        }

        /// <summary>
        /// Reset
        /// </summary>
        public void Reset()
        {
            this.Search = string.Empty;
            this.Query();
        }

        /// <summary>
        /// Edit
        /// </summary>
        /// <param name="Id"></param>
        public void Edit(int Id)
        {
            var model = localDb.QueryById(Id);
            if (model != null)
            {
                StudentWindow view = new StudentWindow(model);
                var r = view.ShowDialog();
                if (r.Value)
                {
                    var newModel = GridModelList.FirstOrDefault(t => t.Id == model.Id);
                    if (newModel != null)
                    {
                        newModel.Name = model.Name;
                        newModel.Age = model.Age;
                        newModel.Classes = model.Classes;
                    }
                    this.Query();
                }
            }
        }

        /// <summary>
        /// Delete
        /// </summary>
        /// <param name="Id"></param>
        public void Delete(int Id)
        {
            var model = localDb.QueryById(Id);
            if (model != null)
            {
                var r = MessageBox.Show($"Are you sure you want to delete [{model.Name}]?","Prompt",MessageBoxButton.YesNo);
                if (r == MessageBoxResult.Yes)
                {
                    localDb.DelStudent(Id);
                    this.Query();
                }
            }
        }

        /// <summary>
        /// Add
        /// </summary>
        public void Add()
        {
            Student model = new Student();
            StudentWindow view = new StudentWindow(model);
            var r = view.ShowDialog();
            if (r.Value)
            {
                model.Id = GridModelList.Max(t => t.Id) + 1;
                localDb.AddStudent(model);
                this.Query();
            }
        }
    }
}
  1. DataContext

After each layer is created, how are they connected? The answer is DataContext.

Query page context, as shown below:

namespace WpfApp3
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            MainViewModel viewModel = new MainViewModel();
            viewModel.Query();
            this.DataContext = viewModel;
        }
    }
}

Add/Edit page context, as shown below:

namespace WpfApp3.Views
{
    /// <summary>
    /// Interaction logic for StudentWindow.xaml
    /// </summary>
    public partial class StudentWindow : Window
    {
        public StudentWindow(Student student)
        {
            InitializeComponent();
            this.DataContext = new
            {
                Model = student
            };
        }

        private void btnSave_Click(object sender, RoutedEventArgs e)
        {
            this.DialogResult = true;
        }

        private void btnCancel_Click(object sender, RoutedEventArgs e)
        {
            this.DialogResult = false;
        }
    }
}

Summary

MVVM has the advantages of low coupling, reusability, testability, and independent development. The core elements are two:

  • Notification when properties change, enabling real-time data updates.
  • Commands serve as the bridge between user interactions and program data/algorithms.

Notes

This article serves as a simple introductory example of MVVM, aiming to spark discussion and mutual learning. If you are not very familiar with other WPF basics, you can refer to other blog posts.

Spring in Jade Pavilion · Since you left, I know not how far you've gone

Ouyang Xiu [Song Dynasty]

Since you left, I know not how far you've gone,
How dreary I feel, how sad and forlorn!
No letter from you now as farther you roam;
Wide water, silent fish — where to find you home?

At dead of night the bamboos beat Autumn's sad tune,
Every leaf sounds like grief for you, night and noon.
I seek for you on a lonely pillow in dreams,
But dreams won't come and the lamp's burnt out its beams.

Keep Exploring

Related Reading

More Articles