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
    • ObservableObject
    • ObservableRecipient
    • ObservableValidator
  • CommunityToolkit.Mvvm.DependencyInjection
    • Ioc
  • CommunityToolkit.Mvvm.Input
    • RelayCommand
    • RelayCommand<T>
    • AsyncRelayCommand
    • AsyncRelayCommand<T>
    • IRelayCommand
    • IRelayCommand<T>
    • IAsyncRelayCommand
    • IAsyncRelayCommand<T>
  • CommunityToolkit.Mvvm.Messaging
    • IMessenger
    • WeakReferenceMessenger
    • StrongReferenceMessenger
    • IRecipient<TMessage>
    • MessageHandler<TRecipient, TMessage>
  • CommunityToolkit.Mvvm.Messaging.Messages
    • PropertyChangedMessage<T>
    • RequestMessage<T>
    • AsyncRequestMessage<T>
    • CollectionRequestMessage<T>
    • AsyncCollectionRequestMessage<T>
    • ValueChangedMessage<T>

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

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<T>([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<TTask>(ITaskNotifier<TTask> taskNotifier, TTask? newValue, Action<TTask?>? 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<T>(string propertyName, ref T field, T newValue = default, bool broadcast = false);

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

  protected bool SetProperty<T>([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<T>.Default.Equals(field, newValue)){return false;}field = newValue;OnPropertyChanged(propertyName);return true;}

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

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

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

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的数据模型。

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

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

可以把它包装成ObservableStudent

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

     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)where TModel : class{if (global::System.Collections.Generic.EqualityComparer<T>.Default.Equals(oldValue, newValue)){return false;}callback(model, newValue);OnPropertyChanged(propertyName);return true;}

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

包装后如下:

    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

  <ListBox ItemsSource="{Binding StudentList}" SelectedItem="{Binding SelectedStudent}"><ListBox.ItemTemplate><DataTemplate><DockPanel Height="45" LastChildFill="False"><TextBlock DockPanel.Dock="Left" Text="{Binding ID}" FontSize="20" FontWeight="Bold" Width="100"/><TextBlock DockPanel.Dock="Left" Text="{Binding Name}" Width="200"/></DockPanel></DataTemplate></ListBox.ItemTemplate></ListBox>

ViewModel

  public class ObservableObjectPageViewModel : CommunityToolkit.Mvvm.ComponentModel.ObservableObject{private ObservableCollection<ObservableStudent> studentList;public ObservableCollection<ObservableStudent> 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<ObservableStudent>(dbStudentList.Select(x => new ObservableStudent(x)));}private List<Student> GetDemoData(){var list = new List<Student>();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<T>属性

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

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

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

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

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

  private bool SetPropertyAndNotifyOnCompletion<TTask>(ITaskNotifier<TTask> taskNotifier, TTask? newValue, Action<TTask?>? callback, [CallerMemberName] string? propertyName = null)where TTask : Task{if (ReferenceEquals(taskNotifier.Task, newValue)){return false;}bool isAlreadyCompletedOrNull = newValue?.IsCompleted ?? true;OnPropertyChanging(propertyName);taskNotifier.Task = newValue;OnPropertyChanged(propertyName);async void MonitorTask(){await newValue!.GetAwaitableWithoutEndValidation();if (ReferenceEquals(taskNotifier.Task, newValue)){OnPropertyChanged(propertyName);}...}MonitorTask();return true;}

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

TaskNotifier类型,

  protected sealed class TaskNotifier<T> : ITaskNotifier<Task<T>>{public static implicit operator Task<T>?(TaskNotifier<T>? notifier);}

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

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

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

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

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

 定义一个Task<T>属性MyTask

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

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

         public ObservableObjectPageViewModel(){MyTask = GetTextAsync();}private async Task<string> 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;}}
 <Page.Resources><converters:TaskStatusConverter x:Key="taskStatusConverter"/></Page.Resources>

 运行后可以看到界面会在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<T>,这也就意味着可以直接使用封装好的方法或lambda表达式。
  • 实现了iRelayCommand(和iRelayCommand <T>)接口,提供NotifyCanExecuteChanged方法来引发CanExeCutechanged事件。

 

下面看一个RelayCommand的简单使用

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

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

创建一个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()函数来进行通知

下面看一个小例子:

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

  <TextBox Text="{Binding InputText,UpdateSourceTrigger=PropertyChanged}" HorizontalAlignment="Left" Width="200" VerticalAlignment="Top"/><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通知

          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<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中处理结果两种情况。我这里都进行演示一下。

界面布局

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

界面上有两个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属性值

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

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<string> 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不是很理解的话,可以阅读下面的内容

Task Cancellation - .NET | Microsoft Learn

注意:

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

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

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

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

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

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

<Grid><Grid.RowDefinitions><RowDefinition/><RowDefinition Height="30"/></Grid.RowDefinitions><Grid.ColumnDefinitions><ColumnDefinition/><ColumnDefinition Width="220"/></Grid.ColumnDefinitions><TextBox TextWrapping="WrapWithOverflow" Margin="10" Background="Transparent" Text="{Binding UrlSource}" ScrollViewer.VerticalScrollBarVisibility="Auto"></TextBox><GroupBox Grid.Column="1" Header="Task状态"><StackPanel><DockPanel><Label Content="CanBeCanceled:"></Label><Label Content="{Binding StartGetHtmlTaskCommand.CanBeCanceled}"></Label></DockPanel><DockPanel><Label Content="IsCancellationRequested:"></Label><Label Content="{Binding StartGetHtmlTaskCommand.IsCancellationRequested}"></Label></DockPanel><DockPanel><Label Content="IsRunning:"></Label><Label Content="{Binding StartGetHtmlTaskCommand.IsRunning}"></Label></DockPanel><DockPanel><Label Content="TaskStatus:"></Label><Label Content="{Binding StartGetHtmlTaskCommand.ExecutionTask.Status}"></Label></DockPanel></StackPanel></GroupBox><Grid Grid.Row="1" Margin="10,0" Grid.ColumnSpan="2"><Label Content="网址" HorizontalAlignment="Left" VerticalAlignment="Center"></Label><TextBox VerticalAlignment="Center" Margin="40,0,200,0" Text="{Binding Url,UpdateSourceTrigger=PropertyChanged}"></TextBox><Button Content="获取"  HorizontalAlignment="Right" Width="88" VerticalAlignment="Center" Margin="0,0,103,0" Command="{Binding StartGetHtmlTaskCommand}"/><Button Content="取消"  HorizontalAlignment="Right" Width="88" VerticalAlignment="Center" Margin="0,0,5,0" Command="{Binding CancelGetHtmlTaskCommand}"/></Grid></Grid>

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<string> 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<User> greetUserCommand;public IRelayCommand<User> GreetUserCommand => greetUserCommand ??= new RelayCommand<User>(GreetUser);private void GreetUser(User user){Console.WriteLine($"Hello {user.Name}!");}

简化以后:

 [ICommand]private void GreetUser(User user){Console.WriteLine($"Hello {user.Name}!");}

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}";

