In-depth understanding of the design ideas of MVVM in WPF

In recent years, as WPF has become more and more widely used in production, manufacturing, industrial control and other fields, many companies have gradually increased their demand for WPF development, causing many people to see potential opportunities and continue to switch from Web and WinForm development to WPF development, but WPF development also has many new concepts and design ideas, such as: data drive, data binding, dependency properties, commands, control templates, data templates, MVVM, etc., which are very different from traditional WinForm and ASP.NET WebForm development. Today, I will use a simple example to briefly describe the design ideas and applications of MVVM in WPF development.

Why use MVVM?

Traditional WinForm development is generally event-driven, that is, the user clicks on the event, triggers the corresponding event, and obtains the data entered by the user on the page through a unique identifier in the event, and then performs business logic processing. This will have a drawback, that is, user input (User Interface) and business logic (Business) are closely coupled together and cannot be separated. As the business of the project continues to become more complex, the drawbacks of this high degree of coupling will become more and more serious. It's becoming more and more obvious. And there will be a phenomenon where the division of labor is unclear (such as back-end engineers, front-end UI) and the work cannot be divided. Therefore, layering (such as MVC, MVVM), testability (Unit Test), and separation of front-end and back-end have become issues that must be faced. The MVVM design pattern we are going to explain today solves the problems we are facing very well.

What is MVVM?

MVVM is Model-View-ViewModel, which is a design pattern used to decouple UI code and non-UI code. With MVVM, you can define the UI declaratively in XAML and use data binding to mark the UI to other layers containing data and commands. Data binding provides loose coupling of data and structures, keeping the UI and linked data in sync while routing user input to the appropriate commands. The details are shown in the figure below:

As shown in FIG:

  1. View (user page) is mainly used to display information to users, receive information input by users (data binding), and respond to user operations (Command).
  2. ViewModel (user view business logic) mainly handles customer requests and data presentation.
  3. Model data model, as a carrier for storing data, is a specific model class that is called through ViewModel. But in small projects, Model is not necessary .
  4. IService (data interface), data access service, is a service used to obtain various types of data. There are many forms of data, such as network data, local data, and database data, but when the ViewModel is called, they are all encapsulated into a Service. In small projects, the IService data interface is not necessary and does not belong to the scope of MVVM .
  5. In the above figure, DataBase, Network, Local, etc. represent different data source forms and do not belong to the category of MVVM .

Prerequisites

To implement MVVM, two conditions need to be met first:

  1. Property change notification , in the MVVM idea, is driven by WinForm events and transformed into data-driven. In C#, ordinary properties do not have the change notification function. To implement the change notification function, the INotifyPropertyChanged interface must be implemented.
  2. Binding commands . In WPF, in order to solve the coupling between event response functions, the idea of ​​binding commands is proposed, that is, commands can be connected to controls in a binding way. Binding commands must implement the ICommand interface.

After the above two conditions are met, how to associate the properties and commands with change notifications in ViewModel with the controls in View? The answer is binding .

When the data control of the View layer is bound to the property with notification function, Binding will automatically listen to the PropertyChanged event from the interface . In order to achieve the effect of data-driven UI, it can be said that [a bridge flies to the north and south, and the natural chasm becomes a thoroughfare].

MVVM instance

In order to further experience the design ideas of MVVM and verify the above theoretical knowledge, examples are used to illustrate. The project structure of this example is as follows:

MVVM core code

1. Attributes with notification function

First define an abstract class ObservableObject, which implements the INotifyPropertyChanged interface, as shown below:

using System.ComponentModel;
using System.Runtime.CompilerServices;
 
namespace DemoMVVM.Core
{
    /// <summary>
    /// 可被观测的类
    /// </summary>
    public abstract class ObservableObject : INotifyPropertyChanged
    {
        /// <summary>
        /// 属性改变事件
        /// </summary>
        public event PropertyChangedEventHandler? PropertyChanged;
 
        /// <summary>
        /// 属性改变触发方法
        /// </summary>
        /// <param name="propertyName">属性名称</param>
        protected void RaisePropertyChanged([CallerMemberName]string propertyName=null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
 
        /// <summary>
        /// 设置属性值,如果发生改变,则调用通知方法
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="target"></param>
        /// <param name="value"></param>
        /// <param name="propertyName"></param>
        /// <returns></returns>
        protected bool SetProperty<T>(ref T target,T value, [CallerMemberName] string propertyName = null)
        {
            if (EqualityComparer<T>.Default.Equals(target, value))
            {
                return false;
            }
            else
            {
                target=value;
                RaisePropertyChanged(propertyName);
                return true;
            }
        }
    }
}

 

Note: The above SetProperty is mainly used to turn ordinary properties into properties with notification functions.

Then define a ViewMode base class, inherited from ObservableObject, for subsequent expansion, as shown below:

namespace DemoMVVM.Core
{
    /// <summary>
    /// ViewModel基类,继承自ObservableObject
    /// </summary>
    public abstract class ViewModelBase:ObservableObject
    {
 
    }
}

 

2. Commands with binding function

First define a DelegateCommand and implement the ICommand interface, as shown below:

namespace DemoMVVM.Core
{
    public class DelegateCommand : ICommand
    {
        private Action<object> execute;
        private Predicate<object> canExecute;
 
 
        public event EventHandler? CanExecuteChanged;
 
        public DelegateCommand(Action<object> execute, Predicate<object> canExecute)
        {
            if (execute == null)
            {
                throw new ArgumentNullException("execute 不能为空");
            }
            this.execute = execute;
            this.canExecute = canExecute;
        }
 
        public DelegateCommand(Action<object> execute):this(execute,null)
        {
 
        }
 
