WPF中MVVM的基本使用

一、MVVM模式的基本概念

MVVMModel-View-ViewModel)是一种设计模式,特别适用于WPFWindows Presentation Foundation)应用程序开发。它将应用程序分为三个核心部分:模型(Model)、视图(View)和视图模型(ViewModel),以实现关注点分离,提高代码的可维护性和可测试性。

MVVM模式包含三个主要部分:

  1. Model(模型) :代表应用程序的核心数据和业务逻辑。它通常包含数据结构、业务规则和数据访问代码。Model不依赖于UI,是独立且可重用的组件。
  2. View(视图) :负责显示数据和接收用户输入。View通过数据绑定命令ViewModel交互,而不直接访问ModelView通常是XAML文件及其相关的代码隐藏文件。
  3. ViewModel(视图模型) :作为ViewModel之间的桥梁。它负责从Model获取数据,并将这些数据提供给View,同时处理用户在View上的交互。ViewModel通常实现通知机制(如 INotifyPropertyChanged 接口),以便在数据变化时通知View进行更新。

在这里,再着重提一下:ViewViewModel的交互是通过命令与数据绑定来完成的。
通过命令View 层可以发出操作请求到ViewModel中让ViewModel进行数据处理。
通过数据绑定,可以让ViewModel把处理好的数据展示到View中。

MVVM的优点

  1. 低耦合:视图和业务逻辑之间的依赖性降低,提高了代码的可维护性。
  2. 可测试性:由于UI和业务逻辑的分离,可以更容易地对业务逻辑进行单元测试。
  3. 重用性:ViewModel可以独立于View存在,因此可以在不同的View之间重用。
  4. 清晰的逻辑:业务逻辑被封装在ViewModel中,使得代码更加模块化和易于理解。

二、MVVM的实现步骤

  1. 创建Model:定义数据模型,如实体类。
  2. 创建ViewModel:实现ViewModel类,处理数据绑定、命令绑定和事件处理等逻辑。
  3. 创建View:使用XAML定义用户界面,并绑定ViewModel。

三、案例演示

需求描述:完成学生信息的添加、查询、删除、清空界面数据功能。
目录结构:

WPF中MVVM的基本使用_wpf

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 界面效果

WPF中MVVM的基本使用_wpf_02

附录

一、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));
            }
        }
    }
}