WPF (five) MVVM pattern

1. The concept of MVVM

        MVVM is an abbreviation of Model-View-ViewModel, and MVVM is a design pattern similar to the more popular MVC. The main purpose of introducing this model is to separate the front-end UI view (View) from the back-end logic data (Model), thereby reducing the coupling between the front-end and the front-end, and improving the development efficiency, maintainability, and scalability of the project.

  • Model: Simple abstraction and model encapsulation of real things without adding any business code
  • View: the front-end visual UI interface, such as Window/Page
  • ViewModel: The view model bound to the front end, which stores the interaction logic and data related to the interface, similar to the Controller in MVC

 2. MVVM project architecture

        This section aims to introduce the organizational structure of MVVM with a simple case. The background of the case is to design a simple student information editing interface, with two buttons "OK" and "Clear", which are used to display input student information and clear edit information respectively. The general project organization structure of the whole case is as follows:

├── Commands : Place all base commands and other encapsulated reusable command classes

├── Converters : place all converters

├── Model : place model/abstract entity class

├── Pages : place all pages Page

├── Style : Place all custom control styles

├── Utils : place all tools

├── ViewModel: Place all the ViewModels bound by the page

└── Windows : place all window classes

1. Commands/RelayCommand base command class

        Customize the ICommand base command class, implement the internal execution logic/behavior of the command by entrusting the Action, and expand the command through generics to realize the separation of the front and back ends of logic processing.

namespace WPF_MVVM.Commands
{
    //2.默认object命令实现:RelayCommand<object>
    public class RelayCommand : RelayCommand<object>
    {
        public RelayCommand(Action<object> action) : base(action)
        {
        }
    }
    //1.自定义ICommand基类:整合泛型,用于在命令内部实现命令逻辑action
    public class RelayCommand<T> : ICommand
    {
        #region Private Members

        /// <summary>
        /// The _action to run
        /// </summary>
        private Action<T> _action;

        #endregion

        #region Constructor

        /// <summary>
        /// Default constructor
        /// </summary>
        public RelayCommand(Action<T> action)
        {
            _action = action;
        }

        #endregion

        #region Command Methods

        /// <summary>
        /// A relay command can always execute
        /// </summary>
        /// <param name="parameter"></param>
        /// <returns></returns>
        public bool CanExecute(object parameter)
        {
            return true;
        }

        /// <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) => { };

        #endregion


    }
}

2.ViewModel

(1) Implementation of ViewModelBase

        ViewModel is used to bind View and provide logic/data binding support for front-end View. Therefore, ViewModel needs to implement notification interfaces such as INotifyPropertyChanged, INotifyCollectionChanged, etc., and has the ability to notify UI data changes. We encapsulate its public part as a base class  ViewModelBase.

namespace WPF_MVVM.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)
        {
            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));
            }
        }
    }
}

 (2) Implementation of MainWindowViewModel

        Each window Window/page Page should have a ViewModel bound to it, providing logic/data binding support for View behind the scenes. Because there is only one window page, MainWindow, in this example, there is only one MainWindowViewModel here.

namespace WPF_MVVM.ViewModel
{
    public class MainWindowViewModel : ViewModelBase
    {

        //1.ViewModel 声明为单例模式
        private static MainWindowViewModel instance;

        public static MainWindowViewModel Instance
        {
            get
            {
                if (instance == null)
                {
                    instance = new MainWindowViewModel();

                }
                return instance;
            }
            private set { instance = value; }
        }
        //2.为前端View提供绑定的列表枚举数据(属性方式,字段不行必须声明为静态)
        public List<GenderEnum> Genders
        {
            get
            {
                return new List<GenderEnum>() { GenderEnum.Male, GenderEnum.Female };
            }
        }

        //3.持有Model对象提供数据绑定Model.xxx(需要先new出来,否则为null)
        public Student StudentModel { get; set; } = new Student();

        //4.自定义Command绑定的处理方式
        public ICommand ConfirmCommand
        {
            get
            {
                return new RelayCommand((parameter) =>
                {
                    MessageBox.Show("StudentId: " + StudentModel.StudentId + "\n" +
                    "StudentName: " + StudentModel.StudentName + "\n" +
                    "StudentAge: " + StudentModel.StudentAge + "\n" +
                    "StudentGender: " + StudentModel.StudentGender.ToString());
                });
            }
        }

