WPF MVVM框架:CommunityToolkit.Mvvm包使用介绍

您所在的位置:网站首页 itemtemplate用法 WPF MVVM框架:CommunityToolkit.Mvvm包使用介绍

WPF MVVM框架:CommunityToolkit.Mvvm包使用介绍

2023-03-08 18:12| 来源: 网络整理| 查看: 265

微软官方文档:https://learn.microsoft.com/zh-cn/dotnet/communitytoolkit/mvvm/

 

最近在需要使用MVVM框架的时候才发现MvvmLight作者宣布停止更新了,有点可惜。

原作者推荐使用CommunityToolkit.Mvvm包,所以这里做一个CommunityToolkit.Mvvm包使用的全面的总结。

 

开发环境:

Visual Studio 2019 Windows 10 1903 CommunityToolkit.Mvvm 8.0.0 

 

CommunityToolkit.Mvvm

项目地址:https://github.com/CommunityToolkit/dotnet/tree/main/CommunityToolkit.Mvvm

 

CommunityToolkit.Mvvm 是一个现代、快速和模块化的 MVVM 库。 它是 CommunityToolkit的一部分。由 Microsoft 维护和发布,也是 .NET Foundation 的一部分。

 

特点如下:

平台和运行时独立 - .NET Standard 2.0、 .NET Standard 2.1 和 .NET 6 易于选取和使用 - 无需对应用程序结构或编码范例的严格要求, (“MVVM”) 之外,即灵活使用。 笛卡尔 - 自由选择要使用的组件,包中的所有类型都是松散耦合的。 参考实现 - 精益和高性能,为基类库中包含的接口提供实现,但缺少直接使用它们的具体类型。

 

CommunityToolkit.Mvvm包中的类型定义

CommunityToolkit.Mvvm.ComponentModel ObservableObject ObservableRecipient ObservableValidator CommunityToolkit.Mvvm.DependencyInjection Ioc CommunityToolkit.Mvvm.Input RelayCommand RelayCommand AsyncRelayCommand AsyncRelayCommand IRelayCommand IRelayCommand IAsyncRelayCommand IAsyncRelayCommand CommunityToolkit.Mvvm.Messaging IMessenger WeakReferenceMessenger StrongReferenceMessenger IRecipient MessageHandler CommunityToolkit.Mvvm.Messaging.Messages PropertyChangedMessage RequestMessage AsyncRequestMessage CollectionRequestMessage AsyncCollectionRequestMessage ValueChangedMessage

这里的类型不算太多,目前我只介绍一些我在项目中使用到的类型,应该能满足大部使用场景了。

 

ViewModelBase

在MvvmLight中,ViewModel一般都会继承自ViewModelBase类,在CommunityToolkit.Mvvm中,具有相同功能的类是ObservableObject。

ObservableObject实现了INotifyPropertyChanged和INotifyPropertyChanging接口,可以作为属性更改引发通知事件的基类。

 

ObservableObject提供了以下功能(说明:每个功能下都贴出了部分实现代码,大概知道是怎么实现的。如果想要深入了解的话,可以去读一下源码。)

1.NotifyPropertyChanged 和 INotifyPropertyChanging接口的实现,公开了PropertyChanged and PropertyChanging事件。

2.公开派生类型中可以重写的 OnPropertyChanged 和 OnPropertyChanging 方法,以便自定义如何引发通知事件。

public abstract class ObservableObject : INotifyPropertyChanged, INotifyPropertyChanging { public event PropertyChangedEventHandler? PropertyChanged; public event PropertyChangingEventHandler? PropertyChanging; protected virtual void OnPropertyChanged(PropertyChangedEventArgs e) { ... PropertyChanged?.Invoke(this, e); } protected virtual void OnPropertyChanging(PropertyChangingEventArgs e) { ... PropertyChanging?.Invoke(this, e); } }

3.SetProperty函数,在MvvmLight中,也有一个类似的的函数Set(...),可以让属性值更改时引发通知事件变得更加简单。

protected bool SetProperty([NotNullIfNotNull(nameof(newValue))] ref T field, T newValue, [CallerMemberName] string? propertyName = null) { OnPropertyChanging(propertyName); ... OnPropertyChanged(propertyName); ... }

