🛠️

WinUI x CommunityToolkit.Mvvm [RelayCommand編]

2024/06/11に公開

前回の記事では、

  • ObservableObject
  • ObservableProperty
    を紹介しましたが、この記事ではRelayCommandのサンプルコードを紹介します。

UI

サンプルコード

SomePageViewModel.cs
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System.Collections.ObjectModel;

namespace CommunityToolkitMvvmDemo;

public record Person(string FirstName, string LastName);

public partial class SomePageViewModel : ObservableObject
{
    [ObservableProperty]
    [NotifyPropertyChangedFor(nameof(FullName))]
    [NotifyCanExecuteChangedFor(nameof(AddPersonToListCommand))]
    private string _firstName = string.Empty;

    [ObservableProperty]
    [NotifyPropertyChangedFor(nameof(FullName))]
    [NotifyCanExecuteChangedFor(nameof(AddPersonToListCommand))]
    private string _lastName = string.Empty;

    [ObservableProperty]
    private ObservableCollection<Person> _people = [];

    public string FullName => $"{FirstName} {LastName}";

    private bool CanAddPerson =>
        string.IsNullOrEmpty(FirstName) is false &&
        string.IsNullOrEmpty(LastName) is false;

    [RelayCommand(CanExecute = nameof(CanAddPerson))]
    private void AddPersonToList()
    {
        var person = new Person(FirstName, LastName);
        People.Add(person);
        FirstName = string.Empty;
        LastName = string.Empty;
    }

    [RelayCommand]
    private void RemoveFromList(Person person)
    {
        People.Remove(person);
    }
}
SomePage.xaml.cs
using Microsoft.UI.Xaml.Controls;

namespace CommunityToolkitMvvmDemo;

public sealed partial class SomePage : Page
{
    public SomePage()
    {
        InitializeComponent();
    }

    public SomePageViewModel ViewModel { get; } = new();
}
SomePage.xaml
<?xml version="1.0" encoding="utf-8" ?>
<Page
    x:Class="CommunityToolkitMvvmDemo.SomePage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:local="using:CommunityToolkitMvvmDemo"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
    mc:Ignorable="d">

    <Page.Resources>
        <x:Double x:Key="Spacing">12</x:Double>
        <Style
            BasedOn="{StaticResource DefaultTextBoxStyle}"
            TargetType="TextBox">
            <Setter Property="Width" Value="200" />
        </Style>
    </Page.Resources>

    <Grid
        Padding="24"
        RowDefinitions="Auto,*"
        RowSpacing="{StaticResource Spacing}">
        <StackPanel
            Grid.Row="0"
            Spacing="{StaticResource Spacing}">
            <StackPanel
                Orientation="Horizontal"
                Spacing="{StaticResource Spacing}">
                <TextBox
                    Header="First Name"
                    Text="{x:Bind ViewModel.FirstName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
                <TextBox
                    Header="Last Name"
                    Text="{x:Bind ViewModel.LastName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
            </StackPanel>
            <StackPanel>
                <TextBlock Text="Full Name" />
                <TextBlock Text="{x:Bind ViewModel.FullName, Mode=OneWay}" />
            </StackPanel>
            <Button
                Command="{x:Bind ViewModel.AddPersonToListCommand}"
                Content="追加" />
            <Button
                Command="{x:Bind ViewModel.RemoveFromListCommand}"
                CommandParameter="{x:Bind PeopleList.SelectedItem, Mode=OneWay}"
                Content="削除"/>
        </StackPanel>

        <ListView
            x:Name="PeopleList"
            
            Grid.Row="1"
            ItemsSource="{x:Bind ViewModel.People}">
            <ListView.ItemTemplate>
                <DataTemplate x:DataType="local:Person">
                    <TextBlock>
                        <Run Text="{x:Bind FirstName}" />
                        <Run Text=" " />
                        <Run Text="{x:Bind LastName}" />
                    </TextBlock>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
    </Grid>

</Page>

解説

RelayCommand属性

次のように、AddPersonToListメソッドにRelayCommand属性を付けると、

[RelayCommand]
private void AddPersonToList()
{
    var person = new Person(FirstName, LastName);
    People.Add(person);
    FirstName = string.Empty;
    LastName = string.Empty;
}

CommunityToolkit.MvvmのソースジェネレーターがコマンドAddPersonToListCommandを自動生成してくれますので、

