The CommunityToolkit.Mvvm
package is mainly used for code generation, which helps reduce the amount of code users need to write. It facilitates the implementation of the MVVM design pattern in WPF, thereby lowering code complexity.
To introduce the project package:
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.1 "/>
To learn MVVM, you need to pay attention to the following types:
- CommunityToolkit.Mvvm.ComponentModel
- CommunityToolkit.Mvvm.DependencyInjection
- CommunityToolkit.Mvvm.Input
- CommunityToolkit.Mvvm.Messaging
- CommunityToolkit.Mvvm.Messaging.Messages
Now, let’s briefly introduce the usage of commonly used types.
ObservableObject and ObservableProperty
ObservableObject
serves as the base class for observable objects that implement the INotifyPropertyChanged
and INotifyPropertyChanging
interfaces. It can act as a starting point for various objects requiring property change notifications.
The type ObservableProperty
is a property that allows observable properties to be generated from annotated fields, significantly reducing the amount of boilerplate code needed to define observable properties.
First, define a ViewModel class.
// The ViewModel class needs to inherit from ObservableObject
public partial class DashboardViewModel : ObservableObject
{
// Define a field starting with an underscore, using camel case
[ObservableProperty]
private int _counter = 0;
public DashboardViewModel()
{
DownloadTextCommand = new AsyncRelayCommand(DownloadText);
}
}
Next, define a ViewModel property in your window.
The author demonstrates using a Page mode; readers can also use a Window.
public DashboardViewModel ViewModel { get; }
The property name can be arbitrary; it does not have to be named ViewModel.
Then, bind the property in the xaml file:
<TextBlock
Grid.Column="1"
Margin="12,0,0,0"
VerticalAlignment="Center"
Text="{Binding ViewModel.Counter, Mode=OneWay}" />
We defined _counter
, but the MVVM framework automatically generates some code for us.
The following is a simplified example; actual situations are much more complex.
public int Counter
{
get => _counter;
set
{
if (!EqualityComparer<int?>.Default.Equals(_counter, value))
{
OnCounterChanging(value);
OnCounterChanging(default, value);
_counter = value;
OnCounterChanged(value);
OnCounterChanged(default, value);
}
}
}
partial void OnCounterChanging(int value);
partial void OnCounterChanging(int value);
MVVM automatically creates the definitions for the step-by-step methods of this property, allowing us to respond to changes in its value.
Commands
If you want to trigger an event after clicking a button, in traditional WPF you would need to define a method. Using MVVM, button events can be treated as commands, making the entire process simpler.
You simply define a function in the ViewModel and add the [RelayCommand]
attribute:
public partial class DashboardViewModel : ObservableObject
{
// Define a field starting with an underscore, using camel case
[ObservableProperty]
private int _counter = 0;
// Still write private
[RelayCommand]
private void OnCounterIncrement()
{
// Note: Use the generated property instead of using _counter;
Counter++;
}
}
MVVM will automatically generate a command function named CounterIncrementCommand
.
Then bind the command in your xaml:
<ui:Button
Grid.Column="0"
Command="{Binding ViewModel.CounterIncrementCommand, Mode=OneWay}"
Content="Click me!"
Icon="Fluent24" />
<TextBlock
Grid.Column="1"
Margin="12,0,0,0"
VerticalAlignment="Center"
Text="{Binding ViewModel.Counter, Mode=OneWay}" />
You can also write it as an asynchronous method.
[RelayCommand]
private async Task OnCounterIncrement()
{
Counter++;
await Task.CompletedTask;
}
Additionally, the RelayCommand
attribute can be configured to enable or disable commands or allow concurrency among them.
Moreover, you can manually set up command property binding methods:
public partial class DashboardViewModel : ObservableObject
{
[ObservableProperty]
private int _counter = 0;
public DashboardViewModel()
{
UpdateCounterCommand = new RelayCommand(UpdateCounter);
}
public ICommand UpdateCounterCommand { get; }
private void UpdateCounter()
{
Counter++;
}
}
public DashboardViewModel()
{
UpdateCounterCommand = new AsyncRelayCommand(UpdateCounter);
}
public ICommand UpdateCounterCommand { get; }
private async Task UpdateCounter()
{
Counter++;
}
Bind the command in xaml:
<ui:Button
Grid.Column="0"
Command="{Binding ViewModel.UpdateCounterCommand, Mode=OneWay}"
Content="Click me!"
Icon="Fluent24" />
<TextBlock
Grid.Column="1"
Margin="12,0,0,0"
VerticalAlignment="Center"
Text="{Binding ViewModel.Counter, Mode=OneWay}" />
Property Change Notification
In MVVM, there is a feature called [NotifyPropertyChangedFor]
. What is its purpose? The author has researched for a long time without finding much information and can only test it gradually.
[NotifyPropertyChangedFor]
helps reduce the amount of code written for notifications.
Typically, WPF controls won't work directly by binding properties.
public partial class DashboardViewModel : ObservableObject
{
[ObservableProperty]
private int _counter = 0;
public int MapValue
{
get { return _counter; }
}
[RelayCommand]
private void OnCounterIncrement()
{
Counter++;
MapValue++;
}
/*
// Or alternatively:
public int MapValue
{
get { return _counter; }
}
[RelayCommand]
private void OnCounterIncrement()
{
Counter++;
}
*/
}
<ui:Button
Grid.Column="0"
Command="{Binding ViewModel.CounterIncrementCommand, Mode=OneWay}"
Content="Click me!"
Icon="Fluent24" />
<TextBlock
Grid.Column="1"
Margin="12,0,0,0"
VerticalAlignment="Center"
Text="{Binding ViewModel.Counter, Mode=OneWay}" />
<Label Grid.Column="2" Margin="60,0,0,0" Content="{Binding ViewModel.MapValue, Mode=OneWay}" />
As can be seen, the label on the right does not update when the value changes.
However, after modifying it to:
public partial class DashboardViewModel : ObservableObject
{
[NotifyPropertyChangedFor(nameof(MapValue))]
[ObservableProperty]
private int _counter = 0;
public int MapValue
{
get { return _counter; }
}
[RelayCommand]
private void OnCounterIncrement()
{
Counter++;
}
}
When the Counter
property changes, it will notify the bound MapValue
property to change as well.
This is suitable for scenarios where MapValue
is only used for display and does not need to be modified directly by the UI.
For example, to input a value and compute the square of x
:
[NotifyPropertyChangedFor(nameof(Result))]
[ObservableProperty]
private int _x = 0;
public int Result
{
get { return _x * _x; }
}
When x
value changes, it will notify the control to update the Result
value by re-invoking get
. For Result
, set
is meaningless.
Multiple properties can also [NotifyPropertyChangedFor]
to the same property.
[NotifyPropertyChangedFor(nameof(MapValue))]
[ObservableProperty]
private int _x = 0;
[NotifyPropertyChangedFor(nameof(MapValue))]
[ObservableProperty]
private int _y = 0;
public int MapValue
{
get { return _x * _y; }
}
When either X
or Y
changes, the interface will recalculate the result.
Additionally, you can use [NotifyCanExecuteChangedFor]
to automatically invoke commands during property changes.
。
However, the code below will have no effect; TestCommand
will not be executed.
public partial class DashboardViewModel : ObservableObject
{
[NotifyCanExecuteChangedFor(nameof(TestCommand))]
[ObservableProperty]
private int _counter = 0;
[RelayCommand]
private void OnCounterIncrement()
{
Counter++;
}
// TestCommand => testCommand ??= new RelayCommand(OnTest);
[RelayCommand]
private void OnTest()
{
Counter++;
}
Due to the limited information available from the official website and other sources, the author is unsure how to use it. The official CommunityToolkit / MVVM-Samples
repository does not provide information on the usage of [NotifyCanExecuteChangedFor]
.
However, based on the code generated by MVVM, theoretically, the TestCommand
should be executed. Since it's ineffective and there is no resource to consult, it will be left at that.
ObservableValidator
A type for model validation. Since ObservableValidator
is also an abstract class, it cannot be mixed with ObservableObject
.
Define a new ViewModel:
public partial class FormViewModel: ObservableValidator
{
private string name;
[Required]
[MinLength(2)]
[MaxLength(5)]
public string Name
{
get => name;
set => SetProperty(ref name, value, true);
}
[RelayCommand]
private void Check()
{
ValidateAllProperties();
if (HasErrors)
{
return;
}
}
}
public partial class DashboardPage : INavigableView<DashboardViewModel>
{
public DashboardViewModel ViewModel { get; }
public FormViewModel Form { get; }
public DashboardPage(DashboardViewModel viewModel)
{
ViewModel = viewModel;
DataContext = this;
Form = new FormViewModel();
InitializeComponent();
}
}
Using it on the UI:
<ui:Button
Grid.Column="0"
Command="{Binding Form.CheckCommand, Mode=OneWay}"
Content="Click me!"
Icon="Fluent24" />
<TextBox
Grid.Column="2"
Margin="12,0,0,0"
VerticalAlignment="Center"
Width="200"
Height="30"
BorderThickness="1,1,1,1"
Background="Blue"
Text="{Binding Form.Name, Mode=TwoWay}" />
If the model validation fails upon clicking, the corresponding input box will have some styles, such as a red border. However, customization of styles is often required, necessitating more extensive work.
文章评论