WPF-理解与使用MVVM,请勿滥用

一、为什么称MVVM被滥用

1、什么是MVVM?

  MVVM是Model-View-ViewModel的简写。它本质上就是MVC的改进版。MVVM模式有助于将应用程序的业务和表示逻辑与用户界面 (UI) 清晰分离。 保持应用程序逻辑和UI之间的清晰分离有助于解决许多开发问题,并使应用程序更易于测试、维护和演变。 它还可以显著提高代码重用机会,并允许开发人员和UI设计人员在开发应用各自的部分时更轻松地进行协作。

  MVVM可用于跨平台软件的代码架构设计。

2、如何被滥用的

  现在一说WPF就需要会MVVM,这股风起源于微软对WPF的宣传。MVVM固然有优势,但是微软描述了MVVM的使用场景。而大多数作者在描述MVVM时,不会去强调MVVM的使用场景。时间一长‘不会MVVM就不算会WPF’就真的变成了写WPF就得用MVVM,而不是MVVM很重要了。对此我希望大家可以正确理解MVVM不要滥用。

  MVVM虽然会使应用程序更易于测试、维护和演变。但是有以下缺点:

    MVVM本身会增加代码量,业务代码不复杂的小界面不适合使用,会增加编码时间和阅读代码的困难度。

    ViewModel分割了model与View间的直接关系,让操作View使用的代码变多;降低编程的快感;

    对于只有一两位开发人员的小型应用程序来说,这种严格分离带来的好处可能无法抵消编码所浪费的时间。原有的事件机制更适合这种程序;

    对于复杂的View,我们在不使用MVVM时,建立一个有嵌套结构的Model就已经感觉到困难,再加一个MVVM的ViewModel要考虑时事情就变多了。可能需要重构ViewModel才能解决。

3、什么时候该用?什么时候不该用?

  (1)该用:

    ① 对View修改频繁,则用MVVM(无需触及代码即可重新设计应用 UI,前提是视图完全在 XAML 中实现);

    ② View用户界面设计人员(程序员,非指原型设计)与应用程序的业务开发人员独立时,可以用MVVM模式分离View与Model;但是要消耗一些时间做好沟通,不要影响上线周期;

    ③ View 与 ViewModel 的分离还使得 ViewModel 更有利于单元测试和重用;需要对View进行独立测试或者排除View进行单元测试时,则选择MVVM;

    ④ 大型项目;

  (2)不该用:

    ① 对View后续控件改动不大;

    ② Hello,World !,小管理系统,小型上位机系统等应用程序业务不复杂的小项目

  (3)都可,根据软件实际情况和代码开发者习惯进行考虑:

    ① 中等项目,后续会改动,界面改动不频繁。

4、使用MVVM后,代码如何写

  记住下面的原则即可:

  1、Model对业务负责,ViewModel对View负责;ViewModel中使用Model中的数据写View相关的逻辑,业务代码写在其他类(如BLL、Service)中,也可直接写在Model中;

  2、Model不了解平台或 ViewModel,Viewmodel可以直接访问Model上的属性和方法,

  3、WPF中的窗体展示可使用如下代码(传参示例)

        public MainWindow(MainViewModel mainViewModel)
        {
            InitializeComponent();

            if (mainViewModel != null)
            {
                DataContext = mainViewModel;
            }
        }

二、原生MVVM案例

  项目地址:https://gitee.com/qq28069933146_admin/wpf_mvvm_simple

1、MVVM知识点

  想要创建一个完整的MVVM,需要用到以下类型或修饰词:

  • Window.DataContext:View Window强绑定ViewModel;
  • Binding:View绑定ViewModel中的变量、事件;
  • INotifyPropertyChanged:用于ViewModel通知View刷新;ViewModel继承该类,并实现PropertyChanged、OnPropertyChanged;
  • ICommand:用于VIew通知ViewModel响应控件事件;创建ICommand的实现类RelayCommand,并实现CanExecuteChanged,CanExecute,Execute;

2、创建Model

namespace WPF_MVVM_Simple.Model
{
    /// <summary>
    /// 用户信息
    /// </summary>
    public class UserModel
    {
        /// <summary>
        /// 主键
        /// </summary>
        public string ID { get; set; } = string.Empty;

        /// <summary>
        /// 用户名
        /// </summary>
        public string Name { get; set; } = string.Empty;

