本系列主要介绍微软社区工具包CommunityToolKit.Mvvm,是本人在观看B站UP主十月的寒流视频时的个人总结,真心感谢!学习C#的同学强力建议关注此UP,并观看其全部视频。
用 CommunityToolkit.Mvvm 加速 MVVM 开发流程_哔哩哔哩_bilibili
语言:C#
IDE:Microsoft Visual Studio Community 2022
框架:WPF,.net 8.0
目录
4.3.2 AllowConcurrentExecutions
一、Mvvm模式下使用频率高的对象
1.1 ViewModelBase
主要用于UI绑定,通过PropertyChangedEventHandler事件通知前台,以下是一个简单实现:
class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
public void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
class MainViewModel:ViewModelBase
{
private string title;
public string Title
{
get => title;
set
{
if (title != value)
{
title = value;
OnPropertyChanged(nameof(Title));
}
}
}
}
可以通过CallerMemberNameAttribute进行简化
class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
public void OnPropertyChanged([CallerMemberName]string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
class MainViewModel:ViewModelBase
{
private string title;
public string Title
{
get => title;
set
{
if (title != value)
{
title = value;
OnPropertyChanged();
}
}
}
}
2.1 RelayCommand
绑定至UI控件的Command,用于执行事件及判断事件是否可以执行,以下是一个简单的实现:
class RelayCommand:ICommand
{
private readonly Action<object> execute;
private readonly Predicate<object> canExecute;
public RelayCommand(Action<object> execute, Predicate<object> canExecute)
{
this.execute = execute;
this.canExecute = canExecute;
}
public RelayCommand(Action<object> execute) : this(excecute,null)
{
}
public bool CanExecute(object parameter)
{
return canExecute == null || canExecute(parameter);
}
public void Execute(object parameter)
{
execute(parameter);
}
public event EventHandler? CanExecuteChanged;
public void RaiseCanExecuteChanged()
{
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
}
二、使用CommunityToolkit后的简化写法
2.1 部分简写
class MainViewModel : ObservableObject
{
private string title;
public string Title
{
get => title;
set => SetProperty(ref title, value);
}
public RelayCommand ButtonClickCommand{get;}
}
2.2 全部简写
[ObservableProperty]
string title = "Hello world";
[RelayCommand()]
void ButtonClick()
{
Title += "Goodbye";
}
三、Demo演示
3.1 Demo1,基础写法
显示效果:CheckBox控制Button的IsEnable,Button点击后改变TextBlock的内容。
ViewModel:
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace Lesson1.ViewModels
{
public class MainViewModel1 : ObservableObject
{
private string title = "Hello,world";
public string Title
{
get => title;
set => SetProperty(ref title, value);
}
private bool isEnabled;
public bool IsEnabled
{
get => isEnabled;
set
{
SetProperty(ref isEnabled, value);
ButtonClickCommad.NotifyCanExecuteChanged();
}
}
public RelayCommand ButtonClickCommad { get; }
public MainViewModel1()
{
ButtonClickCommad = new RelayCommand(() => Title = "Goodbye", () => IsEnabled);
}
}
}
View:
<UserControl x:Class="Lesson1.Views.MainView1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:dxmvvm="http://schemas.devexpress.com/winfx/2008/xaml/mvvm"
xmlns:ViewModels="clr-namespace:Lesson1.ViewModels"
d:DesignHeight="300"
d:DesignWidth="300"
mc:Ignorable="d">
<UserControl.DataContext>
<ViewModels:MainViewModel1 />
</UserControl.DataContext>
<Grid TextElement.FontSize="32">
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
<TextBox Width="300" Text="{Binding Title}" />
<CheckBox Content="Is Enabled" IsChecked="{Binding IsEnabled}" />
<Separator Margin="5" />
<Button Command="{Binding ButtonClickCommad}" Content="Click Me" />
</StackPanel>
</Grid>
</UserControl>
3.2 Demo2,进阶写法
显示效果:
CheckBox1控制Button1的IsEnable,Button1点击后改变TextBlock内容+=1,
CheckBox2控制Button2的IsEnable,Button2点击后删除TextBlock的最后一位。
ViewModel:
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace Lesson1.ViewModels
{
public partial class MainViewModel2 : ObservableObject
{
[ObservableProperty]
string title = "Hello world";
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(ButtonClick1Command))]
bool isEnabled1 = false;
[RelayCommand(CanExecute = nameof(CanButton1Click))]
void ButtonClick1()
{
Title += "1";
}
bool CanButton1Click() => IsEnabled1;
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(ButtonClick2Command))]
bool isEnabled2 = false;
[RelayCommand(CanExecute = nameof(IsEnabled2))]
void ButtonClick2()
{
if (Title.Length > 0) Title = Title.Remove(Title.Length - 1);
}
}
}
View:
<UserControl x:Class="Lesson1.Views.MainView2"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:ViewModels="clr-namespace:Lesson1.ViewModels"
xmlns:local="clr-namespace:Lesson1.Views"
d:DesignHeight="450"
d:DesignWidth="800"
mc:Ignorable="d">
<UserControl.DataContext>
<ViewModels:MainViewModel2 />
</UserControl.DataContext>
<Grid TextElement.FontSize="32">
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
<TextBox Width="300" Text="{Binding Title}" />
<CheckBox Content="Is Enabled 1" IsChecked="{Binding IsEnabled1}" />
<Button Command="{Binding ButtonClick1Command}" Content="Click Me 1" />
<CheckBox Content="Is Enabled 2" IsChecked="{Binding IsEnabled2}" />
<Button Command="{Binding ButtonClick2Command}" Content="Click Me 2" />
</StackPanel>
</Grid>
</UserControl>
3.3 Demo3,异步方法
显示效果:
CheckBox1控制Button的IsEnable,Button点击后启动一个异步任务,任务内容为延时2秒,任务结束后改变TextBlock的内容。
CheckBox2可显示异步任务的状态。
ViewModel:
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System.Threading.Tasks;
namespace Lesson1.ViewModels
{
public partial class MainViewModel3 : ObservableObject
{
[ObservableProperty]
string title = "Hello world";
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(ButtonClickCommand))]
bool isEnabled = false;
[RelayCommand(CanExecute = nameof(CanButtonClick))]
async Task ButtonClickAsync()
{
await Task.Delay(2000);
Title = "Goodbye";
}
bool CanButtonClick() => IsEnabled;
}
}
View:
<UserControl x:Class="Lesson1.Views.MainView3"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:ViewModels="clr-namespace:Lesson1.ViewModels"
xmlns:local="clr-namespace:Lesson1.Views"
d:DesignHeight="450"
d:DesignWidth="800"
mc:Ignorable="d">
<UserControl.DataContext>
<ViewModels:MainViewModel3 />
</UserControl.DataContext>
<Grid TextElement.FontSize="32">
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
<TextBox Width="300" Text="{Binding Title}" />
<CheckBox Content="Is Enabled" IsChecked="{Binding IsEnabled}" />
<Separator Margin="5" />
<Button Command="{Binding ButtonClickCommand}" Content="Click Me" />
<CheckBox Content="Is running" IsChecked="{Binding ButtonClickCommand.IsRunning, Mode=OneWay}" />
</StackPanel>
</Grid>
</UserControl>
3.4 Demo4,小结
显示效果:
CheckBox1控制Button的IsEnable,Button点击后启动一个异步任务,任务内容为延时2秒,任务结束后改变TextBlock1的内容。同时还会改变TextBlock2的内容。
CheckBox2可显示异步任务的状态。
ViewModel:
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System.Threading.Tasks;
namespace Lesson1.ViewModels
{
public partial class MainViewModel4 : ObservableObject
{
[ObservableProperty]
//[NotifyPropertyChangedFor(nameof(Caption))]
private string title = $"{defaultTitle}";
private const string defaultTitle = "Hello world";
partial void OnTitleChanged(string value)
{
OnPropertyChanged(nameof(Caption));
}
private int index = 1;
public string Caption => $"Title: {Title}";
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(ButtonClickCommand))]
private bool isEnabled = false;
[RelayCommand(CanExecute = nameof(CanButtonClick))]
private async Task ButtonClickAsync()
{
await Task.Delay(2000);
Title = $"{defaultTitle}:{index++}";
}
private bool CanButtonClick() => IsEnabled;
}
}
View:
<UserControl x:Class="Lesson1.Views.MainView4"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Lesson1.Views"
xmlns:ViewModels="clr-namespace:Lesson1.ViewModels"
d:DesignHeight="450"
d:DesignWidth="800"
mc:Ignorable="d">
<UserControl.DataContext>
<ViewModels:MainViewModel4 />
</UserControl.DataContext>
<Grid>
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" TextElement.FontSize="32">
<TextBox Width="400" Text="{Binding Title}" />
<TextBox Width="400" Text="{Binding Caption, Mode=OneWay}" />
<CheckBox Content="Is Enabled" IsChecked="{Binding IsEnabled}" />
<Button Command="{Binding ButtonClickCommand}" Content="Click Me" />
<CheckBox Content="Is running" IsChecked="{Binding ButtonClickCommand.IsRunning, Mode=OneWay}" />
</StackPanel>
</Grid>
</UserControl>
四、总结
4.1 ObservableObject
class需要继承基类ObservableObject,且需要添加partial修饰符
public partial class MainViewModel : ObservableObject
{
}
4.2 ObservableProperty
需要绑定到UI的字段添加特性ObservableProperty,会自动生成同名属性
首字母小写或开头添加_或开头添加m_
4.3 RelayCommand
需要绑定到UI的命令添加特性RelayCommand,会自动生成同名命令
4.3.1 CanExecute
为Command是否可以执行的属性或者方法的名称
4.3.1.1 属性名
public partial class MainViewModel : ObservableObject
{
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(buttonClickCommand))]
bool isEnabled;
[RelayCommand(CanExecute = nameof(IsEnabled))]
void ButtonClick()
{
}
}
4.3.1.2 方法名
public partial class MainViewModel : ObservableObject
{
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(buttonClickCommand))]
bool isEnabled;
[RelayCommand(CanExecute = nameof(CanButtonClick))]
void ButtonClick()
{
}
bool CanButtonClick() => IsEnabled;
}
4.3.2 AllowConcurrentExecutions
是否允许并发执行,参考以下代码
ViewModel:
public partial class MainViewModel5 : ObservableObject
{
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(ButtonClickCommand))]
string title = "Count:";
[RelayCommand]
async Task ButtonClickAsync()
{
await Task.Delay(2000);
Title += "1";
}
}
View:
<UserControl x:Class="Lesson1.Views.MainView5"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:ViewModels="clr-namespace:Lesson1.ViewModels"
xmlns:local="clr-namespace:Lesson1.Views"
d:DesignHeight="450"
d:DesignWidth="800"
mc:Ignorable="d">
<UserControl.DataContext>
<ViewModels:MainViewModel5 />
</UserControl.DataContext>
<Grid>
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
<TextBox Width="300" Text="{Binding Title}" />
<Separator Margin="5" />
<Button Command="{Binding ButtonClickCommand}" Content="Click Me" />
</StackPanel>
</Grid>
</UserControl>
点击Button后,Button变成灰色,2秒后恢复,并且更新TextBox的内容。
将RelayCommand添加AllowConcurrentExecutions = true:
public partial class MainViewModel5 : ObservableObject
{
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(ButtonClickCommand))]
string title = "Count:";
[RelayCommand(AllowConcurrentExecutions = true)]
async Task ButtonClickAsync()
{
await Task.Delay(2000);
Title += "1";
}
}
变成:点击Button后,Button仍然可点击,依旧是2秒后更新TextBox内容,且2秒内可多次点击Button,更新的次数与点击次数保持一致 。
4.4 NotifyPropertyChangedFor
添加到实现ObservableProperty特性的字段,在其发生改变时会通知同名参数
public partial class MainViewModel : ObservableObject
{
/// <summary>
/// Title发生改变时,UI绑定Caption的控件也会收到通知
/// </summary>
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(Caption))]
string title;
public string Caption;
}
4.5 OnChanged,OnChanging
实现ObservableProperty的字段会自动新增方法,OnChanged,OnChanging,分别对应其值改变时和改变后
public partial class MainViewModel : ObservableObject
{
[ObservableProperty]
string title;
partial void OnTitleChanged(string oldValue, string newValue)
{
Console.WriteLine("Title has changed");
}
partial void OnTitleChanging(string oldValue, string newValue)
{
Console.WriteLine("Title is changing");
}
}
五、使用技巧
5.1 Json相关
现有一个简单DEMO:
public partial class MainView : Window
{
public MainView()
{
InitializeComponent();
MainViewModel vm = new MainViewModel() { Name = "Jack", Description = "Teacher" };
var data = JsonSerializer.Serialize(vm);
}
public partial class MainViewModel : ObservableObject
{
[ObservableProperty]
string name;
[ObservableProperty]
string description;
}
}
运行后data内容为:
{"Name":"Jack","Description":"Teacher"}
5.1.1 修改字段名
增加name的特性:
[ObservableProperty]
[property:JsonPropertyName("User name")]
string name;
运行后data内容为:
{"User name":"Jack","Description":"Teacher"}
5.1.2 忽略字段名
增加Description的特性
[ObservableProperty]
[property:JsonIgnore]
string description;
运行后data内容为:
{"User name":"Jack"}
5.2 未实现INotifyPropertyChanged的类实现通知功能
假设现在有一个类,它可能是系统生成的,或者外部类,总之他未实现NotifyPropertyChanged接口,属性在改变时无法通知UI:
public class Model
{
public string Name { get; set; }
public string Description { get; set; }
}
现在对其进行封装:
public partial class ObservableModel : ObservableObject
{
private readonly Model model;
public ObservableModel(Model model)
{
this.model = model;
}
public string Name
{
get => model.Name;
set => SetProperty(model.Name, value, model, (model, name) => model.Name = name);
}
[RelayCommand]
void ChangedName()
{
Name = "Tom";
}
}
将其绑定后运行发现,当ChangeNameCommand执行后,ObservableModel的属性Name已经变为Tom,而且Model的属性Name也已改变