sh1’s diary

プログラミング、読んだ本、資格試験、ゲームとか私を記録するところ

C# CommunityToolkit.Mvvm の学習6 RelayCommand

RelayCommands

RelayCommandRelayCommand<T>ICommand の実装で、メソッドやデリゲートを view に公開することができます。これらの型は viewmodel と UI 要素の間でコマンドを binding する手段として機能します。

APIs:

  • RelayCommand
  • RelayCommand
  • IRelayCommand
  • IRelayCommand

どのように機能するか (How it works)

RelayCommandRelayCommand<T> の特徴は、つぎのとおりです:

  • これらは ICommand インターフェースの base 実装を提供しています。
  • IRelayCommand or IRelayCommand<T> インターフェースを実装して CanExecuteChanged イベントを発生させる NotifyCanExecuteChanged メソッドを公開しています。
  • コンストラクタは、標準的なメソッドの Action や Funcラムダ式などのラッピングを可能とします。

ICommand の働き (Working with ICommand)

単純な command をセットアップする例は、つぎのとおりです:

public class MyViewModel : ObservableObject
{
    public MyViewModel()
    {
        IncrementCounterCommand = new RelayCommand(IncrementCounter);
    }

    private int counter;

    public int Counter
    {
        get => counter;
        private set => SetProperty(ref counter, value);
    }

    public ICommand IncrementCounterCommand { get; }

    private void IncrementCounter() => Counter++;
}

XAML はつぎのとおり:

<Page
    x:Class="MyApp.Views.MyPage"
    xmlns:viewModels="using:MyApp.ViewModels">
    <Page.DataContext>
        <viewModels:MyViewModel x:Name="ViewModel"/>
    </Page.DataContext>

    <StackPanel Spacing="8">
        <TextBlock Text="{x:Bind ViewModel.Counter, Mode=OneWay}"/>
        <Button
            Content="Click me!"
            Command="{x:Bind ViewModel.IncrementCounterCommand}"/>
    </StackPanel>
</Page>

Button は viewmodel の ICommand にバインドされて private メソッドの IncrementCounter をラップします。TextBlock は Counter プロパティの値を表示して、プロパティの値が更新されるたびに更新します。

Sample

public class MyViewModel : ObservableObject
{
    public MyViewModel()
    {
        IncrementCounterCommand = new RelayCommand(IncrementCounter);
    }

    /// <summary>
    /// Gets the <see cref="ICommand"/> responsible for incrementing <see cref="Counter"/>.
    /// </summary>
    public ICommand IncrementCounterCommand { get; }

    private int counter;

    /// <summary>
    /// Gets the current value of the counter.
    /// </summary>
    public int Counter
    {
        get => counter;
        private set => SetProperty(ref counter, value);
    }

    /// <summary>
    /// Increments <see cref="Counter"/>.
    /// </summary>
    private void IncrementCounter() => Counter++;
}
<Page
    x:Class="MyApp.Views.MyPage"
    xmlns:viewModels="[viewModels]using:MyApp.ViewModels">
    <Page.DataContext>
        <viewModels:MyViewModel x:Name="ViewModel"/>
    </Page.DataContext>

    <StackPanel Spacing="8">
        <TextBlock Text="{x:Bind ViewModel.Counter, Mode=OneWay}"/>
        <Button
            Content="Click me!"
            Command="{x:Bind ViewModel.IncrementCounterCommand}"/>
    </StackPanel>
</Page>

AsyncCommands

AsyncRelayCommandAsyncRelayCommand<T>RelayCommand の機能を拡張して、非同期操作をサポートする ICommand の実装です。

APIs:

  • AsyncRelayCommand
  • AsyncRelayCommand
  • RelayCommand
  • IAsyncRelayCommand
  • IAsyncRelayCommand

どのように機能するか (How it works)