        /// <summary>
        /// 用户邮箱
        /// </summary>
        public string Email { get; set; } = string.Empty;
    }
}

3、创建View

  注意已下代码:

  • Window.DataContext:View Window强绑定ViewModel;
  • Binding:View绑定ViewModel中的变量、事件;
<Window x:Class="WPF_MVVM_Simple.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:WPF_MVVM_Simple"
        xmlns:vModel="clr-namespace:WPF_MVVM_Simple.ViewModel"
        mc:Ignorable="d"
        Title="MVVM原生示例" Height="450" Width="800">
    <Window.DataContext>
        <vModel:MainViewModel />
    </Window.DataContext>
    <Grid>
        <Label Content="名字:" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top"/>
        <TextBox Text="{Binding Name, Mode=TwoWay}" HorizontalAlignment="Left" Margin="56,14,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="139"/>
        <Label Content="邮箱:" HorizontalAlignment="Left" Margin="10,40,0,0" VerticalAlignment="Top"/>
        <TextBox Text="{Binding Email }" HorizontalAlignment="Left" Margin="56,44,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="139"/>

        <!--Click="Button_Click"-->
        <Button Content="获取作者信息" Command="{Binding ButtonCommand}" HorizontalAlignment="Left" Margin="213,13,0,0" VerticalAlignment="Top" RenderTransformOrigin="0,-0.272" Width="90" />
        <!--Click="Button1_Click"-->
        <Button Content="和作者打招呼" Command="{Binding Button1Command}" HorizontalAlignment="Left" Margin="213,44,0,0" VerticalAlignment="Top" RenderTransformOrigin="0,-0.272" Width="90" />

        <Label Content="{Binding Log }" Margin="28,100,28,13" FontSize="72" Foreground="#FF00EDE8" HorizontalAlignment="Center" VerticalAlignment="Center"/>
    </Grid>
</Window>
using System.Text;
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.Navigation;
using System.Windows.Shapes;
using WPF_MVVM_Simple.Model;
using WPF_MVVM_Simple.ViewModel;

namespace WPF_MVVM_Simple
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        //外部传值
        public MainWindow(MainViewModel mainViewModel)
        {
            InitializeComponent();

            if (mainViewModel != null)
            {
                this.DataContext = mainViewModel;
            }
        }
    }
}

4、创建ViewModel

  • ViewModelBase为INotifyPropertyChanged的实现类;
  • RelayCommand为ICommand的实现类;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
using WPF_MVVM_Simple.Commands;
using WPF_MVVM_Simple.Model;

namespace WPF_MVVM_Simple.ViewModel
{
    /// <summary>
    /// MainWindow的ViewModel类
    /// 集合 请使用 ObservableCollection<MainViewModel>
    /// </summary>
    public class MainViewModel : ViewModelBase
    {
        #region 控件 Binding
        private string _name = string.Empty;
        /// <summary>
        /// 名字
        /// </summary>
        public string Name
        {
            get => _name;
            set
            {
                _name = value;
                OnPropertyChanged("Name");  // 通知UI
            }
        }

        private string _email = string.Empty;
        /// <summary>
        /// 邮箱
        /// </summary>
        public string Email
        {
            get => _email;
            set
            {
                _email = value;
                OnPropertyChanged("Email");  // 通知UI
            }
        }

        private string _log = string.Empty;
        /// <summary>
        /// 打招呼的日志
        /// </summary>
        public string Log
        {
            get => _log;
            set
            {
                _log = value;
                OnPropertyChanged("Log");  // 通知UI
            }
        }
        #endregion 控件 Binding

        #region 控件事件
        /// <summary>
        /// 获取作者信息Command
        /// 数据流向:Model-> ViewModel-> View
        /// </summary>
        public ICommand ButtonCommand => new RelayCommand(
                    excute =>
                    {
                        // Model ->ViewModel
                        UserModel userModel = new UserModel()
                        {
                            ID = Guid.NewGuid().ToString("N"),
                            Name = "Bili执笔小白",
                            Email = "2806933146@qq.com"
                        };

                        #region 清空字符串(可省略)
                        Name = string.Empty;
                        Email = string.Empty;
                        Log = string.Empty;
                        #endregion 清空字符串(可省略)

                        Name = userModel.Name;
                        Email = userModel.Email;
                    },
                    canExcute => { return true; }
                );

