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
- 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; }
}
}
- 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);
}
}
}
}
- 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 viaBinding="{Binding Id}". - On
Button, the command to respond to is bound viaCommand="{Binding AddCommand}". - In
TextBox, the search condition property is bound viaText="{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>
- 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();
}
}
}
}
- 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.