        public ICommand ClearCommand
        {
            get
            {
                return new RelayCommand((parameter) =>
                {
                    StudentModel.StudentId = -1;
                    StudentModel.StudentAge = -1;
                    StudentModel.StudentName = "null";
                    StudentModel.StudentGender = GenderEnum.Male;
                });
            }
        }

    }
}

 3. Model/Student entity class

        An entity class encapsulates all properties of an entity. The reason why the entity class Student also implements ViewModelBase here is that we need to bind the properties of Student in the View, and should be able to notify the front-end View when the Student data changes, so we need to implement the notification interface:

  • Method 1: Encapsulate the properties of Student in MainWindowViewModel, and bind data directly through MainWindowViewModel. But the disadvantage of this is that the difference between ViewModel and Model is not clear, and Model becomes dispensable.
  • Method 2: Separately encapsulate the entity properties of Student in the Model, and then only need to hold the Model object in the MainWindowViewModel (it needs to be new, otherwise it will be null), so that the Model layer also needs to implement ViewModelBase to notify the front-end UI property changes. Here is the second way.
namespace WPF_MVVM.Model
{
    public class Student: ViewModelBase
    {
        private int studentId;
        public int StudentId 
        {
            get
            {
                return this.studentId;
            }
            set
            {
                if (this.studentId != value)
                {
                    this.studentId = value;
                    RaisePropertyChanged("StudentId");
                }
            }
        }

        private string studentName;
        public string StudentName
        {
            get
            {
                return this.studentName;
            }
            set
            {
                if (this.studentName != value)
                {
                    this.studentName = value;
                    RaisePropertyChanged("StudentName");
                }
            }
        }

        private int studentAge;
        public int StudentAge
        {
            get
            {
                return this.studentAge;
            }
            set
            {
                if (this.studentAge != value)
                {
                    this.studentAge = value;
                    RaisePropertyChanged("StudentAge");
                }
            }
        }

        private GenderEnum studentGender;
        public GenderEnum StudentGender
        {
            get
            {
                return this.studentGender;
            }
            set
            {
                if (this.studentGender != value)
                {
                    this.studentGender = value;
                    RaisePropertyChanged("StudentGender");
                }
            }
        }
    }
}

 4.Utils/GenderEnum gender enumeration class

namespace WPF_MVVM.Utils
{
    public enum GenderEnum
    {
        Male,
        Female
    }
}

5. MainWindow interface

        The View part is very simple. We only need to design the UI interface, and then use the Binding method to bind the data (ViewModel) and behavior (ICommand). The front and back ends only maintain the connection through simple data and commands, which greatly decoupling.

<Window
    x:Class="WPF_MVVM.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:enum="clr-namespace:WPF_MVVM.Utils"
    xmlns:local="clr-namespace:WPF_MVVM"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:sys="clr-namespace:System;assembly=mscorlib"
    xmlns:vm="clr-namespace:WPF_MVVM.ViewModel"
    Title="MainWindow"
    Width="300"
    Height="150"
    DataContext="{x:Static vm:MainWindowViewModel.Instance}"
    mc:Ignorable="d">

    <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="135" />
            <Setter Property="Margin" Value="0,5,0,5"/>
            <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>
    </Window.Resources>


    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="*" />
            <RowDefinition Height="*" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>
        <StackPanel Grid.ColumnSpan="2" Orientation="Horizontal">
            <TextBlock Margin="5" Text="学号:" />
            <TextBox Style="{StaticResource LocalTextBoxStyle}" Text="{Binding StudentModel.StudentId,UpdateSourceTrigger=PropertyChanged}"/>
        </StackPanel>
        <StackPanel
            Grid.Row="1"
            Grid.ColumnSpan="2"
            Orientation="Horizontal">
            <TextBlock Margin="5" Text="姓名:" />
            <TextBox Style="{StaticResource LocalTextBoxStyle}" Text="{Binding StudentModel.StudentName,UpdateSourceTrigger=PropertyChanged}"/>
        </StackPanel>
        <StackPanel
            Grid.Row="2"
            Grid.ColumnSpan="2"
            Orientation="Horizontal">
            <TextBlock Margin="5" Text="年龄:" />
            <TextBox Style="{StaticResource LocalTextBoxStyle}" Text="{Binding StudentModel.StudentAge,UpdateSourceTrigger=PropertyChanged}"/>
        </StackPanel>
        <StackPanel
            Grid.Row="3"
            Grid.ColumnSpan="2"
            Orientation="Horizontal">
            <TextBlock Margin="5" Text="性别:" />
            <ComboBox
                Width="135"
                Margin="0,5,0,5"
                ItemsSource="{Binding Genders}" 
                SelectedItem="{Binding StudentModel.StudentGender,UpdateSourceTrigger=PropertyChanged}"/>
        </StackPanel>
        <Button 
            Grid.Column="2"
            Margin="5"
            Content="确定">
            <Button.InputBindings>
                <MouseBinding Command="{Binding ConfirmCommand}" MouseAction="LeftClick"/>
            </Button.InputBindings>
        </Button>
        <Button 
            Grid.Row="1"
            Grid.Column="2"
            Margin="5"
            Content="清除">
            <Button.InputBindings>
                <MouseBinding Command="{Binding ClearCommand}" MouseAction="LeftClick"/>
            </Button.InputBindings>
        </Button>
    </Grid>
