一、MVVM简介
MVC Model View Control
MVP
MVVM即Model-View-ViewModel,MVVM模式与MVP(Model-View-Presenter)模式相似,主要目的是分离视图(View)和模型(Model),具有低耦合、可重用性、独立开发、可测试性等优点。
MVVM框架有很多,开源的主要有:
-
PRISM:由微软提供,和MEF/Unity一起用于依赖注入,支持组合命令,可以扩展。MSDN上有详细的教程和演练。
-
MVVM Light Toolkit:有visual Studio和Expression Blend的项目和项的模板。更多信息请看这里,另外可以参考VS和Expression Blend的使用教程。
-
Caliburn Micro:支持视图模型先行(ViewModel-First)和视图先行(View-First)两种开发方式,通过co-routine支持异步编程。
-
Simple MVVM Toolkit:提供VS项目和项的模板,依赖注入,支持深拷贝以及模型和视图模型之间的属性关联。
-
Catel:包含项目和项的模板,用户控件和企业类库。支持动态视图模型注入,视图模型的延迟加载和验证。还支持WP7专用的视图模型服务。
闭源框架主要有:
-
Intersoft ClientUI:付费的,只支持WPF和Silverlight,但是,除了MVVM框架,它还提供其它一些特性。
-
Vidyano:免费但不开源。带有实体映射/虚拟持久化对象(数据容器),业务规则以及内置基于ACL的安全特性。
二、原生版本的MVVM实例
Models 存放的是数据模型
Service存放的是业务逻辑
ViewModels存放的便是视图模型
Views存放WPF窗口
2.1 Models文件夹中创建一个用户模型User.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Wpfmvvm_demo.Models
{
public class User
{
/// <summary>
/// 用户名
/// </summary>
public string? Name { get; set; }
/// <summary>
/// 密码
/// </summary>
public string? Password { get; set; }
}
}
2.2 在Services文件夹中添加用户的业务逻辑UserService.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Wpfmvvm_demo.Models;
namespace Wpfmvvm_demo.Services
{
public class UserService
{
/// <summary>
/// 获取所有用户方法
/// </summary>
/// <returns></returns>
public List<User> GetAllUser()
{
List<User> users = new ();
for (int i = 0; i < 3; i++)
{
var user = new User();
user.Name = "用户" + i;
user.Password = "密码" + i;
users.Add(user);
}
return users;
}
}
}
2.3 在ViewModels中创建NotificationObject.cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Wpfmvvm_demo.ViewModels
{
public class NotificationObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
public void RaisePropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
}
2.4 在ViewModels文件中创建DelegateCommand.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
namespace Wpfmvvm_demo.ViewModels
{
public class DelegateCommand : ICommand
{
public event EventHandler? CanExecuteChanged;
public Func<object?,bool>? DoCanExecute { get; set; }
public Action<object?>? DoExecute { get; set; }
public bool CanExecute(object? parameter)
{
if (DoCanExecute != null)
{
return DoCanExecute(parameter);
}
return true;
}
public void Execute(object? parameter)
{
if (DoExecute != null)
{
DoExecute.Invoke(parameter);
}
}
}
}
2.5 ViewModels中创建主窗口的视图模型MainWindowViewModel.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Wpfmvvm_demo.Models;
using Wpfmvvm_demo.Services;
namespace Wpfmvvm_demo.ViewModels
{
public class MainWindowViewModel : NotificationObject
{
/// <summary>
/// 用户List
/// </summary>
private List<User>? users;
public List<User>? Users
{
get { return users; }
set
{
users = value;
RaisePropertyChanged("Users");
}
}
/// <summary>
/// 程序名
/// </summary>
public string AppName { get; set; }
/// <summary>
/// 电话
/// </summary>
private string? phone;
public string? Phone
{
get { return phone; }
set
{
phone = value;
RaisePropertyChanged("Phone");
}
}
/// <summary>
/// 获取所有用户命令
/// </summary>
public DelegateCommand? GetAllUsersCommand { get; set; }
/// <summary>
/// 构造初始化
/// </summary>
public MainWindowViewModel()
{
AppName = "WPF MVVM 模式测试";
Phone = "123456";
GetAllUsersCommand = new DelegateCommand
{
DoExecute = new Action<object?>(GetAllUsersCommandExecute)
};
}
/// <summary>
/// 获取所有用户命令执行方法
/// </summary>
private void GetAllUsersCommandExecute(object? paramater)
{
Phone = Phone!.Equals("123456") ? "1234567" : "123456";
UserService userService = new UserService();
Users = userService.GetAllUser();
}
}
}
2.6 设计MainWindow.xaml界面
<Window x:Class="Wpfmvvm_demo.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:Wpfmvvm_demo"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="550">
<Grid>
<StackPanel Orientation="Vertical">
<StackPanel Orientation="Horizontal">
<Label Content="程序名"></Label>
<Label Content="{Binding AppName}"></Label>
</StackPanel>
<StackPanel Orientation="Horizontal">
<Label Content="电话"></Label>
<Label Content="{Binding Phone}"></Label>
</StackPanel>
<StackPanel>
<Button Content="获取所有用户" Command="{Binding GetAllUsersCommand}"></Button>
</StackPanel>
<DataGrid ItemsSource="{Binding Users}" AutoGenerateColumns="False" GridLinesVisibility="All" CanUserDeleteRows="False" CanUserAddRows="False" >
<DataGrid.Columns>
<DataGridTextColumn Header="用户名" Binding="{Binding Name}"></DataGridTextColumn>
<DataGridTextColumn Header="密码" Binding="{Binding Password}"></DataGridTextColumn>
</DataGrid.Columns>
</DataGrid>
</StackPanel>
</Grid>
</Window>
2.7 把ViewModel数据绑定到窗口
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
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 Wpfmvvm_demo.ViewModels;
namespace Wpfmvvm_demo
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new MainWindowViewModel();
}
}
}
三、MVVM Toolkit
微软虽然提出了 MVVM,但又没有提供一个官方的 MVVM 库(多年前有过 Prism,但已经离家出走了)。每次有人提起 MVVM 库,有些人会推荐 Prism(例如我),有些人会推荐 MVVMLight。可是现在 Prism 已经决定不再支持 UWP , 而 MVVMLight 又不再更新,在这左右为难的时候 Windows Community Toolkit 挺身而出发布了 MVVM Toolkit。 MVVM Toolkit 延续了 MVVMLight 的风格,是一个轻量级的组件,而且它基于 .NET Standard 2.0,可用于UWP, WinForms, WPF, Xamarin, Uno 等多个平台。相比它的前身 MVVMLight,它有以下特点:
更高:版本号更高,一出手就是 7.0。
更快:速度更快,MVVM Toolkit 从一开始就以高性能为实现目标。
更强:后台更强,MVVM Toolkit 的全称是 'Microsoft.Toolkit.Mvvm',根正苗红。
这个包主要提供了如下的
Microsoft.Toolkit.Mvvm.ComponentModel
ObservableObject
ObservableRecipient
ObservableValidator
Microsoft.Toolkit.Mvvm.DependencyInjection
Ioc
Microsoft.Toolkit.Mvvm.Input
RelayCommand
AsyncRelayCommand
IRelayCommand
IAsyncRelayCommand
Microsoft.Toolkit.Mvvm.Messaging
IMessenger
WeakReferenceMessenger
StrongReferenceMessenger
IRecipient
MessageHandler
Microsoft.Toolkit.Mvvm.Messaging.Messages
PropertyChangedMessage
RequestMessage
AsyncRequestMessage
CollectionRequestMessage
AsyncCollectionRequestMessage
ValueChangedMessage
3.1 可观察对象
public class UserVM : ObservableObject
{
private string name;
public string Name
{
get => name;
set => SetProperty(ref name, value);
}
private int age;
public int Age
{
get => age;
set => SetProperty(ref age, value);
}
}
页面的类中添加
public partial class MainWindow : Window
{
private UserVM userVM = new UserVM();
public MainWindow()
{
InitializeComponent();
DataContext = userVM;
Task.Run(() =>
{
while (true)
{
userVM.Age += 1;
Thread.Sleep(1000);
}
});
}
}
页面中
<Grid>
<StackPanel>
<TextBlock FontSize="30" Text="{Binding Age}" />
</StackPanel>
</Grid>
我们就可以看到数字就会一直递增了。
3.2 命令
页面中我们添加命令
<StackPanel Height="40" Orientation="Horizontal">
<TextBlock FontSize="30" Text="{Binding Name}" />
<TextBlock FontSize="30" Text="{Binding Age}" />
<Button Command="{Binding IncrementAgeCommand}" Content="年龄递增" />
</StackPanel>
对应的ViewModel中添加命令及响应的事件
public class UserVM : ObservableObject
{
private string name;
public string Name
{
get => name;
set => SetProperty(ref name, value);
}
private int age;
public int Age
{
get => age;
set => SetProperty(ref age, value);
}
public ICommand IncrementAgeCommand { get; }
public UserVM()
{
IncrementAgeCommand = new RelayCommand(IncrementAge);
}
private void IncrementAge() => Age++;
}
这样只要我们点击按钮,年龄就会递增1。
3.3 消息机制
3.3.1 注册与发送
添加传递消息的类
public class ZMessage
{
public int Code { get; set; }
public string Message { get; set; }
public ZMessage(int code, string msg)
{
Code = code;
Message = msg;
}
}
消息接收与发送
public MainWindow()
{
InitializeComponent();
initMessage();
}
private void initMessage()
{
WeakReferenceMessenger.Default.Register<ZMessage>(
this,
(r, m) =>
{
Console.WriteLine("接收到信息:" + m.Message);
}
);
WeakReferenceMessenger.Default.Send(new ZMessage(100, "Hello"));
}
3.3.2 可接收消息的类
当然我们也可以让我们的ViewModel接收消息
public class UserVM : ObservableRecipient, IRecipient<ZMessage>
{
private string name;
public string Name
{
get => name;
set => SetProperty(ref name, value);
}
private int age;
public int Age
{
get => age;
set => SetProperty(ref age, value);
}
public UserVM()
{
IsActive = true;
}
public void Receive(ZMessage message)
{
Name = message.Message;
Age = message.Code;
}
}
这里一定要注意
-
要继承
ObservableRecipient
类,实现IRecipient<>
接口。 -
只有设置
IsActive = true;
,才能接收消息。
我们还是在页面的类中发送消息
WeakReferenceMessenger.Default.Send(new ZMessage(18, "XiaoMing"));
页面也稍做修改
<StackPanel Orientation="Horizontal">
<TextBlock FontSize="30" Text="{Binding Name}" />
<TextBlock FontSize="30" Text="{Binding Age}" />
</StackPanel>
我们会发现页面上已经变更为我们发送消息的数据了。
3.4 控制反转(IOC)
添加依赖库Microsoft.Extensions.DependencyInjection
Install-Package Microsoft.Extensions.DependencyInjection -Version 6.0.0
创建我们要自动注入的类
加入如下是我们用户相关的服务
public interface IUserService
{
string getUserName();
}
和
public class UserService : IUserService
{
public string getUserName()
{
return "XiaoMing";
}
}
添加注入的控制
public partial class App : Application
{
public App()
{
Services = ConfigureServices();
this.InitializeComponent();
}
public new static App Current => (App)Application.Current;
public IServiceProvider Services { get; }
private static IServiceProvider ConfigureServices()
{
var services = new ServiceCollection();
services.AddSingleton<IUserService, UserService>();
// ViewModels
services.AddTransient<UserVM>();
return services.BuildServiceProvider();
}
}
其中
-
AddSingleton 单一实例服务
-
AddScoped 范围内的服务
-
AddTransient 暂时性服务
权重:
AddSingleton`→`AddTransient`→`AddScoped
生命周期:
-
AddSingleton 项目启动-项目关闭 相当于静态类 只会有一个 每一次获取的对象都是同一个
-
AddScoped 请求开始-请求结束 在这次请求中获取的对象都是同一个 请求时创建
-
AddTransient 请求获取-(GC回收-主动释放) 获取时创建 每一次获取的对象都不是同一个
注意:
由于AddScoped对象是在请求的时候创建的 所以不能在AddSingleton对象中使用 甚至也不能在AddTransient对象中使用
使用
private UserVM userVM = (UserVM)App.Current.Services.GetService(typeof(UserVM));
private IUserService userService = (IUserService)App.Current.Services.GetService(typeof(IUserService));
这样是不是感觉还麻烦了
但是如果我们的ViewModel是这样的
public class UserVM : ObservableObject
{
private string name;
public string Name
{
get => name;
set => SetProperty(ref name, value);
}
private int age;
public int Age
{
get => age;
set => SetProperty(ref age, value);
}
public ICommand IncrementAgeCommand { get; }
public IUserService userService { get; }
public UserVM(IUserService _userService)
{
userService = _userService;
IncrementAgeCommand = new RelayCommand(IncrementAge);
}
private void IncrementAge() => Age++;
}
这里的IUserService
的实例并没有传入但是就可以用了,因为IOC框架已经自动注入了。
四、WPF Prism
4.1 常见的MVVM框架
众所周知, 如果你了解WPF当中的ICommand, INotifyPropertyChanged的作用, 就会发现众多框架都是基于这些进行扩展, 实现其通知、绑定、命令等功能。
对于不同的MVVM框架而言, 大体使用上会在声明方式上的差异, 以及特定功能上的差别。
下面列举了常用的3个MVVM框架,他们的一些差异。如下所示:
功能↓ / →框架名 | Prism | Mvvmlight | Micorosoft.Toolkit.Mvvm |
---|---|---|---|
通知 | BindableBase | ViewModelBase | ObservableObject |
命令 | DelegateCommand | RelayCommand | Async/RelayCommand |
聚合器 | IEventAggregator | IMessenger | IMessenger |
模块化 | √ | × | × |
容器 | √ | × | × |
依赖注入 | √ | × | × |
导航 | √ | × | × |
对话 | √ | × | × |
正如你所见, 各个框架之间都有各自的通知、绑定、事件聚合器等基础的功能, 而Prsim自带的依赖注入、容器、以及导航会话等功能, 可以为你提供更加强大的功能。
当然,在实际的开发过程当中, 可以根据实际的功能需求, 对不同的框架选型, 同时这也需要你对各个框架之间的优缺点进行判断。
那么, 下面将主要介绍Prism
4.2 BindableBase
在Prism当中, 你需要继承于BindableBase
public class TestViewModel : BindableBase
{
private string _message;
public string Message
{
get { return _message; }
set { _message = value; RaisePropertyChanged(); }
}
}
在Prism当中, 你可以使用DelegateCommand及带参数的Command
public class TestViewModel : ViewModelBase
{
public DelegateCommand SendCommand { get; set; }
public DelegateCommand<string> SendMessageCommand { get; set; }
}
4.3 CompositeCommand
对于单个Command而言, 只是触发单个对应的功能, 而复合命令是Prism当中非常强大的功能, CompositeCommand简单来说是一个父命令, 它可以注册N个子命令
当父命令被激活, 它将触发对所有的子命令, 如果任意一个命令CanExecute=false,它将无法被激活,如下所示:
public class MyViewModel : NotificationObject
{
private readonly CompositeCommand saveAllCommand;
public ArticleViewModel(INewsFeedService newsFeedService,
IRegionManager regionManager,
IEventAggregator eventAggregator)
{
this.saveAllCommand = new CompositeCommand();
this.saveAllCommand.RegisterCommand(new SaveProductsCommand());
this.saveAllCommand.RegisterCommand(new SaveOrdersCommand());
}
public ICommand SaveAllCommand
{
get { return this.saveAllCommand; }
}
}
4.4 IEventAggregator
松耦合基于事件通讯
多个发布者和订阅者
微弱的事件
过滤事件
传递参数
取消订阅
该功能主要作用为, 事件聚合器负责接收订阅以及发布消息。订阅者可以接收到发布者发送的内容。
//创建事件
public class SavedEvent : PubSubEvent<string> { }
//发布
IEventAggregator.GetEvent<SavedEvent>().Publish("some value");
//订阅
IEventAggregator.GetEvent<SavedEvent>().Subscribe(.Subscribe(message=>
{
//do something
});
4.5 Filtering Events
在实际的开发过程当中,我们往往会在多个位置订阅一个事件, 但是对于订阅者而言, 他并不需要接收任何消息, 如下所示:
在Prism当中, 我们可以指定为事件指定过滤条件, 如下所示:
eventAggregator.GetEvent<MessageSentEvent>()
.Subscribe(arg =>
{
//do something
},
ThreadOption.PublisherThread,
false,
//设置条件为token等于“MessageListViewModel” 则接收消息
message => message.token.Equals(nameof(MessageListViewModel)));
关于Subscribe当中的4个参数, 详解:
-
1.action: 发布事件时执行的委托。
-
2.ThreadOption枚举: 指定在哪个线程上接收委托回调。
-
3.keepSubscriberReferenceAlive: 如果为true,则Prism.Events.PubSubEvent保留对订阅者的引用因此它不会收集垃圾。
-
4.filter: 进行筛选以评估订阅者是否应接收事件。
4.6 Unsubscribe
为注册的消息取消订阅, Prism提供二种方式取消订阅,如下:
1.通过委托的方式取消订阅
var event = IEventAggregator.GetEvent<MessageSentEvent>();
event.Subscribe(OnMessageReceived);
event.Unsubscribe(OnMessageReceived);
2.通过获取订阅者token取消订阅
var _event = eventAggregator.GetEvent<MessageSentEvent>();
var token = _event.Subscribe(OnMessageReceived);
_event.Unsubscribe(token);