        public bool CanExecute(object? parameter)
        {
            return  canExecute?.Invoke(parameter)!=false;
        }
 
        public void Execute(object? parameter)
        {
            execute?.Invoke(parameter);
        }
    }
}

 

Note that the constructor of DelegateCommand receives two parameters, one is Execute (to do the work) and the other is CanExecute (to determine whether the work can be done) .

MVVM application code

This example mainly implements the operation of two numbers. Such as addition, subtraction, multiplication, division and other functions.

First define ViewModel, which inherits from ViewModelBase and mainly implements properties and commands with notification functions, as shown below:

using DemoMVVM.Core;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime;
using System.Text;
using System.Threading.Tasks;
 
namespace DemoMVVM
{
    public class MainWindowViewModel:ViewModelBase
    {
        #region 属性及构造函数
 
        private double leftNumber;
 
		public double LeftNumber
		{
			get { return leftNumber; }
			set { SetProperty(ref leftNumber , value); }
		}
 
		private double rightNumber;
 
		public double RightNumber
		{
			get { return rightNumber; }
			set { SetProperty(ref rightNumber , value); }
		}
 
		private double resultNumber;
 
		public double ResultNumber
		{
			get { return resultNumber; }
			set { SetProperty(ref resultNumber , value); }
		}
 
 
		public MainWindowViewModel()
		{
 
		}
 
		#endregion
 
		#region 命令
 
		private DelegateCommand operationCommand;
 
		public DelegateCommand OperationCommand
		{
			get {
 
				if (operationCommand == null)
				{
					operationCommand = new DelegateCommand(Operate);
				}
				return operationCommand; }
		}
 
		private void Operate(object obj)
		{
			if(obj == null)
			{
				return;
			}
			var type=obj.ToString();
			switch (type)
			{
				case "+":
					this.ResultNumber = this.LeftNumber + this.RightNumber;
					break;
				case "-":
                    this.ResultNumber = this.LeftNumber - this.RightNumber;
                    break;
				case "*":
                    this.ResultNumber = this.LeftNumber * this.RightNumber;
                    break;
				case "/":
					if (this.RightNumber == 0)
					{
						this.ResultNumber = 0;
					}
					else
					{
						this.ResultNumber = this.LeftNumber / this.RightNumber;
					}
                    break;
			}
		}
 
 
        #endregion
 
    }
}

 

 Create a view, perform data binding in the view, and associate the ViewModel with the UI, as shown below:

<Window x:Class="DemoMVVM.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:DemoMVVM"
        mc:Ignorable="d"
        Title="MVVM示例" Height="350" Width="600">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition></ColumnDefinition>
            <ColumnDefinition></ColumnDefinition>
            <ColumnDefinition Width="0.3*"></ColumnDefinition>
            <ColumnDefinition></ColumnDefinition>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition></RowDefinition>
            <RowDefinition></RowDefinition>
            <RowDefinition></RowDefinition>
            <RowDefinition></RowDefinition>
        </Grid.RowDefinitions>
        <StackPanel Grid.Row="1" Grid.Column="0" Orientation="Horizontal">
            <TextBlock Text="A1:" VerticalAlignment="Center" ></TextBlock>
            <TextBox  Margin="10" Width="120" Height="35" Text="{Binding LeftNumber, UpdateSourceTrigger=PropertyChanged}" VerticalContentAlignment="Center"></TextBox>
        </StackPanel>
        <StackPanel Grid.Row="1" Grid.Column="1" Orientation="Horizontal">
            <TextBlock Text="A2:" VerticalAlignment="Center" ></TextBlock>
            <TextBox  Margin="10" Width="120" Height="35" Text="{Binding RightNumber, UpdateSourceTrigger=PropertyChanged}" VerticalContentAlignment="Center"></TextBox>
        </StackPanel>
        <TextBlock Grid.Row="1" Grid.Column="2" Text="=" VerticalAlignment="Center" HorizontalAlignment="Center"></TextBlock>
        <StackPanel Grid.Row="1" Grid.Column="3" Orientation="Horizontal">
            <TextBlock Text="A3:" VerticalAlignment="Center" ></TextBlock>
            <TextBox  Margin="10" Width="120" Height="35" Text="{Binding ResultNumber, UpdateSourceTrigger=PropertyChanged}" VerticalContentAlignment="Center"></TextBox>
        </StackPanel>
        <StackPanel Grid.Row="2" Grid.ColumnSpan="4" Orientation="Horizontal" HorizontalAlignment="Center">
            <Button Content="+" Width="100" Height="35" Margin="10" Command="{Binding OperationCommand}" CommandParameter="+"></Button>
            <Button Content="-" Width="100" Height="35" Margin="10" Command="{Binding OperationCommand}" CommandParameter="-"></Button>
            <Button Content="*" Width="100" Height="35" Margin="10" Command="{Binding OperationCommand}" CommandParameter="*"></Button>
            <Button Content="/" Width="100" Height="35" Margin="10" Command="{Binding OperationCommand}" CommandParameter="/"></Button>
        </StackPanel>                                               
    </Grid>
</Window>

 

Note that in the xaml front-end UI code, the Text of TextBox and the Command of Button are bound respectively, achieving the function of data-driven UI and UI responding to customers .

In the UI constructor, associate the DataContext data context with the ViewModel, as shown below:

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

 

MVVM example demonstration

Through the above steps, a simple application of MVVM has been completed. Example demonstration is as follows:

The above is all the content to deeply understand the design ideas of MVVM in WPF. I hope we can inspire others, learn together, and make progress together.

Guess you like

Origin blog.csdn.net/fengershishe/article/details/132999332