WPF MVVM框架: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包中的类型定义

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

ViewModelBase

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

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

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(...),可以让属性值更改时引发通知事件变得更加简单。

1     protected bool SetProperty<T>(\[NotNullIfNotNull(nameof(newValue))\] ref T field, T newValue, \[CallerMemberName\] string? propertyName = null)
2 {
3 OnPropertyChanging(propertyName);
4 ...
5 OnPropertyChanged(propertyName);
6 ...
7     }

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

 1     protected bool SetPropertyAndNotifyOnCompletion(\[NotNull\] ref TaskNotifier? taskNotifier, Task? newValue, \[CallerMemberName\] string? propertyName = null)
 2     {
 3         return SetPropertyAndNotifyOnCompletion(taskNotifier ??= new TaskNotifier(), newValue, null, propertyName);
 4     }
 5 
 6 private bool SetPropertyAndNotifyOnCompletion<TTask>(ITaskNotifier<TTask> taskNotifier, TTask? newValue, Action<TTask?>? callback, \[CallerMemberName\] string? propertyName = null)
 7         where TTask : Task 8     {
 9 ...
10         bool isAlreadyCompletedOrNull = newValue?.IsCompleted ?? true;
11 
12 OnPropertyChanging(propertyName);
13         taskNotifier.Task = newValue;
14 OnPropertyChanged(propertyName);
15         if (isAlreadyCompletedOrNull)
16 {
17             if (callback is not null)
18 {
19 callback(newValue);
20 }
21 
22             return true;
23 }
24 ...
25     }

如何使用ObservableObject类

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

简单属性

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

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

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

 1  protected bool SetProperty<T>(\[global::System.Diagnostics.CodeAnalysis.NotNullIfNotNull("newValue")\] ref T field, T newValue, \[global::System.Runtime.CompilerServices.CallerMemberName\] string? propertyName = null)
 2     {
 3         if (global::System.Collections.Generic.EqualityComparer<T>.Default.Equals(field, newValue))
 4         {
 5             return false;
 6         }
 7 
 8         field = newValue; 9 
10 OnPropertyChanged(propertyName);
11 
12         return true;
13     }

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

在界面上放置一个TextBoxContent绑定到CurrentTime属性

1  <GroupBox Header="简单属性"\>
2             <DockPanel Grid.Row="0" LastChildFill="False"\>
3                 <Label Content="当前时间" VerticalAlignment="Center"/>
4                 <TextBox Width="200" Text="{Binding CurrentTime}" VerticalAlignment="Center"/>
5             </DockPanel>
6         </GroupBox>

ViewModel如下:

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

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

 1         
 2        ......
 3         public ObservableObjectPageViewModel() 4         {
 5             StartUpdateTimer();
 6         }
 7 
 8         private void StartUpdateTimer() 9 {
10             System.Windows.Threading.DispatcherTimer dispatcherTimer = new System.Windows.Threading.DispatcherTimer();
11             dispatcherTimer.Interval = TimeSpan.FromSeconds(1);
12             dispatcherTimer.Tick += (a, b) => UpdateTime();
13 dispatcherTimer.Start();
14 }    
15         .......

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

包装非Observable的模型

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

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

1  public class Student
2 {
3         public string ID { get; set; }
4         public string Name { get; set; }
5     }

可以把它包装成ObservableStudent

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

 1    protected bool SetProperty<TModel, T>(T oldValue, T newValue, TModel model, global::System.Action<TModel, T> callback, \[global::System.Runtime.CompilerServices.CallerMemberName\] string? propertyName = null)
 2         where TModel : class
 3     {
 4         if (global::System.Collections.Generic.EqualityComparer<T>.Default.Equals(oldValue, newValue))
 5         {
 6             return false;
 7         }
 8 
 9 callback(model, newValue);
10 
11 OnPropertyChanged(propertyName);
12 
13         return true;
14     }

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