</Window>

 3. Case summary

        In the above cases, we have basically clarified the organizational ideas of MVVM. Next, we found that we encountered a lot of syntax we hadn't seen before, so let's make a brief explanation and summary here.

1. Binding restrictions

  • Binding source Source: Source is an object type, which means that the binding source can be any object type that can provide data.
  • Binding path Path: Path is used to set the path of the binding source [property], that is, which data value of the binding Source. It should be noted here that Path is a PropertyPath type, which can only be used to bind properties, not ordinary fields. In fact, fields can be bound through C# code, but ordinary fields cannot implement the notification function after data changes.

2. Introduction to ObjectDataProvider 

        ObjectDataProvider provides a convenient way to create and use objects as binding source objects in XAML. Simply put, ObjectDataProvider can wrap and create the return result of a method as a bound data source in XAML. Since there is no direct way to use an enum as a data binding source, this method is often used to wrap an array of enum values ​​provided by the enum type itself as a data binding source. Its usage is as follows (refer to WPF ObjectDataProvider ​​​​​​​​):

  • Use the attribute to  pass the ConstructorParameters  parameter to the constructor of the object, and wrap the object through the constructor.

  • Use  the MethodName  property to call a method, and use  the MethodParameters  property to pass parameters to the method. The data source can then be bound to the return result of the method.

- ObjectDataProvider 属性:
    - ConstructorParameters:获取要传递给该构造函数的参数列表。
    - MethodName: 获取或设置要调用的方法的名称。
    - MethodParameters: 获取要传递给方法的参数列表。
    - ObjectInstance: 获取或设置用作绑定源的对象。
    - ObjectType: 获取或设置要创建其实例的对象的类型。

- 注意: ObjectInstance和ObjectType不可同时赋值。ObjectType的类型是Type,ObjectInstance的类型是Object。换句话说objecttype可以使用x:type进行赋值,也只有这种方式。objectinstance则是可以通过 staticresource的方式进行绑定,绑定的也是实例化后的元素。

        In the binding enumeration processing, the enumeration base class Enum has a static method Enum.GetValues(Type), which can retrieve and return the constant value array Array in the specified enumeration type. So we don't need to create the enumeration array by ourselves, just process it through ObjectDataProvider. The above case binding enum can also be written as:

  <Window.Resources>
        <ObjectDataProvider
            x:Key="Genders_XAML"
            MethodName="GetValues"
            ObjectType="{x:Type sys:Enum}">
            <ObjectDataProvider.MethodParameters>
                <x:Type TypeName="enum:GenderEnum" />
            </ObjectDataProvider.MethodParameters>
        </ObjectDataProvider>
  </Window.Resources>

<StackPanel
    Grid.Row="3"
    Grid.ColumnSpan="2"
    Orientation="Horizontal">
    <TextBlock Margin="5" Text="性别:" />
    <ComboBox
        Width="135"
        Margin="0,5,0,5"
        ItemsSource="{Binding Source={StaticResource Genders_XAML}}"
        SelectedItem="{Binding StudentModel.StudentGender,UpdateSourceTrigger=PropertyChanged}"/>
</StackPanel>

Guess you like

Origin blog.csdn.net/qq_40772692/article/details/126529074