4.SetPropertyAndNotifyOnCompletion函数,它和SetProperty函数的功能类似,将负责更新目标字段、监视新任务(如果存在)以及在该任务完成时引发通知事件.

protected bool SetPropertyAndNotifyOnCompletion([NotNull] ref TaskNotifier? taskNotifier, Task? newValue, [CallerMemberName] string? propertyName = null) { return SetPropertyAndNotifyOnCompletion(taskNotifier ??= new TaskNotifier(), newValue, null, propertyName); } private bool SetPropertyAndNotifyOnCompletion(ITaskNotifier taskNotifier, TTask? newValue, Action? callback, [CallerMemberName] string? propertyName = null) where TTask : Task { ... bool isAlreadyCompletedOrNull = newValue?.IsCompleted ?? true; OnPropertyChanging(propertyName); taskNotifier.Task = newValue; OnPropertyChanged(propertyName); if (isAlreadyCompletedOrNull) { if (callback is not null) { callback(newValue); } return true; } ... }

如何使用ObservableObject类

下面会用几个小例子来演示一下如何使用ObservableObject类。

 

简单属性

 在MvvmLight中,包装属性通知使用的是Set函数

Set(string propertyName, ref T field, T newValue = default, bool broadcast = false);

在CommunityToolkit.Mvvm中,使用的是SetProperty函数。由于propertyName参数增加了CallerMemberName特性,所以并不需要我们手动再去指定,可以直接为空。

protected bool SetProperty([global::System.Diagnostics.CodeAnalysis.NotNullIfNotNull("newValue")] ref T field, T newValue, [global::System.Runtime.CompilerServices.CallerMemberName] string? propertyName = null) { if (global::System.Collections.Generic.EqualityComparer.Default.Equals(field, newValue)) { return false; } field = newValue; OnPropertyChanged(propertyName); return true; }

下面用一个小例子演示一下。

在界面上放置一个TextBox,Content绑定到CurrentTime属性

ViewModel如下:

public class ObservableObjectPageViewModel : CommunityToolkit.Mvvm.ComponentModel.ObservableObject { private string currentTime; public string CurrentTime { get => currentTime; set => SetProperty(ref currentTime, value); } }

然后我们在ViewModel中启动一个定时器,用于更新时间

...... public ObservableObjectPageViewModel() { StartUpdateTimer(); } private void StartUpdateTimer() { System.Windows.Threading.DispatcherTimer dispatcherTimer = new System.Windows.Threading.DispatcherTimer(); dispatcherTimer.Interval = TimeSpan.FromSeconds(1); dispatcherTimer.Tick += (a, b) => UpdateTime(); dispatcherTimer.Start(); } .......

 

运行后,可以看到时间在更新

包装非Observable的模型

在日常开发中,可能有些数据模型是来自数据库或其它地方,而这些模型不允许我们去重新定义,但是我们又想在属性更改时触发通知事件,这个时候就可以重新包装这些非Observable的数据模型。

 

有如下的来自数据库的数据模型:

public class Student { public string ID { get; set; } public string Name { get; set; } }

可以把它包装成ObservableStudent

这里的SetProperty使用的是如下重载:

protected bool SetProperty(T oldValue, T newValue, TModel model, global::System.Action callback, [global::System.Runtime.CompilerServices.CallerMemberName] string? propertyName = null) where TModel : class { if (global::System.Collections.Generic.EqualityComparer.Default.Equals(oldValue, newValue)) { return false; } callback(model, newValue); OnPropertyChanged(propertyName); return true; }

T OldValue : 属性的当前值。T newValue: 属性的新值Tmodel:正在包装的目标模型Action:如果属性的新值与当前属性不同,并且需要设置属性。由此回调函数完成。

包装后如下:

public class ObservableStudent : ObservableObject { private readonly Student student; public ObservableStudent(Student student) => this.student = student; public string Name { get => student.Name; set => SetProperty(student.Name, value, student, (u, n) => u.Name = n); } public string ID { get => student.ID; set => SetProperty(student.ID, value, student, (u, n) => u.ID = n); } }

在界面上放置一个ListBox,绑定到StudentList

ViewModel