包装后如下:

 1   public class ObservableStudent : ObservableObject 2     {
 3         private readonly Student student; 4 
 5         public ObservableStudent(Student student) => this.student = student; 6 
 7         public string Name 8         {
 9             get => student.Name;
10             set => SetProperty(student.Name, value, student, (u, n) => u.Name = n);
11 }
12 
13         public string ID
14 {
15             get => student.ID;
16             set => SetProperty(student.ID, value, student, (u, n) => u.ID = n);
17 }
18     }

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

 1  <ListBox ItemsSource="{Binding StudentList}" SelectedItem="{Binding SelectedStudent}"\>
 2                 <ListBox.ItemTemplate>
 3                     <DataTemplate>
 4                         <DockPanel Height="45" LastChildFill="False"\>
 5                             <TextBlock DockPanel.Dock="Left" Text="{Binding ID}" FontSize="20" FontWeight="Bold" Width="100"/>
 6                             <TextBlock DockPanel.Dock="Left" Text="{Binding Name}" Width="200"/>
 7                         </DockPanel>
 8                     </DataTemplate>
 9                 </ListBox.ItemTemplate>
10             </ListBox>

ViewModel

 1  public class ObservableObjectPageViewModel : CommunityToolkit.Mvvm.ComponentModel.ObservableObject 2     {
 3         private ObservableCollection<ObservableStudent> studentList; 4         public ObservableCollection<ObservableStudent> StudentList { get => studentList; set => SetProperty(ref studentList, value); } 5 
 6         private ObservableStudent selectedStudent; 7         public ObservableStudent SelectedStudent { get => selectedStudent; set => SetProperty(ref selectedStudent, value); } 8 
 9 
10         public ObservableObjectPageViewModel()
11 {
12 InitStudentList();
13 }
14 
15         private void InitStudentList()
16 {
17             //假设这些数据来自数据库
18             var dbStudentList = GetDemoData();
19 
20             StudentList = new ObservableCollection<ObservableStudent>(dbStudentList.Select(x => new ObservableStudent(x)));
21 }
22 
23         private List<Student> GetDemoData()
24 {
25             var list = new List<Student>();
26             Student student1 = new Student() { ID = "1", Name = "相清" };
27             Student student2 = new Student() { ID = "2", Name = "濮悦" };
28 list.Add(student1);
29 list.Add(student2);
30             return list;
31 }
32 
33     }

运行结果如下:

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

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

