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

news2024/9/25 13:18:02

最近在需要使用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.coloradmin.cn/o/1949741.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Centos安装、迁移gitlab

Centos安装迁移gitlab 一、下载安装二、配置rb修改&#xff0c;起服务。三、访问web&#xff0c;个人偏好设置。四、数据迁移1、查看当前GitLab版本2、备份旧服务器的文件3、将上述备份文件拷贝到新服务器同一目录下&#xff0c;恢复GitLab4、停止新gitlab数据连接服务5、恢复备…

Idea如何查看Maven依赖树

1、使用idea自带的功能查看依赖树 2、使用Maven Helper插件 https://zhuanlan.zhihu.com/p/699663369

Linux中MySQL 双主复制(互为主从)配置指南(详细过程)!

&#x1f3e1;作者主页&#xff1a;点击&#xff01; &#x1f427;Linux基础知识(初学)&#xff1a;点击&#xff01; &#x1f427;Linux高级管理防护和群集专栏&#xff1a;点击&#xff01; &#x1f510;Linux中firewalld防火墙&#xff1a;点击&#xff01; ⏰️创作…

stats 监控 macOS 系统

Stats 监控 macOS 系统 CPU 利用率GPU 利用率内存使用情况磁盘利用率网络使用情况电池电量 brew install stats参考 stats github

[Unity] ShaderGraph实现DeBuff污染 溶解叠加效果

本篇是在之前的基础上&#xff0c;继续做的功能衍生。 [Unity] ShaderGraph实现Sprite消散及受击变色 完整连连看如下所示&#xff1a;

分布式事务(典型的分布式事务场景+CAP+解决方案)

分布式事务与分布式锁的区别&#xff1a; 分布式锁解决的是分布式资源抢占的问题&#xff1b;分布式事务和本地事务是解决流程化提交问题。 SQL中的4个事务隔离级别&#xff1a;&#xff08;1&#xff09;读未提交&#xff08;2&#xff09;读已提交&#xff08;3&#xff09…

【React】详解样式控制:从基础到进阶应用的全面指南

文章目录 一、内联样式1. 什么是内联样式&#xff1f;2. 内联样式的定义3. 基本示例4. 动态内联样式 二、CSS模块1. 什么是CSS模块&#xff1f;2. CSS模块的定义3. 基本示例4. 动态应用样式 三、CSS-in-JS1. 什么是CSS-in-JS&#xff1f;2. styled-components的定义3. 基本示例…

基于JSP、java、Tomcat、mysql三层交互的项目实战--校园交易网(1)-项目搭建(前期准备工作)

这是项目的初始页面 接下来我先写下我的初始项目搭建 技术支持&#xff1a;JAVA、JSP 服务器&#xff1a;TOMCAT 7.0.86 编程软件&#xff1a;IntelliJ IDEA 2021.1.3 x64 首先我们打开页面&#xff0c;准备搭建项目的初始准备 1.New Project 2.随后点击Next&#xff0c;勾…

【数据结构】顺序表(杨辉三角、简单的洗牌算法)

&#x1f387;&#x1f389;&#x1f389;&#x1f389;点进来你就是我的人了 博主主页&#xff1a;&#x1f648;&#x1f648;&#x1f648;戳一戳&#xff0c;欢迎大佬指点&#xff01; 欢迎志同道合的朋友一起加油喔 &#x1f4aa;&#x1f4aa;&#x1f4aa; 谢谢你这么帅…

【机器学习】GPT LoRA:大模型微调的艺术与效率

文章目录 往期热门专栏回顾1、前言介绍 1.1、文生图效果 2、LoRA的工作原理3、LoRA的应用场景4、LoRA的优势5、LoRA的挑战6、LoRA的实现7、未来展望 1、前言介绍 在深度学习和自然语言处理领域&#xff0c;大规模预训练模型&#xff08;如GPT-3、BERT等&#xff09;已经展示了…

ansible基础讲解和加密文件讲解