简化以后

  [ObservableProperty][AlsoNotifyChangeFor(nameof(FullName))][AlsoNotifyCanExecuteFor(nameof(GreetUserCommand))]private string? firstName;[ObservableProperty][AlsoNotifyChangeFor(nameof(FullName))][AlsoNotifyCanExecuteFor(nameof(GreetUserCommand))]private string? lastName;public string? FullName => $"{FirstName} {LastName}";

示例代码

GitHub - zhaotianff/CommunityToolkit.Mvvm.WpfDemo: Simple CommunityToolkit.Mvvm.WpfDemo

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/383978.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

四、GD32 MCU 常见外设介绍 (5) TIMER 模块介绍

5.1.TIMER 基础知识 TIMER分高级定时器&#xff0c;通用定时器L0&#xff0c;L1&#xff0c;L2和基本定时器。 5.2.硬件连接说明 TIMER 属于片内外设&#xff0c;对于外部硬件设计&#xff0c;只需要单独IO口外接信号线即可。 5.3.GD32 TIMER 外设原理简介&#xff08;以 G…

电脑屏幕录制软件,分享4款(2024最新)

在今天&#xff0c;我们的电脑屏幕成为了一个多彩多姿的窗口。通过它我们可以浏览网页、观看视频、处理文档、进行游戏……有时&#xff0c;我们想要记录下这些精彩瞬间&#xff0c;与朋友分享&#xff0c;或者作为教程留存&#xff0c;这时&#xff0c;电脑屏幕录制就显得尤为…