        /// <summary>
        /// 和作者打招呼Command
        /// 数据流向:ViewModel-> View
        /// </summary>
        public ICommand Button1Command => new RelayCommand(
                    excute =>
                    {
                        Log = "你好!" + Name;
                    },
                    canExcute => { return true; }
                );
        #endregion 控件事件
    }
}

5、实现INotifyPropertyChanged

  INotifyPropertyChanged:用于ViewModel通知View刷新;ViewModel继承该类,并实现PropertyChanged、OnPropertyChanged;

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace WPF_MVVM_Simple.ViewModel
{
    /// <summary>
    /// ViewModel基类,主要为实现INotifyPropertyChanged接口
    /// </summary>
    public class ViewModelBase : INotifyPropertyChanged
    {
        /// <summary>
        /// MVVM 绑定事件
        /// </summary>
        public event PropertyChangedEventHandler? PropertyChanged;

        /// <summary>
        /// 数据变更时通知UI
        /// </summary>
        /// <param name="propName">绑定的值</param>
        protected virtual void OnPropertyChanged(string propName)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));
        }
    }
}

6、实现ICommand

  ICommand:用于VIew通知ViewModel响应控件事件;创建ICommand的实现类RelayCommand,并实现CanExecuteChanged,CanExecute,Execute;

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;

namespace WPF_MVVM_Simple.Commands
{
    /// <summary>
    /// 用ICommand解耦‘用户界面’与‘事件’,即控件事件不写在页面的默认.cs代码中
    /// 这是实现MVVM(Model-View-ViewModel)设计模式的关键部分
    /// </summary>
    public class RelayCommand : ICommand
    {
        #region 变量
        /// <summary>
        /// 执行操作
        /// </summary>
        private Action<object?> _Excute;
        /// <summary>
        /// 如果可以执行Execute方法,则返回true;否则返回false
        /// </summary>
        private Func<object?, bool> _CanExcute;

        /// <summary>
        /// 当CanExecute的返回值可能发生更改时,会触发该事件
        /// </summary>
        public event EventHandler? CanExecuteChanged;
        #endregion 变量

        /// <summary>
        /// 如果可以执行Execute方法,则返回true;否则返回false
        /// </summary>
        /// <param name="parameter"></param>
        /// <returns></returns>
        public bool CanExecute(object? parameter)
        {
            return _CanExcute == null ? true : _CanExcute(parameter);
        }

        /// <summary>
        /// 执行操作
        /// </summary>
        /// <param name="parameter"></param>
        public void Execute(object? parameter)
        {
            if (_Excute != null)
                _Excute(parameter);
        }

        /// <summary>
        /// 构造函数
        /// </summary>
        /// <param name="ExcuteMethod"></param>
        /// <param name="CanExcuteMethod"></param>
        public RelayCommand(Action<object?> ExcuteMethod, Func<object?, bool> CanExcuteMethod)
        {
            _Excute = ExcuteMethod;
            _CanExcute = CanExcuteMethod;
        }
    }
}

7、效果

8、补充:

 (1)若ViewModel中有List<>或Array[]类型的变量,请使用ObservableCollection<>代替;ObservableCollection知识见:WPF-双向绑定通知机制之ObservableCollection

三、简化MVVM代码量(CommunityToolkit.MVVM)

  项目地址:https://gitee.com/qq28069933146_admin/wpf_mvvm_simple

  在一、中我们完成了原生MVVM的开发,能够发现一个问题,ViewModel中我们添加一个成员对象就需要添加下面一段代码

        private string _name = string.Empty;
        /// <summary>
        /// 名字
        /// </summary>
        public string Name
        {
            get => _name;
            set
            {
                _name = value;
                OnPropertyChanged("Name");  // 通知UI
            }
        }

  那我们能不能像平时写model一样,只添加public string Name{get;set;} = string.Empty;呢,大概是肯定的。我们只需要引入CommunityToolkit.MVVM包即可;当然代码也不可能是public string Name{get;set;} = string.Empty;,而是[ObservableProperty]private string _name = string.Empty;,类名也需要继承ObservableObject并添加partial修饰词,完整代码如下:

using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
using System.Xml.Linq;
using WPF_CommunityToolkitMVVM_Simple.Model;

