WPF/MVVM系列(6)——MVVM模式

原文地址:李浩的博客 lihaohello.top


实例说明

本文以一个简单的小实例,说明如何使用WPF的MVVM模式。
窗体上有两个文本框和一个按钮,当两个文本框都不为空时使按钮可用,否则不可用。点击按钮后,消息框显示两个文本框里的内容。
image.png

原生库

(1)Model

public class PersonModel {
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

(2)View

<Window x:Class="TestMvvm.Views.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:TestMvvm.Views"
        mc:Ignorable="d"
        Title="MainWindow" SizeToContent="WidthAndHeight">
    <Grid>
        <StackPanel Margin="10">
            <TextBox Text="{Binding FirstName,UpdateSourceTrigger=PropertyChanged}" Margin="0 0 0 5" Width="150" Height="30"/>
            <TextBox Text="{Binding LastName,UpdateSourceTrigger=PropertyChanged}" Margin="0 0 0 5" Width="150" Height="30"/>
            <Button Content="提交" Command="{Binding SubmitCommand}" Width="150" Height="30"/>
        </StackPanel>
    </Grid>
</Window>
  • 按钮命令的绑定和文本框文本的绑定类似。
  • 设置UpdateSourceTrigger=PropertyChanged,保证文本框内容发生改变马上更新ViewModel中的数据。

(3)ViewModel

using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Input;
using TestMvvm.Models;

namespace TestMvvm.ViewModels {
    public class PersonViewModel : INotifyPropertyChanged {
        public PersonViewModel() {
            _person = new PersonModel();
            _submitCommand = new MyCommand(ShowSubmitMessage, EnableSubmit);
        }

        #region 界面绑定数据定义
        private PersonModel _person;
        public string FirstName {
            get { return _person.FirstName; }
            set {
                if (_person.FirstName != value) {
                    _person.FirstName = value;
                    OnPropertyChanged();
                    SubmitCommand.RaiseCanExecuteChange();
                }
            }
        }
        public string LastName {
            get { return _person.LastName; }
            set {
                if (_person.LastName != value) {
                    _person.LastName = value;
                    OnPropertyChanged();
                    SubmitCommand.RaiseCanExecuteChange();
                }
            }
        }
        #endregion

        #region 命令定义
        private MyCommand _submitCommand;
        public MyCommand SubmitCommand {
            get {
                return _submitCommand;
            }
            set {
                _submitCommand = value;
                OnPropertyChanged();
            }
        }
        #endregion


        #region 业务处理逻辑,程序的核心
        public void ShowSubmitMessage(object parameter) {
            MessageBox.Show($"FirstName: {FirstName},LastName: {LastName}");
        }

        public bool EnableSubmit(object parameter) {
            if (FirstName != null && !string.IsNullOrWhiteSpace(FirstName)
                && LastName != null && !string.IsNullOrWhiteSpace(LastName))
                return true;
            return false;
        }
        #endregion

        public event PropertyChangedEventHandler PropertyChanged;
        private void OnPropertyChanged([CallerMemberName] string propertyName = null) {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }

    /// <summary>
    /// 自定义命令类
    /// </summary>
    public class MyCommand : ICommand {
        private Action<object> execute;
        private Func<object, bool> canExecute;

        public MyCommand(Action<object> execute, Func<object, bool> canExecute = null) {
            this.execute = execute;
            this.canExecute = canExecute;
        }

        public event EventHandler CanExecuteChanged;

        public bool CanExecute(object parameter) {
            if (canExecute != null)
                return canExecute(parameter);
            return true;
        }

        public void Execute(object parameter) {
            execute?.Invoke(parameter);
        }