AsyncRelayCommandAsyncRelayCommand<T> の主な特徴はつぎのとおり:

  • これらは、ライブラリーに含まれる同期 Commandの機能を拡張して Task を返却する delegate をサポートします。
  • キャンセルをサポートするために CancellationToken 引数を追加して非同期関数をラップすることができる。CancellationToken プロパティ、CanBeCanceled プロパティ、IsCancellationRequested メソッドを公開しています。
  • 保留中の操作の進行状況を監視するために使用できる ExecutionTask プロパティと、操作の完了を確認するために使用できる IsRunning プロパティを公開しています。これは特に loading のインジケーターなどの UI 要素を binding する際に便利です。
  • IAsyncRelayCommandIAsyncRelayCommand<T> インターフェースを実装しており viewmodel はこれらを使用して command を公開して、(view-viewmodel 間の)密結合を減らすことができます。例えば、同じ public API サーフェイスを公開するカスタム実装で command を置き換えることが簡単になります。(必要であれば)

非同期コマンドの働き (Working with asynchronous commands)

RelayCommand のサンプルで説明した内容で、(非同期化した)同サンプルはつぎのとおり:

public class MyViewModel : ObservableObject
{
    public MyViewModel()
    {
        DownloadTextCommand = new AsyncRelayCommand(DownloadText);
    }

    public IAsyncRelayCommand DownloadTextCommand { get; }

    private Task<string> DownloadText()
    {
        return WebService.LoadMyTextAsync();
    }
}

UI はつぎのとおり:

<Page
    x:Class="MyApp.Views.MyPage"
    xmlns:viewModels="using:MyApp.ViewModels"
    xmlns:converters="using:Microsoft.Toolkit.Uwp.UI.Converters">
    <Page.DataContext>
        <viewModels:MyViewModel x:Name="ViewModel"/>
    </Page.DataContext>
    <Page.Resources>
        <converters:TaskResultConverter x:Key="TaskResultConverter"/>
    </Page.Resources>

    <StackPanel Spacing="8" xml:space="default">
        <TextBlock>
            <Run Text="Task status:"/>
            <Run Text="{x:Bind ViewModel.DownloadTextCommand.ExecutionTask.Status, Mode=OneWay}"/>
            <LineBreak/>
            <Run Text="Result:"/>
            <Run Text="{x:Bind ViewModel.DownloadTextCommand.ExecutionTask, Converter={StaticResource TaskResultConverter}, Mode=OneWay}"/>
        </TextBlock>
        <Button
            Content="Click me!"
            Command="{x:Bind ViewModel.DownloadTextCommand}"/>
        <ProgressRing
            HorizontalAlignment="Left"
            IsActive="{x:Bind ViewModel.DownloadTextCommand.IsRunning, Mode=OneWay}"/>
    </StackPanel>
</Page>

Button をクリックすると Command が実行されて ExecutionTask が実行されます。作業が完了するとプロパティは通知を発生させるので UI に反映されます。この場合、タスクのステータスと現在の結果の両方が表示されます。タスクの結果を表示するには TaskExtensions.GetResultOrDefault メソッドを使用する必要があることに注意してください。これによって、スレッドをロックすることなく(デッドロックが発生する恐れもあるので)、まだ完了していないタスクの結果にアクセスできます。

Sample

public MyViewModel()
{
    DownloadTextCommand = new AsyncRelayCommand(DownloadTextAsync);
}

public IAsyncRelayCommand DownloadTextCommand { get; }

private async Task<string> DownloadTextAsync()
{
    await Task.Delay(3000); // Simulate a web request

    return "Hello world!";
}
<Page.Resources>
    <converters:TaskResultConverter x:Key="TaskResultConverter"/>
</Page.Resources>
<StackPanel Spacing="8">
    <TextBlock>
        <Run Text="Task status:"/>
        <Run Text="{x:Bind ViewModel.DownloadTextCommand.ExecutionTask.Status, Mode=OneWay}"/>
        <LineBreak/>
        <Run Text="Result:"/>
        <Run Text="{x:Bind ViewModel.DownloadTextCommand.ExecutionTask, Converter={StaticResource TaskResultConverter}, Mode=OneWay}"/>
    </TextBlock>
    <Button
        Content="Click me!"
        Command="{x:Bind ViewModel.DownloadTextCommand}"/>
    <muxc:ProgressRing
        HorizontalAlignment="Left"
        IsActive="{x:Bind ViewModel.DownloadTextCommand.IsRunning, Mode=OneWay}"/>
</StackPanel>

参考