namespace WPF_CommunityToolkitMVVM_Simple.ViewModel
{
    /// <summary>
    /// MainWindow的ViewModel类
    /// 集合 请使用 ObservableCollection<MainViewModel>
    /// </summary>
    public partial class MainViewModel : ObservableObject
    {
        #region 控件 Binding
        /// <summary>
        /// 名字
        /// </summary>
        [ObservableProperty]
        private string _name = string.Empty;

        /// <summary>
        /// 邮箱
        /// </summary>
        [ObservableProperty]
        private string _email = string.Empty;

        /// <summary>
        /// 打招呼的日志
        /// </summary>
        [ObservableProperty]
        private string _log = string.Empty;
        #endregion 控件 Binding

        #region 控件事件(可省略ButtonCommand变量直接写ButtonFunCommand;但为了好找到ButtonCommand,本文章不推荐省略;)
        /// <summary>
        /// 获取作者信息Command
        /// 数据流向:Model-> ViewModel-> View
        /// </summary>
        [RelayCommand]
        //public void ButtonFun(bool bool1)
        public void ButtonFun()
        {
            // Model ->ViewModel
            UserModel userModel = new UserModel()
            {
                ID = Guid.NewGuid().ToString("N"),
                Name = "Bili执笔小白",
                Email = "2806933146@qq.com"
            };

            #region 清空字符串(可省略)
            Name = string.Empty;
            Email = string.Empty;
            Log = string.Empty;
            #endregion 清空字符串(可省略)

            Name = userModel.Name;
            Email = userModel.Email;
        }

        //private RelayCommand<User>? buttonCommand;
        //public IRelayCommand<User> ButtonCommand => buttonCommand ??= new RelayCommand<bool>(ButtonFun);
        private RelayCommand? buttonCommand;
        public IRelayCommand ButtonCommand => buttonCommand ??= new RelayCommand(ButtonFun);


        /// <summary>
        /// 和作者打招呼Command - 异步
        /// 数据流向:ViewModel-> View
        /// </summary>
        [RelayCommand]
        public async Task Button1Fun()
        {
            await Task.Run(() =>
            {
                 Log = "你好!" + Name;
            });
        }

        private AsyncRelayCommand? button1Command;
        public IRelayCommand Button1Command => button1Command ??= new AsyncRelayCommand(Button1Fun);
        #endregion 控件事件(可省略ButtonCommand变量直接写ButtonFunCommand;但为了好找到ButtonCommand,本文章不推荐省略;)
    }
}

1、CommunityToolkit.MVVM可简化哪些代码

  • 可简化上面代码块中的代码;即ViewModel继承ObservableObject并添加partial修饰词,即可省略OnPropertyChanged
  • 省略OnPropertyChanged就意味着可删除ViewModelBase类;
  • ObservableObject自带RelayCommand的实现,可删除我们项目里的RelayCommand类(使用案例如上代码);
  • 可省略ButtonCommand变量直接写ButtonFunCommand;但为了好找到ButtonCommand,本文章不推荐省略;

2、解析RelayCommand类

(1)RelayCommand无参与有参

  无参

        // 可省略ButtonCommand变量直接写ButtonFunCommand;但为了好找到ButtonCommand,本文章不推荐省略;
        [RelayCommand]
        //public void ButtonFun(bool bool1)
        public void ButtonFun()
        {
        }
        private RelayCommand? buttonCommand;
        public IRelayCommand ButtonCommand => buttonCommand ??= new RelayCommand(ButtonFun);

  有参

        // 可省略ButtonCommand变量直接写ButtonFunCommand;但为了好找到ButtonCommand,本文章不推荐省略;
        [RelayCommand]
        public void ButtonFun(User user1)
        {
        }

        private RelayCommand<User>? buttonCommand;
        public IRelayCommand<User> ButtonCommand => buttonCommand ??= new RelayCommand<User>(ButtonFun);

(2)AsyncRelayCommand异步无参与有参

  无参

        // 可省略ButtonCommand变量直接写ButtonFunCommand;但为了好找到ButtonCommand,本文章不推荐省略;        
        [RelayCommand]
        public async Task Button1Fun()
        {
            // 示例代码1
            await Task.Run(() =>
            {
                ...
            });
            
            // 示例代码2
            await ...
        }

        private AsyncRelayCommand? button1Command;
        public IRelayCommand Button1Command => button1Command ??= new AsyncRelayCommand(Button1Fun);

  有参

        // 可省略ButtonCommand变量直接写ButtonFunCommand;但为了好找到ButtonCommand,本文章不推荐省略;
        [RelayCommand]
        public async Task Button1Fun(User user1)
        {
            // 示例代码1
            await Task.Run(() =>
            {
                ...
            });
            
            // 示例代码2
            await ...
        }

        private AsyncRelayCommand<User>? button1Command;
        public IRelayCommand<User> Button1Command => button1Command ??= new AsyncRelayCommand<User>(Button1Fun);

