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 控件事件-简写
}
}
本文来自博客园,作者:꧁执笔小白꧂,转载请注明原文链接:https://www.cnblogs.com/qq2806933146xiaobai/p/18316292