WPF中MVVM的基本使用
一、MVVM模式的基本概念
MVVM
(Model-View-ViewModel
)是一种设计模式,特别适用于WPF
(Windows Presentation Foundation
)应用程序开发。它将应用程序分为三个核心部分:模型(Model
)、视图(View
)和视图模型(ViewModel
),以实现关注点分离,提高代码的可维护性和可测试性。
MVVM模式包含三个主要部分:
- Model(模型) :代表应用程序的核心数据和业务逻辑。它通常包含数据结构、业务规则和数据访问代码。
Model
不依赖于UI
,是独立且可重用的组件。 - View(视图) :负责显示数据和接收用户输入。
View
通过数据绑定和命令与ViewModel
交互,而不直接访问Model
。View
通常是XAML
文件及其相关的代码隐藏文件。 - ViewModel(视图模型) :作为
View
和Model
之间的桥梁。它负责从Model
获取数据,并将这些数据提供给View
,同时处理用户在View
上的交互。ViewModel
通常实现通知机制(如INotifyPropertyChanged
接口),以便在数据变化时通知View进行更新。
在这里,再着重提一下:View
与ViewModel
的交互是通过命令与数据绑定来完成的。
通过命令,View
层可以发出操作请求到ViewModel
中让ViewModel
进行数据处理。
通过数据绑定,可以让ViewModel
把处理好的数据展示到View中。
MVVM的优点
- 低耦合:视图和业务逻辑之间的依赖性降低,提高了代码的可维护性。
- 可测试性:由于UI和业务逻辑的分离,可以更容易地对业务逻辑进行单元测试。
- 重用性:ViewModel可以独立于View存在,因此可以在不同的View之间重用。
- 清晰的逻辑:业务逻辑被封装在ViewModel中,使得代码更加模块化和易于理解。
二、MVVM的实现步骤
- 创建Model:定义数据模型,如实体类。
- 创建ViewModel:实现ViewModel类,处理数据绑定、命令绑定和事件处理等逻辑。
- 创建View:使用XAML定义用户界面,并绑定ViewModel。
三、案例演示
需求描述:完成学生信息的添加、查询、删除、清空界面数据功能。
目录结构:
3.1 创建Model
Student.cs
在Model中声明student的属性变量,并在set方法中调用RaisePropertyChanged方法,用于通知界面
using ExampleDemo._01_MvvmDemo.Utils;
using ExampleDemo.Common.ViewModel;
namespace ExampleDemo._03_StudentList
{
internal class Student : ViewModelBase
{
private int studentId;
public int StudentId
{
get
{
return this.studentId;
}
set
{
if (this.studentId != value)
{
this.studentId = value;
RaisePropertyChanged();
}
}
}
private string studentName;
public string StudentName
{
get
{
return this.studentName;
}
set
{
if (this.studentName != value)
{
this.studentName = value;
RaisePropertyChanged();
}
}
}
private int studentAge;
public int StudentAge
{
get
{
return this.studentAge;
}
set
{
if (this.studentAge != value)
{
this.studentAge = value;
RaisePropertyChanged();
}
}
}
private GenderEnum studentGender;
public GenderEnum StudentGender
{
get
{
return this.studentGender;
}
set
{
if (this.studentGender != value)
{
this.studentGender = value;
RaisePropertyChanged();
}
}
}
}
}
3.2 创建View
在View中需要绑定ViewModel MainView.xaml
<Window x:Class="ExampleDemo._03_StudentList.View.MainView"
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:ExampleDemo._03_StudentList.View"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
xmlns:enum="clr-namespace:ExampleDemo._03_StudentList.Utils"
mc:Ignorable="d"
Title="StudentView" Height="450" Width="1000">
<Window.Resources>
<ObjectDataProvider
x:Key="Genders_XAML"
MethodName="GetValues"
ObjectType="{x:Type sys:Enum}">
<ObjectDataProvider.MethodParameters>
<x:Type TypeName="enum:GenderEnum" />
</ObjectDataProvider.MethodParameters>
</ObjectDataProvider>
<Style x:Key="LocalTextBoxStyle" TargetType="TextBox">
<Setter Property="Width" Value="100" />
<Setter Property="Height" Value="20" />
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="true">
<Setter Property="ToolTip" Value="{Binding RelativeSource={x:Static RelativeSource.Self}, Path=(Validation.Errors)/ErrorContent}" />
<Setter Property="BorderThickness" Value="0" />
</Trigger>
</Style.Triggers>
</Style>
<Style x:Key="LocalTextBlockStyle" TargetType="TextBlock">
<Setter Property="Height" Value="20" />
<Setter Property="Margin" Value="5"></Setter>
</Style>
<Style x:Key="BtnStyle" TargetType="Button">
<Setter Property="Width" Value="80" />
<Setter Property="Height" Value="20" />
</Style>
<Style x:Key="StudentListInfo" TargetType="TextBlock">
<Setter Property="Margin" Value="5"></Setter>
</Style>
</Window.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="32" />
<RowDefinition Height="200" />
</Grid.RowDefinitions>
<!--第一行-->
<StackPanel Grid.Row="0" Grid.Column="0" Orientation="Horizontal">
<TextBlock Style="{StaticResource LocalTextBlockStyle}" Text="学号:" />
<TextBox Style="{StaticResource LocalTextBoxStyle}" Text="{Binding StudentModel.StudentId,UpdateSourceTrigger=PropertyChanged}"/>
<TextBlock Style="{StaticResource LocalTextBlockStyle}" Text="姓名:" />
<TextBox Style="{StaticResource LocalTextBoxStyle}" Text="{Binding StudentModel.StudentName,UpdateSourceTrigger=PropertyChanged}"/>
<TextBlock Style="{StaticResource LocalTextBlockStyle}" Text="年龄:" />
<TextBox Style="{StaticResource LocalTextBoxStyle}" Text="{Binding StudentModel.StudentAge,UpdateSourceTrigger=PropertyChanged}"/>
<TextBlock Style="{StaticResource LocalTextBlockStyle}" Text="性别:" />
<ComboBox
Width="100"
Height="20"
Margin="0,5,0,5"
ItemsSource="{Binding Genders}"
SelectedItem="{Binding StudentModel.StudentGender,UpdateSourceTrigger=PropertyChanged}"/>
<Button Style="{StaticResource BtnStyle}" Command="{Binding AddStudentCommand}" Margin="5" Content="添加" />
<Button Style="{StaticResource BtnStyle}" Command="{Binding ClearCommand}" Margin="5" Content="清除" />
<Button Style="{StaticResource BtnStyle}" Command="{Binding GetStudentListCommand}" Margin="5" Content="查询" />
<Button Style="{StaticResource BtnStyle}" Command="{Binding RemoveStudentCommand}" Margin="5" Content="移除" />
</StackPanel>
<StackPanel
Grid.Row="1"
Grid.ColumnSpan="2"
Orientation="Horizontal">
<ListBox ItemsSource="{Binding Students}" SelectedItem="{Binding SelectStudent}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Style="{StaticResource StudentListInfo}" Text="{Binding StudentId}"></TextBlock>
<TextBlock Style="{StaticResource StudentListInfo}" Text="{Binding StudentName}"></TextBlock>
<TextBlock Style="{StaticResource StudentListInfo}" Text="{Binding StudentAge}"></TextBlock>
<TextBlock Style="{StaticResource StudentListInfo}" Text="{Binding StudentGender}"></TextBlock>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</StackPanel>
</Grid>
</Window>
MainView.xaml.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
namespace ExampleDemo._03_StudentList.View
{
/// <summary>
/// StudentListView.xaml 的交互逻辑
/// </summary>
public partial class MainView : Window
{
public MainView()
{
InitializeComponent();
// 将View与ViewModel进行绑定
this.DataContext = new StudentListViewModel();
}
}
}
3.3 创建ViewModel
StudentListViewModel.cs
在这里,会创建界面需要用到的命令以及属性绑定。
using ExampleDemo._01_MvvmDemo.Utils;
using ExampleDemo.Common.ViewModel;
using ExampleDemo.Common.Command;
using System.Windows;
using System.Windows.Input;
using System.Collections.ObjectModel;
namespace ExampleDemo._03_StudentList
{
internal class StudentListViewModel : ViewModelBase
{
// 1.VIewModel 声明为单例模式
private static StudentListViewModel _instance;
public static StudentListViewModel Instance
{
get
{
if (_instance == null)
{
_instance = new StudentListViewModel();
}
return _instance;
}
private set { _instance = value; }
}
// 学生列表
private ObservableCollection<Student> _students = new ObservableCollection<Student>();
public ObservableCollection<Student> Students
{
get => _students;
set
{
_students = value;
RaiseCollectionChanged(Students);
//return new Student { StudentId = 1001, StudentName="TOM", StudentAge=18, StudentGender = GenderEnum.Female };
}
}
// 选中学生
private Student _selectStudent;
public Student SelectStudent
{
get => _selectStudent;
set
{
_selectStudent = value;
RaisePropertyChanged();
}
}
//2.为前端View提供绑定的列表枚举数据(属性方式,字段不行必须声明为静态)
public List<GenderEnum> Genders
{
get
{
return new List<GenderEnum>() { GenderEnum.Male, GenderEnum.Female };
}
}
// 添加学生
public Student StudentModel { get; set; } = new Student();
/// <summary>
/// 获取学生信息命令
/// </summary>
public ICommand GetStudentListCommand
{
get => new RelayCommand(this.GetStudents);
}
/// <summary>
/// 添加学生命令
/// </summary>
public ICommand AddStudentCommand
{
get => new RelayCommand(this.AddStudent);
}
/// <summary>
/// 移除学生命令
/// </summary>
public ICommand RemoveStudentCommand
{
get => new RelayCommand(this.RemoveStudent, this.CanRemoveStudent);
}
/// <summary>
/// 清空命令
/// </summary>
public ICommand ClearCommand
{
get => new RelayCommand(this.ClearAll, CanClearAll);
}
/// <summary>
/// 获取学生信息
/// </summary>
/// <param name="parameter"></param>
public void GetStudents(Object parameter)
{
_students.Add(new Student { StudentId = 1001, StudentName = "Tom", StudentAge = 18, StudentGender = GenderEnum.Female });
_students.Add(new Student { StudentId = 1002, StudentName = "Jack", StudentAge = 17, StudentGender = GenderEnum.Male });
_students.Add(new Student { StudentId = 1003, StudentName = "Sally", StudentAge = 16, StudentGender = GenderEnum.Female });
_students.Add(new Student { StudentId = 1004, StudentName = "Marry", StudentAge = 21, StudentGender = GenderEnum.Male });
_students.Add(new Student { StudentId = 1005, StudentName = "Alice", StudentAge = 27, StudentGender = GenderEnum.Female });
}
/// <summary>
/// 添加学生
/// </summary>
/// <param name="parameter"></param>
public void AddStudent(Object parameter)
{
_students.Add(new Student{ StudentId = StudentModel.StudentId, StudentName = StudentModel.StudentName, StudentAge = StudentModel.StudentAge, StudentGender = StudentModel.StudentGender });
StudentModel.StudentId = 0;
StudentModel.StudentName = "";
StudentModel.StudentAge = 0;
StudentModel.StudentGender = GenderEnum.Male;
}
/// <summary>
/// 移除学生
/// </summary>
/// <param name="parameter"></param>
public void RemoveStudent(Object parameter)
{
if (_selectStudent != null)
{
_students.Remove(_selectStudent);
}
}
/// <summary>
/// 移除学生按钮是否可用
/// </summary>
/// <returns></returns>
public bool CanRemoveStudent()
{
return _selectStudent != null;
}
/// <summary>
/// 清空界面输入框
/// </summary>
/// <param name="parameter"></param>
public void ClearAll(Object parameter)
{
StudentModel.StudentId = 0;
StudentModel.StudentName = "";
StudentModel.StudentAge = 0;
StudentModel.StudentGender = GenderEnum.Male;
}
/// <summary>
/// 清空按钮是否可用
/// </summary>
/// <returns></returns>
public bool CanClearAll()
{
return StudentModel.StudentId != 0 && StudentModel.StudentName != "";
}
}
}
3.4 界面效果
附录
一、GenderEnum.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ExampleDemo._03_StudentList.Utils
{
public enum GenderEnum
{
Male,
Female
}
}
二、RelayCommand.cs
路径:/Common/Command/RelayCommand.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
namespace ExampleDemo.Common.Command
{
//2.默认object命令实现:RelayCommand<object>
public class RelayCommand : RelayCommand<object>
{
public RelayCommand(Action<object> action, Func<bool> canExecute = null) : base(action, canExecute)
{
}
}
//1.自定义ICommand基类:整合泛型,用于在命令内部实现命令逻辑action
public class RelayCommand<T> : ICommand
{
#region Private Members
/// <summary>
/// The _action to run
/// </summary>
private Action<T> _action;
private readonly Func<bool> _canExecute;
#endregion
#region Constructor
/// <summary>
/// Default constructor
/// </summary>
public RelayCommand(Action<T> action, Func<bool> canExecute = null)
{
_action = action;
_canExecute = canExecute;
}
#endregion
#region Command Methods
/// <summary>
/// A relay command can always execute
/// </summary>
/// <param name="parameter"></param>
/// <returns></returns>
public bool CanExecute(object parameter)
{
return _canExecute == null || _canExecute();
}
/// <summary>
/// Executes the commands Action
/// </summary>
/// <param name="parameter"></param>
public void Execute(object parameter)
{
_action((T)parameter);
}
#endregion
#region Public Events
/// <summary>
/// 提供了一个默认的空实现,以避免在没有订阅者时引发异常
/// The event thats fired when the <see cref="CanExecute(object)"/> value has changed
/// </summary>
//public event EventHandler CanExecuteChanged = (sender, e) => { };
/// <summary>
/// 定义方式是特定于 WPF 命令模式的实现,它确保了命令的可执行状态变化能够及时反馈到 UI 上。与 WPF 的事件系统和数据绑定机制紧密集成。
///
/// 这种定义方式将 CanExecuteChanged 事件与 WPF 的 CommandManager.RequerySuggested 事件关联起来。当 CanExecuteChanged 事件被订阅时,它会将 CommandManager.RequerySuggested 事件的处理程序添加到 WPF 框架中。这样,每当命令的可执行状态发生变化时,WPF 会自动触发 UI 更新(例如,重新评估按钮的启用状态)。这是一种特定于 WPF 命令模式的实现方式,它确保了命令模式与 WPF 的数据绑定和事件系统无缝集成。
/// </summary>
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
#endregion
}
}
三、ViewModelBase.cs
路径:/Common/ViewModel/ViewModelBase.cs
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
namespace ExampleDemo.Common.ViewModel
{
public class ViewModelBase : INotifyPropertyChanged, INotifyCollectionChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged(string propertyName = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
public event NotifyCollectionChangedEventHandler CollectionChanged;
public void RaiseCollectionChanged(ICollection collection = null)
{
if (CollectionChanged != null)
{
CollectionChanged(collection, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
}
public void RaiseCollectionAdd(ICollection collection, object item)
{
if (CollectionChanged != null)
{
if (PropertyChanged != null)
{
PropertyChanged(collection, new PropertyChangedEventArgs("Count"));
PropertyChanged(collection, new PropertyChangedEventArgs("Item[]"));
}
CollectionChanged(collection, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
//CollectionChanged(collection, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item));
}
}
}
}