3、IOCDI(CommunityToolkit.MVVM)

  大家或许会疑惑CommunityToolkit.MVVM是一个MVVM库,为啥有IOCDI功能。因为在CommunityToolkit.MVVM类库之前,微软提供了MVVMLight用于实现MVVM;CommunityToolkit.MVVM的IOCDI功能继承于MVVMLight。

所以CommunityToolkit.MVVM的IOCDI可学可不学,微软推荐过的WPF IOCDI库有 Microsoft.Extensions.DependencyInjection(首选)、Unity;

using Microsoft.Extensions.DependencyInjection;   // .NET Core内置依赖注入模块。
using CommunityToolkit.Mvvm.DependencyInjection;  // mvvm框架的内置的依赖注入模块。

  Microsoft.Extensions.DependencyInjection教程:https://learn.microsoft.com/zh-cn/dotnet/communitytoolkit/mvvm/ioc;  

四、窗体事件如何使用MVVM

1、安装Nuget包

  窗体事件使用MVVM需要使用安装Negut包 Microsoft.Xaml.Behaviors.Wpf

2、XAML引用标签

  xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"

3、事件用法

  Behavior:行为,主要有鼠标、键盘、数据、位置等变化行为;

  Triggers:触发器;可用来给事件绑定触发器;

    <i:Interaction.Triggers>
        <i:EventTrigger EventName="Loaded">
            <i:InvokeCommandAction Command="{Binding LoadedCommand}"/>
        </i:EventTrigger>
        <i:EventTrigger EventName="Closing">
            <i:InvokeCommandAction Command="{Binding ClosingCommand}"/>
        </i:EventTrigger>
        <i:EventTrigger EventName="Closed">
            <i:InvokeCommandAction Command="{Binding ClosedCommand}"/>
        </i:EventTrigger>
    </i:Interaction.Triggers>

4、完整代码

<Window x:Class="WPF_CommunityToolkitMVVM_Simple.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:i="http://schemas.microsoft.com/xaml/behaviors"
        xmlns:local="clr-namespace:WPF_CommunityToolkitMVVM_Simple"
        xmlns:vModel="clr-namespace:WPF_CommunityToolkitMVVM_Simple.ViewModel"
        mc:Ignorable="d"
        Title="CommunityToolkitMVVM快捷MVVM示例" Height="450" Width="800">
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="Loaded">
            <i:InvokeCommandAction Command="{Binding LoadedCommand}"/>
        </i:EventTrigger>
        <i:EventTrigger EventName="Closing">
            <i:InvokeCommandAction Command="{Binding ClosingCommand}"/>
        </i:EventTrigger>
        <i:EventTrigger EventName="Closed">
            <i:InvokeCommandAction Command="{Binding ClosedCommand}"/>
        </i:EventTrigger>
    </i:Interaction.Triggers>
    <Window.DataContext>
        <vModel:MainViewModel/>
    </Window.DataContext>
    <Grid>
    </Grid>
</Window>
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
using System.Xml.Linq;
using WPF_CommunityToolkitMVVM_Simple.Model;

namespace WPF_CommunityToolkitMVVM_Simple.ViewModel
{
    /// <summary>
    /// MainWindow的ViewModel类
    /// 集合 请使用 ObservableCollection<MainViewModel>
    /// </summary>
    public partial class MainViewModel : ObservableObject
    {
        #region 控件事件-简写
        /// <summary>
        /// 加载、关闭
        /// </summary>
        [RelayCommand]
        public void Loaded()
        {
            Debug.WriteLine("Loaded");
        }
        [RelayCommand]
        public void Closing()
        {
            Debug.WriteLine("Closing");
        }
        [RelayCommand]
        public  async Task Closed()
        {
            await Task.Run(() =>
            {
                Debug.WriteLine("Closed");
            });
        }
        #endregion 控件事件-简写
    }
}

 

posted @ 2024-07-22 16:23  ꧁执笔小白꧂  阅读(586)  评论(0编辑  收藏  举报