public class ObservableObjectPageViewModel : CommunityToolkit.Mvvm.ComponentModel.ObservableObject { private ObservableCollection studentList; public ObservableCollection StudentList { get => studentList; set => SetProperty(ref studentList, value); } private ObservableStudent selectedStudent; public ObservableStudent SelectedStudent { get => selectedStudent; set => SetProperty(ref selectedStudent, value); } public ObservableObjectPageViewModel() { InitStudentList(); } private void InitStudentList() { //假设这些数据来自数据库 var dbStudentList = GetDemoData(); StudentList = new ObservableCollection(dbStudentList.Select(x => new ObservableStudent(x))); } private List GetDemoData() { var list = new List(); Student student1 = new Student() { ID = "1", Name = "相清" }; Student student2 = new Student() { ID = "2", Name = "濮悦" }; list.Add(student1); list.Add(student2); return list; } }

运行结果如下:

 

 

如果没有再次包装成ObservableStudent,直接使用的Student。显示到界面是没有问题的,但是在更改某一项的某个属性时,就会发现界面不会实时刷新。

包装成ObservableStudent后,更改属性值时,界面也会同步更新

 

 

Task属性

日常开发中,我还没有使用过将Task类型包装成属性,一般是直接将需要显示的值定义成属性,等待一个Task的结果,然后绑定显示即可。

在CommunityToolkit.Mvvm包中,可以将Task直接包装成属性,并且能在任务完成后触发通知事件

因为这里官方的文档说得比较简单,示例代码只是演示了如何显示Task的状态,而并没有获取Task的结果,也是困扰了我几天。

后面查了一些资料,受到一些启发。前面在介绍ObservableObject的功能时,说到公开了PropertyChanged事件,这里这里正好可以利用这一点。

 

这里主要用到SetPropertyAndNotifyOnCompletion函数,跟SetProperty功能类似,但是会在Task完成时引发通用事件。

1 private bool SetPropertyAndNotifyOnCompletion(ITaskNotifier taskNotifier, TTask? newValue, Action? callback, [CallerMemberName] string? propertyName = null) 2 where TTask : Task 3 { 4 if (ReferenceEquals(taskNotifier.Task, newValue)) 5 { 6 return false; 7 } 13 bool isAlreadyCompletedOrNull = newValue?.IsCompleted ?? true; 15 OnPropertyChanging(propertyName); 17 taskNotifier.Task = newValue; 19 OnPropertyChanged(propertyName); 45 async void MonitorTask() 46 { 48 await newValue!.GetAwaitableWithoutEndValidation(); 51 if (ReferenceEquals(taskNotifier.Task, newValue)) 52 { 53 OnPropertyChanged(propertyName); 54 } ... } 61 62 MonitorTask(); 64 return true; 65 }

这里还有一个新的类型需要了解

TaskNotifier类型,

1 protected sealed class TaskNotifier : ITaskNotifier 2 { 3 public static implicit operator Task?(TaskNotifier? notifier); 4 }

它重新包装了System.Threading.Tasks.Task类型,在封装Task类型的属性时,需要用到它。

TaskNotifier支持直接使用Task进行强制类型转换

 

下面先演示一下如何在界面上显示一个Task的状态

在界面上放置一个Label,绑定到MyTask.Status(Converter代码在后面)

 定义一个Task属性MyTask

private TaskNotifier? myTask; public Task? MyTask { get => myTask; private set => SetPropertyAndNotifyOnCompletion(ref myTask, value); }

然后模拟一个Task,等待5秒返回一个字符串结果。

public ObservableObjectPageViewModel() { MyTask = GetTextAsync(); } private async Task GetTextAsync() { await Task.Delay(5000); return "任务执行后的结果"; }

Converter代码

public class TaskStatusConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { var status = (TaskStatus)value; switch(status) { case TaskStatus.RanToCompletion: return "任务完成"; default: return "加载中"; } } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { return DependencyProperty.UnsetValue; } } 1 Page.Resources> 2 3

运行后可以看到界面会在5秒后更新显示任务状态

 

如果还想在Task完成后,获取Task的结果,可以增加一个NotifyPropertyChanged事件处理程序方法。