全年销售7亿块,巧克力企业如何通过相邻业务打造极致产品力?

蒂罗尔巧克力是日本经典的巧克力品牌。 糖果业务是松尾早期的主营业务&#xff0c;在主营业务下滑的情况下&#xff0c;确立新的竞争方向&#xff0c;通过主营业务优势进入相邻业务&#xff0c;打造新产品成就巧克力极致产品力&#xff0c;避免衰退重回增长。 如何通过进入相邻…

Ubuntu下载jdk:cannot execute binary file

虚拟机上Ubuntu系统安装jdk且配置环境之后&#xff0c;java -version显示cannot execute binary file&#xff0c;多番查阅推测是由于系统和jdk版本不兼容的原因。 uname -m查看系统版本位i686&#xff0c;是32位的&#xff0c;和64位的jdk版本不兼容。因此&#xff0c;下载32位…

Linux:core文件无法生成排查步骤

1、进程的RLIMIT_CORE或RLIMIT_SIZE被设置为0。使用getrlimit和ulimit检查修改。 使用ulimit -a 命令检查是否开启core文件生成限制 如果发现-c后面的结果是0&#xff0c;就临时添加环境变量ulimit -c unlimited&#xff0c;之后在启动程序观察是否有core生成&#xff0c;如果…

【学习笔记】解决Serial Communication Library编译问题

【学习笔记】解决编译 Serial Communication Library 时的 Catkin 依赖问题 Serial Communication Library 是一个用 C 编写的用于连接类似 rs-232 串口的跨平台库。它提供了一个现代的 C 接口&#xff0c;它的工作流程设计在外观和感觉上与 PySerial 相似&#xff0c;但串口速…

Axure软件新功能解析与应用技巧分享

Axure是一种用于创建原型和交互设计的软件工具&#xff0c;广泛应用于操作界面。&#xff08;UI&#xff09;和客户体验&#xff08;UX&#xff09;为了展示和测试应用程序、网站或其他数据产品的性能和操作界面&#xff0c;设计帮助产品经理、设计师和开发者制作具有交互性的原…

服务器上使用Docker部署sonarQube,并集成到Jenkins实现自动化。

目标是要在目标服务器上使用docker工具部署好sonar环境&#xff0c;然后再集成到Jenkins中实现自动化的代码审查工作。 Docker 首先Dokcer的源大部分现在都用不了&#xff0c;于是我上网查询&#xff0c;终于找到了一个可用的镜像。 编辑/etc/docker/daemon.json文件&#x…

lua 游戏架构 之 游戏 AI (五)ai_autofight_find_way

这段Lua脚本定义了一个名为 ai_autofight_find_way 的类&#xff0c;继承自 ai_base 类。 lua 游戏架构 之 游戏 AI &#xff08;一&#xff09;ai_base-CSDN博客文章浏览阅读238次。定义了一套接口和属性&#xff0c;可以基于这个基础类派生出具有特定行为的AI组件。例如&…

【北京迅为】《i.MX8MM嵌入式Linux开发指南》-第三篇 嵌入式Linux驱动开发篇-第三十八章 驱动模块编译进内核

i.MX8MM处理器采用了先进的14LPCFinFET工艺&#xff0c;提供更快的速度和更高的电源效率;四核Cortex-A53&#xff0c;单核Cortex-M4&#xff0c;多达五个内核 &#xff0c;主频高达1.8GHz&#xff0c;2G DDR4内存、8G EMMC存储。千兆工业级以太网、MIPI-DSI、USB HOST、WIFI/BT…