ansible最重要的三个文件 /etc/ansible/ansible.cfg #####ansible的配置文件 /etc/ansible/host ##清单文件inventory ansible-navigator.yml ####以yml结尾的文件可以理解为conf结尾的文件&#xff0c;是配置文件&#xff0c;用于设置剧本playbook playbook讲解 以.yml结…

【Android】Activity与Fragment的数据传递

上一篇文章学到了碎片的创建与生命周期&#xff0c;接下来学习碎片的常用操作&#xff0c;其中会用到上一篇文章的三个碎片&#xff0c;就做一个简单的说明吧&#xff1a;LeftFragment&#xff08;包含一个按钮&#xff09;、RightFragment4&#xff08;以粉色为背景的文本&…

优化医疗数据管理:Kettle ETL 数据采集方案详解

在现代医疗保健领域&#xff0c;数据的准确性、完整性和及时性对于提高医疗服务质量和患者护理至关重要。为了有效管理和利用医疗数据&#xff0c;Kettle ETL&#xff08;Extract, Transform, Load&#xff09;数据采集方案成为了许多医疗机构的首选工具之一。本文将深入探讨Ke…

【Gitlab】SSH配置和克隆仓库

生成SSH Key ssh-keygen -t rsa -b 4096 私钥文件: id_rsa 公钥文件:id_rsa.pub 复制生成的ssh公钥到此处 克隆仓库 git clone repo-address 需要进行推送和同步来更新本地和服务器的文件 推送更新内容 git push <remote><branch> 拉取更新内容 git pull &…

MySQL数据库的DQL的高级数据查询语句

目录 非等值联查&#xff1a; 等值联查&#xff1a; eg&#xff1a;5张表联查 连接查询——left/right/inner join on eg: 连接查询——union Eg&#xff1a; 不去重的并集——union all 子查询&#xff08;内部查询&#xff09; 1、where型子查询 2、from型子查询&a…

Servlet2-HTTP协议、HttpServletRequest类、HttpServletResponse类

目录 HTTP协议 什么是HTTP协议 HTTP协议的特点 请求的HTTP协议格式 GET请求 POST请求 常用的请求头说明 哪些是GET请求&#xff0c;哪些是POST请求 响应的HTTP协议格式 常见的响应码说明 MIME类型说明 HttpServletRequest类 作用 常用方法 如何获取请求参数 po…

Linux:基础

一、安装 二、 一些组件 2.1 git管理 集中式版本控制系统:版本库是集中存放在中央服务器的,需要时要先从中央服务器取得最新的版本进行修改,修改后再推送给中央服务器。集中式版本控制系统最大的毛病就是必须联网才能工作,网速慢的话影响太大。 分布式版本控制系统:分布…

MySQL SQL 编程练习

目录 创建表并插入数据 查看表结构 创建触发器 创建INSERT 触发器 创建DELETE 触发器 创建更新触发器 创建存储过程 创建提取emp_new表所有员工姓名和工资的存储过程s1 创建存储过程s2&#xff0c;实现输入员工姓名后返回员工的年龄 创建一个存储过程s3&#xff0c;有2个参数&…

Apache ShardingSphere Proxy5.5.0实现MySQL分库分表与读写分离

1. 前提准备 1.1 主机IP:192.168.186.77 version: 3.8services:mysql-master:image: mysql:latestcontainer_name: mysql-masterenvironment:MYSQL_ROOT_PASSWORD: 123456MYSQL_USER: masterMYSQL_PASSWORD: 123456MYSQL_DATABASE: db1 ports:- "3306:3306&quo…

搭建NFS、web、dns服务器

目录 1、搭建一个nfs服务器&#xff0c;客户端可以从该服务器的/share目录上传并下载文件 服务端配置&#xff1a; 客户端测试&#xff1a; 2、搭建一个Web服务器&#xff0c;客户端通过www.haha.com访问该网站时能够看到内容:this is haha 服务端配置&#xff1a; 客户端…