 这里需要注意的是,要在MyTask赋值完成后,再增加NotifyPropertyChanged事件处理程序方法,否则会触发两次,在Task未完成时,调用Task.Resut会引起阻塞。

public ObservableObjectPageViewModel() { MyTask = GetTextAsync(); this.PropertyChanged += ObservableObjectPageViewModel_PropertyChanged; } private void ObservableObjectPageViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e) { if (e.PropertyName == nameof(MyTask)) { //在这里处理Task的结果 var result = MyTask.Result; } }

RelayCommand

 

ICommand接口是用于在 .NET 中为 Windows 运行时 应用编写的命令的代码协定。 这些命令为 UI 元素提供命令行为,如Button的Command。

RelayCommand实现了ICommand接口,可以将一个方法或委托绑定到视图(View)上。

MvvmLight中的命令类也叫RelayCommand,使用方法大同小异,但是在引发CanExeCutechanged事件时,有点区分,这点会在后面说明。

 

CommunityToolkit.Mvvm库中RelayCommand具备的功能如下(第1点和第2点跟MvvmLight中都是一样的,第3点有区别):

提供了ICommand接口的基本实现。 可以直接在构造函数中使用委托,如 Action 和Func,这也就意味着可以直接使用封装好的方法或lambda表达式。 实现了iRelayCommand(和iRelayCommand )接口,提供NotifyCanExecuteChanged方法来引发CanExeCutechanged事件。

 

下面看一个RelayCommand的简单使用

首先创建一个窗口,然后添加一个TextBox和一个Button,TextBox用于显示当前时间,绑定到CurrentTime属性,Button用于更新时间,命令绑定为UpdateCommand:

1 2 3 4

创建一个ViewModel类,继承自ObservableObject。增加属性CurrentTime和命令UpdateCommand

public class ObservableObjectPageViewModel : CommunityToolkit.Mvvm.ComponentModel.ObservableObject { private string currentTime; public string CurrentTime { get => currentTime; set => SetProperty(ref currentTime, value); } public ICommand UpdateCommand { get; set; } public ObservableObjectPageViewModel() { UpdateCommand = new RelayCommand(UpdateTime); } private void UpdateTime() { CurrentTime = DateTime.Now.ToString("F"); } }

设置窗口的DataContext

this.DataContext = new ViewModels.ObservableObjectPageViewModel();

运行后,单击按钮,可以在文本框显示时间

 

命令的CanExecute

在MvvmLight中,设置命令的CanExecute后,命令会自动去调用CanExecute去判断命令是否处于可用状态。

调用的时机可以参考

https://blog.walterlv.com/post/when-wpf-commands-update-their-states.html

 

在CommunityToolkit.Mvvm中,这里有点不一样。需要使用实现了IRelayCommand接口的类RelayCommand,然后再手动调用NotifyCanExecuteChanged()函数来进行通知

 

下面看一个小例子:

创建一个窗口,界面布局如下:

1 2 3

 ViewModel如下:

public class ObservableObjectPageViewModel : CommunityToolkit.Mvvm.ComponentModel.ObservableObject { private string inputText; public string InputText { get => inputText; set => SetProperty(ref inputText, value); } public ICommand MsgShowCommand { get; set; } public ObservableObjectPageViewModel() { MsgShowCommand = new RelayCommand(ShowMsg, CanShowMsgExecute); } private void ShowMsg() => MessageBox.Show(InputText); private bool CanShowMsgExecute() => !string.IsNullOrEmpty(InputText); }

此时我们运行程序后,输入文本,发现按钮并没有变成可用状态

将ICommand改成IRelayCommand,然后在InputText修改时,调用CanExecute通知

private string inputText; public string InputText { get => inputText; set { SetProperty(ref inputText, value); MsgShowCommand.NotifyCanExecuteChanged(); } } public IRelayCommand MsgShowCommand { get; set; }

再次运行,就可以达到预期效果

AsyncRelayCommand

AsyncRelayCommand提供了和RelayCommand一样的基础命令功能,但是在此基础上,增加了异步。

 

AsyncRelayCommand具备功能如下:

支持异步操作,可以返回Task。 使用带ConcellationToken重载的版本,可以取消Task。公开了CanBeCanceled和IsCancellationRequested属性,以及Cancel()方法。 公开ExecutionTask属性,可用于监视待处理操作的进度。公开 IsRunning属性,可以用于判断操作是否完成 实现了IAsyncRelayCommand and IAsyncRelayCommand接口。IAsyncRelayCommand就是在IRelayCommand接口的基础上增加异步操作的接口。

 

AsyncRelayCommand中定义的属性如下(部分翻译存在疑问,所以贴出了MSDN中的原文。):

 

CanBeCanceled

获取当前命令能否被取消

ExecutionTask

获取任务调度中的最后一个任务。 任务完成后,会引发属性更改通知事件(Gets the last scheduled Task, if available. This property notifies a change when the Task completes.)

IsCancellationRequested

获取是否已经请求取消当前操作

IsRunning

获取一个值,指示该命令当前是否是执行状态(Gets a value indicating whether the command currently has a pending operation being executed.)

 

 在官方的示例代码中,我看到了返回Task和直接在Task中处理结果两种情况。我这里都进行演示一下。

 

界面布局

3 4

界面上有两个Label,一个显示任务状态,一个显示任务结果

 

ViewModel

AsyncRelayCommand的构造函数需要传入一个返回Task类型的函数或委托。我这里定义了一个GetText函数,在函数里模拟等待了5秒(正常使用时,这个等待可以是任意一个耗时操作。)

public class AsyncRelayCommandPageViewModel : ObservableObject { private string textResult; public string TextResult { get => textResult; set => SetProperty(ref textResult, value); } public IAsyncRelayCommand GetTextCommand { get; set; } public AsyncRelayCommandPageViewModel() { GetTextCommand = new AsyncRelayCommand(GetText); } public async Task GetText() { await Task.Delay(3000); //模拟耗时操作 TextResult = "Hello world!"; } }

 运行结果:

 

这种情况是直接在Task内部处理结果的,也可以直接绑定到AsyncRelayCommand的ExecutionTask,然后用一个Converter来转换值。

下面看另外一个示例

界面布局:

依旧在界面上放置两个Label,一个显示状态,一个显示结果,一个开始任务的按钮。但是这里的结果绑定的是ExecutionTask属性值

1 2 3

ViewModel:

通过ExecutionTask属性,可以获取到GetTextCommand2最后执行的Task。

然后再通过一个CommunityToolkit.Common包中的Task.GetResultOrDefault()扩展函数,可以获取ExecutionTask的任务返回结果。

public class AsyncRelayCommandPageViewModel : ObservableObject { public IAsyncRelayCommand GetTextCommand2 { get; set; } public AsyncRelayCommandPageViewModel() { GetTextCommand2 = new AsyncRelayCommand(GetText2); } public async Task GetText2() { await Task.Delay(3000); //模拟耗时操作 return "Hello world!"; } }

Converter:

using CommunityToolkit.Common; using System; using System.Globalization; using System.Threading.Tasks; using System.Windows.Data; namespace CommunityToolkit.Mvvm.WpfDemo.Converters { public class TaskResultConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { if (value is Task task) { return task.GetResultOrDefault(); } return null; } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { throw new NotImplementedException(); } } }

运行结果:

 

如何取消AsyncRelayCommand

前面在介绍AsyncRelayCommand的功能时,提到了Cancel函数。可以使用AsyncRelayCommand.Cancel()函数来取消Task的执行。

使用带CancellationToken的重载版本,可以让AsyncRelayCommand具备取消功能。AsyncRelayCommand内部会维护一个CancellationTokenSource实例,然后将CancellationTokenSource.CancellationToken暴露出来。

如果对Task Cancellation不是很理解的话,可以阅读下面的内容

https://learn.microsoft.com/en-us/dotnet/standard/parallel-programming/task-cancellation

 

注意:

1.如果AsyncRelayCommand未执行(Task未执行),或者它不支持取消,调用Cancel函数会不起作用。

2.即使成功调用函数,当前的操作也可能 不会立即被取消,这个要根据实际情况。例如:我在过程A和过程B开始前都增加了任务取消操作,但是如果过程A已经执行了,此时去调用取消任务,是不会立即生效的,必须要等到过程A执行完。

public AsyncRelayCommand(Func cancelableExecute);

下面用一个示例来演示一下如何取消AsyncRelayCommand

界面上右边区域用于显示Task的状态,左边是获取并显示一个网站的源码。

获取按钮绑定到StartGetHtmlTaskCommand命令,取消按钮绑定到CancelGetHtmlTaskCommand命令。

ViewModel:

StartGetHtmlTaskCommand使用了带CancellationToken的重载版本。

防止加载太快,看不到效果,我这里增加了5秒的等待。

后面获取网页源码的过程,因为HttpWebRequest中异步的函数都不支持传入CancellationToken,需要重新封装。我这里仅做演示,所以直接把CancellationToken放在了这等待的5秒里。

public class AsyncRelayCommandPageViewModel : ObservableObject { private string urlSource; public string UrlSource { get => urlSource; set => SetProperty(ref urlSource, value); } private string url; public string Url { get => url; set { SetProperty(ref url, value); StartGetHtmlTaskCommand.NotifyCanExecuteChanged(); } } public IAsyncRelayCommand StartGetHtmlTaskCommand { get; set; } public ICommand CancelGetHtmlTaskCommand { get; set; } public AsyncRelayCommandPageViewModel() { StartGetHtmlTaskCommand = new AsyncRelayCommand(StartTask, () => !string.IsNullOrEmpty(Url)); CancelGetHtmlTaskCommand = new RelayCommand(CancelTask); } private async Task StartTask(System.Threading.CancellationToken cancellationToken) { UrlSource = await GetHtmlSource(Url, cancellationToken); } private async Task GetHtmlSource(string url,System.Threading.CancellationToken cancellationToken) { var result = await Task.Run(async () => { try { //模拟等待5秒,防止加载太快看不到效果 await Task.Delay(5000,cancellationToken); HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(url); using (var response = request.GetResponse()) { using (var stream = response.GetResponseStream()) { using (var reader = new System.IO.StreamReader(stream, Encoding.UTF8)) { return reader.ReadToEnd(); } } } } catch (OperationCanceledException ex) { return ex.Message; } }, cancellationToken); return result; } private void CancelTask() { StartGetHtmlTaskCommand.Cancel(); } }

运行结果:

 

 

 

 

代码生成器

CommunityToolkit.Mvvm提供了一个便捷的方式,可以使用自带的源码生成器来快速生成属性、命令。

详细了解可以阅读这篇文章

https://devblogs.microsoft.com/ifdef-windows/announcing-net-community-toolkit-v8-0-0-preview-1/

 

就像下面这样

private IRelayCommand greetUserCommand; public IRelayCommand GreetUserCommand => greetUserCommand ??= new RelayCommand(GreetUser); private void GreetUser(User user) { Console.WriteLine($"Hello {user.Name}!"); }

简化以后:

1 [ICommand] 2 private void GreetUser(User user) 3 { 4 Console.WriteLine($"Hello {user.Name}!"); 5 } private string? firstName; public string? FirstName { get => firstName; set { if (SetProperty(ref firstName, value)) { OnPropertyChanged(nameof(FullName)); GreetUserCommand.NotifyCanExecuteChanged(); } } } private string? lastName; public string? LastName { get => lastName; set { if (SetProperty(ref lastName, value)) { OnPropertyChanged(nameof(FullName)); GreetUserCommand.NotifyCanExecuteChanged(); } } } public string? FullName => $"{FirstName} {LastName}";

 

简化以后

3 [ObservableProperty] 4 [AlsoNotifyChangeFor(nameof(FullName))] 5 [AlsoNotifyCanExecuteFor(nameof(GreetUserCommand))] 6 private string? firstName; 7 8 [ObservableProperty] 9 [AlsoNotifyChangeFor(nameof(FullName))] 10 [AlsoNotifyCanExecuteFor(nameof(GreetUserCommand))] 11 private string? lastName; 12 13 public string? FullName => $"{FirstName} {LastName}";

示例代码:

https://github.com/zhaotianff/CommunityToolkit.Mvvm.WpfDemo

 

 

原文:https://www.cnblogs.com/zhaotianff/p/16870550.html

 



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3