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包中的类型定义
- CommunityToolkit.Mvvm.ComponentModel
- CommunityToolkit.Mvvm.DependencyInjection
- CommunityToolkit.Mvvm.Input
- CommunityToolkit.Mvvm.Messaging
- CommunityToolkit.Mvvm.Messaging.Messages
这里的类型不算太多,目前我只介绍一些我在项目中使用到的类型,应该能满足大部使用场景了。
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(...),可以让属性值更改时引发通知事件变得更加简单。
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 }
下面用一个小例子演示一下。
在界面上放置一个TextBox,Content绑定到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<T>属性
日常开发中,我还没有使用过将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<T>进行强制类型转换
下面先演示一下如何在界面上显示一个Task的状态
在界面上放置一个Label,绑定到MyTask.Status(Converter代码在后面)
1 <Label Content="{Binding MyTask.Status,Converter={StaticResource taskStatusConverter}}" VerticalAlignment="Center"/>
定义一个Task<T>属性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 <T>)接口,提供NotifyCanExecuteChanged方法来引发CanExeCutechanged事件。
下面看一个RelayCommand的简单使用
首先创建一个窗口,然后添加一个TextBox和一个Button,TextBox用于显示当前时间,绑定到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<T>接口。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<T>和直接在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