Golang | Leetcode Golang题解之第279题完全平方数

题目&#xff1a; 题解&#xff1a; // 判断是否为完全平方数 func isPerfectSquare(x int) bool {y : int(math.Sqrt(float64(x)))return y*y x }// 判断是否能表示为 4^k*(8m7) func checkAnswer4(x int) bool {for x%4 0 {x / 4}return x%8 7 }func numSquares(n int) i…

【快速逆向二/无过程/有源码】掌上高考—2024高考志愿填报服务平台

逆向日期&#xff1a;2024.07.21 使用工具&#xff1a;Node.js 加密工具&#xff1a;Crypto-js标准库 文章全程已做去敏处理&#xff01;&#xff01;&#xff01; 【需要做的可联系我】 AES解密处理&#xff08;直接解密即可&#xff09;&#xff08;crypto-js.js 标准算法&…

深蓝学院 机器人操作系统ROS理论与实践(四)

一、机器人是什么&#xff1f; 机器人是如何组成的&#xff08;控制的角度&#xff09; 二、机器人系统构建 执行机构的实现——机器人底盘、电机、舵机等 驱动系统的实现 内部传感器的实现 控制系统的实现 外部传感系统的实现——摄像头、激光雷达、GPS等 1、连接摄像头 …

STM32——GPIO(点亮LEDLED闪烁)

一、什么是GPIO&#xff1f; GPIO&#xff08;通用输入输出接口&#xff09;&#xff1a; 1.GPIO 功能概述 GPIO 是通用输入/输出&#xff08;General Purpose I/O&#xff09;的简称&#xff0c;既能当输入口使用&#xff0c;又能当输出口使用。端口&#xff0c;就是元器件…

uniapp手写滚动选择器

文章目录 效果展示HTML/Template部分&#xff1a;JavaScript部分&#xff1a;CSS部分&#xff1a;完整代码 没有符合项目要求的选择器 就手写了一个 效果展示 实现一个时间选择器的功能&#xff0c;可以选择小时和分钟&#xff1a; HTML/Template部分&#xff1a; <picker…

【OpenCV C++20 学习笔记】扫描图片数据

扫描图片数据 应用情景图像数据扫描的难点颜色空间缩减&#xff08;color space reduction&#xff09;查询表 扫描算法计算查询表统计运算时长连续内存3种扫描方法C风格的扫描方法迭代器方法坐标方法LUT方法 算法效率对比结论 应用情景 图像数据扫描的难点 在上一篇文章《基…

调度子系统在特定时间执行

时序逻辑调度器设计模式允许您安排Simulink子系统在指定时间执行。以下模型说明了这种设计模式。 时序逻辑调度器图表包含以下逻辑&#xff1a; 时序逻辑调度器的关键行为 时序逻辑调度器图表包含两个状态&#xff0c;它们以不同的速率调度函数调用子系统A1、A2和A3的执行&…

【管控业财一体化】

1. 引言 大型集团在现代企业管理中扮演着举足轻重的角色&#xff0c;其管控业财一体化解决方案是实现企业高效运营的关键。随着数字化转型的加速&#xff0c;业财一体化不再局限于财务与业务流程的简单融合&#xff0c;而是向着更深层次的数据驱动、智能化决策和价值创造方向发…

Python小工具——监听某网站的数据变化并进行邮件通知

目录 一、需求描述 二、解析 三、实例代码 一、需求描述 监听自考网2024年广东省6月份的毕业生学历注册进度&#xff0c;这是网址&#xff1a;https://www.chsi.com.cn/xlcx/count_zk.jsp&#xff0c; 如上图所示&#xff0c;我们想知道这个红色的空格啥时候被填满&#xf…

7月26日贪心练习-摆动序列专题

前言 大家好&#xff0c;今天学习用贪心思想解决摆动序列问题&#xff0c;共三题&#xff0c;分享自己的思路&#xff0c;请大家多多支持 算法思想 大家可以先看看这道我们后面会讲的题看看怎么个事&#xff0c;. - 力扣&#xff08;LeetCode&#xff09; 由此题题解说明算…