/// <summary>The backing field for <see cref="AddPersonToListCommand"/>.</summary>
[global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator", "8.2.0.0")]
private global::CommunityToolkit.Mvvm.Input.RelayCommand? addPersonToListCommand;
/// <summary>Gets an <see cref="global::CommunityToolkit.Mvvm.Input.IRelayCommand"/> instance wrapping <see cref="AddPersonToList"/>.</summary>
[global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator", "8.2.0.0")]
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
public global::CommunityToolkit.Mvvm.Input.IRelayCommand AddPersonToListCommand => addPersonToListCommand ??= new global::CommunityToolkit.Mvvm.Input.RelayCommand(new global::System.Action(AddPersonToList));

XAMLでCommandにバインディングできます。

<Button
    Command="{x:Bind ViewModel.AddPersonToListCommand}"
    Content="追加" />

もちろん引数にも対応しています。

[RelayCommand]
private void RemoveFromList(Person person)
{
    People.Remove(person);
}
/// <summary>The backing field for <see cref="RemoveFromListCommand"/>.</summary>
[global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator", "8.2.0.0")]
private global::CommunityToolkit.Mvvm.Input.RelayCommand<global::CommunityToolkitMvvmDemo.Person>? removeFromListCommand;
/// <summary>Gets an <see cref="global::CommunityToolkit.Mvvm.Input.IRelayCommand{T}"/> instance wrapping <see cref="RemoveFromList"/>.</summary>
[global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator", "8.2.0.0")]
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
public global::CommunityToolkit.Mvvm.Input.IRelayCommand<global::CommunityToolkitMvvmDemo.Person> RemoveFromListCommand => removeFromListCommand ??= new global::CommunityToolkit.Mvvm.Input.RelayCommand<global::CommunityToolkitMvvmDemo.Person>(new global::System.Action<global::CommunityToolkitMvvmDemo.Person>(RemoveFromList));
<Button
    Command="{x:Bind ViewModel.RemoveFromListCommand}"
    CommandParameter="{x:Bind PeopleList.SelectedItem, Mode=OneWay}"
    Content="削除" />

また、CanExecuteでコマンドの実行可否を制御し、

private bool CanAddPerson =>
    string.IsNullOrEmpty(FirstName) is false &&
    string.IsNullOrEmpty(LastName) is false;

[RelayCommand(CanExecute = nameof(CanAddPerson))]
private void AddPersonToList()
{
    var person = new Person(FirstName, LastName);
    People.Add(person);
    FirstName = string.Empty;
    LastName = string.Empty;
}

NotifyCanExecuteChangedFor属性でコマンド実行可否の変化をUIに通知することができます。

[ObservableProperty]
[NotifyPropertyChangedFor(nameof(FullName))]
[NotifyCanExecuteChangedFor(nameof(AddPersonToListCommand))]
private string _firstName = string.Empty;

[ObservableProperty]
[NotifyPropertyChangedFor(nameof(FullName))]
[NotifyCanExecuteChangedFor(nameof(AddPersonToListCommand))]
private string _lastName = string.Empty;
<Button
    Command="{x:Bind ViewModel.RemoveFromListCommand}"
    CommandParameter="{x:Bind PeopleList.SelectedItem, Mode=OneWay}"
    Content="削除"/>

さらに、次のような非同期メソッドの場合、

[RelayCommand]
private async Task LoadPeopleAsync()
{
    var people = await _someService.GetListAsync();
    People = new ObservableCollection<Person>(people);
}

にも対応し、非同期コマンドを生成してくれます。

/// <summary>The backing field for <see cref="LoadPeopleCommand"/>.</summary>
[global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator", "8.2.0.0")]
private global::CommunityToolkit.Mvvm.Input.AsyncRelayCommand? loadPeopleCommand;
/// <summary>Gets an <see cref="global::CommunityToolkit.Mvvm.Input.IAsyncRelayCommand"/> instance wrapping <see cref="LoadPeopleAsync"/>.</summary>
[global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator", "8.2.0.0")]
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
public global::CommunityToolkit.Mvvm.Input.IAsyncRelayCommand LoadPeopleCommand => loadPeopleCommand ??= new global::CommunityToolkit.Mvvm.Input.AsyncRelayCommand(new global::System.Func<global::System.Threading.Tasks.Task>(LoadPeopleAsync));

さらにさらに、IncludeCancelCommand=trueを設定することで、CancellationTokenを引数として渡せるようになり、

[RelayCommand(IncludeCancelCommand = true)]
private async Task LoadPeopleAsync(CancellationToken cancellationToken)
{
    var people = await _someService.GetListAsync(cancellationToken);
    People = new ObservableCollection<Person>(people);
}

LoadPeopleCommandだけでなく、LoadPeopleCancelCommandも生成してくれますので、

/// <summary>The backing field for <see cref="LoadPeopleCommand"/>.</summary>
[global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator", "8.2.0.0")]
private global::CommunityToolkit.Mvvm.Input.AsyncRelayCommand? loadPeopleCommand;
/// <summary>Gets an <see cref="global::CommunityToolkit.Mvvm.Input.IAsyncRelayCommand"/> instance wrapping <see cref="LoadPeopleAsync"/>.</summary>
[global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator", "8.2.0.0")]
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
public global::CommunityToolkit.Mvvm.Input.IAsyncRelayCommand LoadPeopleCommand => loadPeopleCommand ??= new global::CommunityToolkit.Mvvm.Input.AsyncRelayCommand(new global::System.Func<global::System.Threading.CancellationToken, global::System.Threading.Tasks.Task>(LoadPeopleAsync));

/// <summary>The backing field for <see cref="LoadPeopleCancelCommand"/>.</summary>
[global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator", "8.2.0.0")]
private global::System.Windows.Input.ICommand? loadPeopleCancelCommand;
/// <summary>Gets an <see cref="global::System.Windows.Input.ICommand"/> instance that can be used to cancel <see cref="LoadPeopleCommand"/>.</summary>
[global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator", "8.2.0.0")]
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
public global::System.Windows.Input.ICommand LoadPeopleCancelCommand => loadPeopleCancelCommand ??= global::CommunityToolkit.Mvvm.Input.IAsyncRelayCommandExtensions.CreateCancelCommand(LoadPeopleCommand);

非同期処理のキャンセルに使うことができます。

<Button
    Command="{x:Bind ViewModel.LoadPeopleCommand}"
    Content="ロード開始" />
<Button
    Command="{x:Bind ViewModel.LoadPeopleCancelCommand}"
    Content="ロードキャンセル" />

以上です

Happy Coding!😊

GitHubで編集を提案

Discussion