Task属性

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

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

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

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

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

 1  private bool SetPropertyAndNotifyOnCompletion<TTask>(ITaskNotifier<TTask> taskNotifier, TTask? newValue, Action<TTask?>? 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<T> : ITaskNotifier<Task<T>>
2 {
3             public static implicit operator Task<T>?(TaskNotifier<T>? notifier);
4         }

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

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

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

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

1  <Label Content\="{Binding MyTask.Status,Converter={StaticResource taskStatusConverter}}" VerticalAlignment\="Center"/>

 定义一个Task属性MyTask

1  private TaskNotifier<string\>? myTask;
2 
3         public Task<string\>? MyTask
4 {
5 get => myTask;
6 private set => SetPropertyAndNotifyOnCompletion(ref myTask, value);
7         }

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

 1        public ObservableObjectPageViewModel() 2         {
 3             MyTask = GetTextAsync(); 4         }
 5 
 6         private async Task<string\> GetTextAsync() 7         {
 8             await Task.Delay(5000);
 9             return "任务执行后的结果";
10         }

Converter代码

 1 public class TaskStatusConverter : IValueConverter 2     {
 3         public object Convert(object value, Type targetType, object parameter, CultureInfo culture) 4         {
 5             var status = (TaskStatus)value; 6 
 7             switch(status)
 8             {
 9                 case TaskStatus.RanToCompletion:
10                     return "任务完成";
11                 default:
12                     return "加载中";
13 }
14 }
15 
16         public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
17 {
18             return DependencyProperty.UnsetValue;
19 }
20     }
1 Page.Resources>
2         <converters:TaskStatusConverter x:Key\="taskStatusConverter"/>
3     </Page.Resources\>

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

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

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

 1         public ObservableObjectPageViewModel() 2         {
 3             MyTask = GetTextAsync(); 4 
 5             this.PropertyChanged += ObservableObjectPageViewModel\_PropertyChanged; 6         }
 7 
 8         private void ObservableObjectPageViewModel\_PropertyChanged(object sender, PropertyChangedEventArgs e) 9 {
10             if (e.PropertyName == nameof(MyTask))
11 {
12                 //在这里处理Task的结果
13                 var result = MyTask.Result;
14 }
15         }

RelayCommand

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

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

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

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

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

下面看一个RelayCommand的简单使用

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

1   <DockPanel Grid.Row="0" LastChildFill="False"\>
2                 <TextBox Width="200" Text="{Binding CurrentTime,Mode=OneWay}" VerticalAlignment="Center"/>
3                 <Button Content="更新时间" VerticalAlignment="Center" Command="{Binding UpdateCommand}"/>
4   </DockPanel>

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

 1  public class ObservableObjectPageViewModel : CommunityToolkit.Mvvm.ComponentModel.ObservableObject 2     {
 3         private string currentTime; 4 
 5         public string CurrentTime { get => currentTime; set => SetProperty(ref currentTime, value); } 6 
 7         public ICommand UpdateCommand { get; set; }
 8     
 9 
10         public ObservableObjectPageViewModel()
11 {
12             UpdateCommand = new RelayCommand(UpdateTime);
13 }
14 
15         private void UpdateTime()
16 {
17             CurrentTime = DateTime.Now.ToString("F");
18 }
19 }

设置窗口的DataContext

1  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  <TextBox Text="{Binding InputText,UpdateSourceTrigger=PropertyChanged}" HorizontalAlignment="Left" Width="200" VerticalAlignment="Top"/>
2 
3  <Button Content="MsgShow" HorizontalAlignment="Left" VerticalAlignment="Top" Command="{Binding MsgShowCommand}"/>

 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通知

 1         private string inputText; 2 
 3         public string InputText 4         { 
 5             get => inputText; 6             set
 7             {
 8                 SetProperty(ref inputText, value); 9 MsgShowCommand.NotifyCanExecuteChanged();
10 }
11 }
12 
13         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中处理结果两种情况。我这里都进行演示一下。

界面布局

2  <Label Content\="{Binding GetTextCommand.ExecutionTask.Status}" HorizontalAlignment\="Left"\></Label\>
3  <Label HorizontalAlignment\="Left" Content\="{Binding TextResult}"/>
4  <Button HorizontalAlignment\="Left" VerticalAlignment\="Top" Width\="88" Content\="开始任务" Command\="{Binding GetTextCommand}"\></Button\>

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

ViewModel

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

 1 public class AsyncRelayCommandPageViewModel : ObservableObject 2     {
 3         private string textResult; 4         public string TextResult { get => textResult; set => SetProperty(ref textResult, value); } 5 
 6         public IAsyncRelayCommand GetTextCommand { get; set; }
 7 
 8      
 9         public AsyncRelayCommandPageViewModel()
10 {
11             GetTextCommand = new AsyncRelayCommand(GetText);
12 }
13 
14         public async Task GetText()
15 {
16             await Task.Delay(3000); //模拟耗时操作
17             TextResult =  "Hello world!";
18 }
19     }

 运行结果:

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

下面看另外一个示例

界面布局:

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

1  <Label Content\="{Binding GetTextCommand2.ExecutionTask.Status}" HorizontalAlignment\="Left"\></Label\>
2  <Label HorizontalAlignment\="Left" Content\="{Binding GetTextCommand2.ExecutionTask,Converter={StaticResource TaskResultConverter}}"/>
3  <Button HorizontalAlignment\="Left" VerticalAlignment\="Top" Width\="88" Content\="开始任务" Command\="{Binding GetTextCommand2}"\></Button\>

ViewModel:

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

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

 1 public class AsyncRelayCommandPageViewModel : ObservableObject 2     {
 3         public IAsyncRelayCommand GetTextCommand2 { get; set; }
 4 
 5 
 6         public AsyncRelayCommandPageViewModel() 7         {
 8            
 9             GetTextCommand2 = new AsyncRelayCommand(GetText2);
10            
11 }
12 
13 
14         public async Task<string\> GetText2()
15 {
16             await Task.Delay(3000); //模拟耗时操作
17             return "Hello world!";
18 }
19 }

Converter:

 1 using CommunityToolkit.Common; 2 using System; 3 using System.Globalization; 4 using System.Threading.Tasks; 5 using System.Windows.Data; 6 
 7 namespace CommunityToolkit.Mvvm.WpfDemo.Converters 8 {
 9     public class TaskResultConverter : IValueConverter
10 {
11 
12         public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
13 {
14             if (value is Task task)
15 {
16                 return task.GetResultOrDefault();
17 }
18 
19             return null;
20 }
21 
22         public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
23 {
24             throw new NotImplementedException();
25 }
26 }
27 }

运行结果:

如何取消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执行完。

1 public AsyncRelayCommand(Func<CancellationToken, Task> cancelableExecute);

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

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

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

 1  <Grid\>
 2                 <Grid.RowDefinitions\>
 3                     <RowDefinition/>
 4                     <RowDefinition Height\="30"/>
 5                 </Grid.RowDefinitions\>
 6 
 7 
 8                 <Grid.ColumnDefinitions\>
 9                     <ColumnDefinition/>
10                     <ColumnDefinition Width\="220"/>
11                 </Grid.ColumnDefinitions\>
12 
13                 <TextBox TextWrapping\="WrapWithOverflow" Margin\="10" Background\="Transparent" Text\="{Binding UrlSource}" ScrollViewer.VerticalScrollBarVisibility\="Auto"\></TextBox\>
14 
15                 <GroupBox Grid.Column\="1" Header\="Task状态"\>
16                     <StackPanel\>
17                         <DockPanel\>
18                             <Label Content\="CanBeCanceled:"\></Label\>
19                             <Label Content\="{Binding StartGetHtmlTaskCommand.CanBeCanceled}"\></Label\>
20                         </DockPanel\>
21                         <DockPanel\>
22                             <Label Content\="IsCancellationRequested:"\></Label\>
23                             <Label Content\="{Binding StartGetHtmlTaskCommand.IsCancellationRequested}"\></Label\>
24                         </DockPanel\>
25                         <DockPanel\>
26                             <Label Content\="IsRunning:"\></Label\>
27                             <Label Content\="{Binding StartGetHtmlTaskCommand.IsRunning}"\></Label\>
28                         </DockPanel\>
29                         <DockPanel\>
30                             <Label Content\="TaskStatus:"\></Label\>
31                             <Label Content\="{Binding StartGetHtmlTaskCommand.ExecutionTask.Status}"\></Label\>
32                         </DockPanel\>
33                     </StackPanel\>
34                 </GroupBox\>
35 
36                 <Grid Grid.Row\="1" Margin\="10,0" Grid.ColumnSpan\="2"\>
37                     <Label Content\="网址" HorizontalAlignment\="Left" VerticalAlignment\="Center"\></Label\>
38                     <TextBox VerticalAlignment\="Center" Margin\="40,0,200,0" Text\="{Binding Url,UpdateSourceTrigger=PropertyChanged}"\></TextBox\>
39                     <Button Content\="获取"  HorizontalAlignment\="Right" Width\="88" VerticalAlignment\="Center" Margin\="0,0,103,0" Command\="{Binding StartGetHtmlTaskCommand}"/>
40                     <Button Content\="取消"  HorizontalAlignment\="Right" Width\="88" VerticalAlignment\="Center" Margin\="0,0,5,0" Command\="{Binding CancelGetHtmlTaskCommand}"/>
41                 </Grid\>
42             </Grid\>

ViewModel:

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

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

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

 1 public class AsyncRelayCommandPageViewModel : ObservableObject 2     {
 3         private string urlSource; 4 
 5         public string UrlSource { get => urlSource; set => SetProperty(ref urlSource, value); } 6 
 7         private string url; 8         public string Url 9 { 
10             get => url;
11             set
12 {
13                 SetProperty(ref url, value);
14 StartGetHtmlTaskCommand.NotifyCanExecuteChanged();
15 }
16 }
17 
18         public IAsyncRelayCommand StartGetHtmlTaskCommand { get; set; }
19 
20         public ICommand CancelGetHtmlTaskCommand { get; set; }
21 
22         public AsyncRelayCommandPageViewModel()
23 {
24             StartGetHtmlTaskCommand = new AsyncRelayCommand(StartTask, () => !string.IsNullOrEmpty(Url));
25             CancelGetHtmlTaskCommand = new RelayCommand(CancelTask);
26 }
27 
28         private async Task StartTask(System.Threading.CancellationToken cancellationToken)
29 {
30             UrlSource = await GetHtmlSource(Url, cancellationToken);
31 }
32 
33         private async Task<string\> GetHtmlSource(string url,System.Threading.CancellationToken cancellationToken)
34 {
35             var result = await Task.Run(async () =>
36 {
37 
38                 try
39 {
40                     //模拟等待5秒,防止加载太快看不到效果
41                     await Task.Delay(5000,cancellationToken);
42                     HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(url);
43                     using (var response = request.GetResponse())
44 {
45                         using (var stream = response.GetResponseStream())
46 {
47                             using (var reader = new System.IO.StreamReader(stream, Encoding.UTF8))
48 {
49                                 return reader.ReadToEnd();
50 }
51 }
52 }
53 }
54                 catch (OperationCanceledException ex)
55 {
56                     return ex.Message;
57 }
58 
59 }, cancellationToken);
60 
61             return result;
62 }
63 
64         private void CancelTask()
65 {
66 StartGetHtmlTaskCommand.Cancel();
67 }
68 
69     }

运行结果:

代码生成器

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

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

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

就像下面这样

1 private IRelayCommand<User> greetUserCommand;
2 
3 public IRelayCommand<User> GreetUserCommand => greetUserCommand ??= new RelayCommand<User>(GreetUser);
4 
5 private void GreetUser(User user)
6 {
7     Console.WriteLine($"Hello {user.Name}!");
8 }

简化以后:

1 \[ICommand\]
2 private void GreetUser(User user)
3 {
4     Console.WriteLine($"Hello {user.Name}!");
5 }
 1 private string? firstName;
 2 
 3 public string? FirstName
 4 {
 5     get => firstName;
 6     set
 7     {
 8         if (SetProperty(ref firstName, value))
 9         {
10             OnPropertyChanged(nameof(FullName));
11             GreetUserCommand.NotifyCanExecuteChanged();
12         }
13     }
14 }
15 
16 private string? lastName;
17 
18 public string? LastName
19 {
20     get => lastName;
21     set
22     {
23         if (SetProperty(ref lastName, value))
24         {
25             OnPropertyChanged(nameof(FullName));
26             GreetUserCommand.NotifyCanExecuteChanged();
27         }
28     }
29 }
30 
31 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,如有侵权,请联系删除。

版权声明:
作者:亦灵一梦
链接:https://blog.haokaikai.cn/2023/program/aspnet/wpf/1196.html
来源:开心博客
文章版权归作者所有,未经允许请勿转载。

THE END
分享
二维码
海报
WPF MVVM框架:CommunityToolkit.Mvvm包使用介绍
最近在需要使用MVVM框架的时候才发现MvvmLight作者宣布停止更新了,有点可惜。 原作者推荐使用CommunityToolkit.Mvvm包,所以这里做一个CommunityToolkit.Mvvm……
<<上一篇
下一篇>>
文章目录
关闭
目 录