        public void RaiseCanExecuteChange() {
            CanExecuteChanged?.Invoke(this, EventArgs.Empty);
        }
    }
}
  • 当属性值发生变化时,调用RaiseCanExecuteChange()函数,触发CanExecute()函数,从而更新按钮的可用状态。
  • 用属性对命令进行包装,可实现命令绑定和动态变化;在大多数情况下,一个按钮的逻辑基本是固定的,这里的做法可能略显过度,可以声明一个只读属性。
private MyCommand _submitCommand;
public MyCommand SubmitCommand => _submitCommand;
  • 这里为了演示CanExecute()的作用,实际上可以使用多值绑定的方式更新按钮的可用状态,这样可以保证UI逻辑更加紧凑。
  • ViewModel文件中的内容较杂,可以使用#region来分块整理。

(4)在View.xaml的后台代码中绑定ViewModel

using System.Windows;
using TestMvvm.ViewModels;

namespace TestMvvm.Views {
    public partial class MainWindow : Window {
        public MainWindow() {
            InitializeComponent();
            // 绑定ViewModel
            this.DataContext = new PersonViewModel();
        }
    }
}
  • 依旧采用最经典的DataContext方式进行绑定。

CommunityToolkit.Mvvm库

下面使用比较主流的CommunityToolkit.Mvvm库改造上面的例子,重点依然集中在数据和命令绑定上。

using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System.Windows;
using TestMvvm.Models;

namespace TestMvvm.ViewModels {
    public class PersonViewModel : ObservableObject {
        public PersonViewModel() {
            _person = new PersonModel();
        }

        #region 界面绑定数据定义
        private PersonModel _person;

        public string FirstName {
            get => _person.FirstName;
            set {
                SetProperty(_person.FirstName, value, _person, (u, n) => u.FirstName = n);
                SubmitCommand.NotifyCanExecuteChanged();
            }
        }

        public string LastName {
            get => _person.LastName;
            set {
                SetProperty(_person.LastName, value, _person, (u, n) => u.LastName = n);
                SubmitCommand.NotifyCanExecuteChanged();
            }
        }
        #endregion


        #region 命令定义
        private RelayCommand<object> _submitCommand;
        public RelayCommand<object> SubmitCommand {
            get {
                if (_submitCommand == null) {
                    _submitCommand = new RelayCommand<object>(ShowSubmitMessage, (p) => {
                        if (FirstName != null && !string.IsNullOrWhiteSpace(FirstName) &&
                        LastName != null && !string.IsNullOrWhiteSpace(LastName))
                            return true;
                        return false;
                    });
                }
                return _submitCommand;
            }

        }
        #endregion


        #region 业务处理逻辑,程序的核心
        public void ShowSubmitMessage(object parameter) {
            MessageBox.Show($"FirstName: {FirstName},LastName: {LastName}");
        }
        #endregion
    }
}

从以上代码可以看到,相比采用原生的数据和命令绑定,使用CommunityToolkit.Mvvm库后代码量可以进一步下降,且程序的可读性更好,有以下几点需要重点说明下:

  • ViewModel类继承自ObservableObject类,在属性set方法中直接调用SetProperty()方法更新属性值,就可以通知View上的绑定目标刷新。
  • 本文中SetProperty()方法的最后一个参数是Action类型,主要用于将value赋值给属性时的数据转换或者验证。
  • RelayCommand是一个泛型命令类,其泛型参数表示命令的参数类型,第一个参数指定命令的Execute()逻辑,第二个参数指定命令的CanExecute()逻辑。
  • 当文本框内容发生变化时,会自动更新内存数据,这时调用SubmitCommand.NotifyCanExecuteChanged(),可以触发执行CanExecute()逻辑,从而更新按钮的可用状态。

总结

(1)本文以一个小例子介绍了如何采用WPF原生库和CommunityToolkit.Mvvm库实现简单的MVVM模式,后者对MVVM模式的常用功能进行了进一步封装,使用更方便,代码更简洁,在大型项目中可以优先考虑使用。
(2)除了数据和命令绑定,CommunityToolkit.Mvvm库也提供了很多其他功能,比如依赖注入、控制反转等。
(3)对一个设计理念的理解或优秀框架的学习没有止境,后面随着自己理解的深入,我将不断更新本系列文章。


原文地址:李浩的博客 lihaohello.top

  • 24
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值