sh1’s diary

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

C# CommunityToolkit.Mvvm の学習4 ObservableObject

ObservableObject は、INotifyPropertyChanged と INotifyPropertyChanging インターフェースを実装することによって observable(監視)できるオブジェクトの base クラスです。プロパティの変更通知をサポートする必要のある、あらゆるオブジェクトの起点として使用することができます。

APIs:

  • ObservableObject
  • TaskNotifier
  • TaskNotifier

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

ObservableObject の主な特徴はつぎのとおりです:

  • INotifyPropertyChanged と INotifyPropertyChanging の基本実装を提供して、PropertyChanged と PropertyChanging イベントを公開します。
  • ObservableObject を継承した型のプロパティの値を簡単に設定して、適切なイベントを自動的に発生させるために使用できる SetProperty メソッドを提供します。
  • SetPropertyAndNotifyOnCompletion メソッドを提供します。このメソッドは SetProperty に似ています。Task プロパティを設定して、割り当てられたタスクが完了したときに通知イベントを発生させる機能を備えています。
  • OnPropertyChanged および OnPropertyChanging メソッドを公開しています。派生型を override することで、通知イベントの発生方法をカスタマイズすることができます。

単純な例

以下は、カスタムプロパティに通知のサポートを実装する例です:

public class User : ObservableObject
{
    private string name;

    public string Name
    {
        get => name;
        set => SetProperty(ref name, value);
    }
}

提供される SetProperty(ref T, T, string) メソッドは、プロパティの現在の値をチェックして、値が異なる場合は、値の更新+関連するイベントを自動的に発生させます。プロパティの名前は [CallerMemberName] 属性の使用によって、自動的に取得されるため、更新されるプロパティを手動で(記述するコード上で)指定する必要はありません。

サンプル

private string name;

/// <summary>
/// Gets or sets the name to display.
/// </summary>
public string Name
{
    get => name;
    set => SetProperty(ref name, value);
}
<StackPanel Spacing="8">
    <TextBox
        PlaceholderText="Type here to update the text below"
        Text="{x:Bind ViewModel.Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
    <TextBlock Text="{x:Bind ViewModel.Name, Mode=OneWay}"/>
</StackPanel>

non-observable モデルのラッピング

例えば、データベースのアイテムを扱う際によくあるシナリオを挙げます。データベースモデルのプロパティをリレーして、プロパティが変更されたことを通知する "bindable" な(バインディング可能な)ラップモデルを作成します。

これは、INotifyPropertyChanged インターフェースを実装していないモデルに通知機能のサポートを追加したい場合に必要なラッピングです。ObservableObject は、この処理を簡単にするための専用メソッドを提供します。

以下の例では、User は ObservableObject を継承せずに、データベースのテーブルを直接にマッピングしたモデルになります:

public class ObservableUser : ObservableObject
{
    private readonly User user;

    public ObservableUser(User user) => this.user = user;

    public string Name
    {
        get => user.Name;
        set => SetProperty(user.Name, value, user, (u, n) => u.Name = n);
    }
}

この場合、SetProperty<TModel, T>(T, T, TModel, Action<TModel, T>, string) の overload を使用します。これは、以前のシナリオのように backing field にアクセスできなくても、コードを効率的にするために必要なことです。このメソッドのシグネチャの各部分を詳しく見て、コンポーネントの役割を理解しましょう:

  • TModel は型引数で、ラップする(ターゲットの)モデル型のことを示しています。この例だと User クラスのことです。C# コンパイラーは、SetProperty メソッドをどのように呼び出しているのかによって、自動的に判断します。
  • T は設定したいプロパティの型です。TModel と同様に判断されます。
  • T.oldValue が最初の引数で、この例ではプロパティの現在値として user.Name を渡している。
  • T.newValue はプロパティに設定をする新しい値です。例では プロパティの入力値である value を渡しています。
  • TModel は、ラップする対象のモデルで、例では user インスタンスを渡します。
  • Action<TModel, T> callback は、プロパティに与えられた新しい値が現在値と違うとき、プロパティに新しい値を設定する際に呼び出される Action です。この callback 関数は、ターゲットのモデル TModel と設定する新しい値を引数として受け取っています。(入力値 n を Name プロパティに代入してるだけ)ここで重要なことは、現在のスコープから値の読み取りを避け、callback の引数として与えられた値のみと対話することです。この理由は、C# コンパイラが callback 関数をキャッシュして、多くのパフォーマンス改善ができるようになります。なので、ここではフィールドや setter のパラメーターにアクセスするのではなくて、ラムダ式の入力パラメーター(引数)だけを使用しています。

SetProperty<TModel, T>(T, T, TModel, Action<TModel, T>, string) メソッドは、非常にコンパクトな API を提供しながら、ターゲット+プロパティの取得と設定の両方ができるので、ラッピング プロパティの作成をかなり簡単にしてくれます。

Note: LINQ 式を使ったメソッドの実装に比べると、特に callback パラメーターの代わりに Expression<Func> 型のパラメーターを使用することで達成できるパフォーマンスの向上は大きいです。特にこのバージョンは LINQ 式を使ったものよりも ~200 倍高速で、メモリ割り当てを全くしない。

Task の扱い (Handling Task properties)

もしも、プロパティが Task なら、タスクが完了したときに通知イベントを発生させ、binding を適切なタイミングで更新する必要があります。(例えば、タスクに紐づいた読み込みのインジケーター、または、その他のステータス情報の表示)ObservableObject はこのシナリオのための API を持っています:

public class MyModel : ObservableObject
{
    private TaskNotifier<int>? requestTask;

    public Task<int>? RequestTask
    {
        get => requestTask;
        set => SetPropertyAndNotifyOnCompletion(ref requestTask, value);
    }

    public void RequestValue()
    {
        RequestTask = WebService.LoadMyValueAsync();
    }
}

SetPropertyAndNotifyOnCompletion<T>(ref TaskNotifier<T>, Task<T>, string) メソッドが、フィールドの更新、新しいタスクの監視、タスクが完了したときの通知イベントを行います。タスクプロパティに binding するだけで、そのステータスが更新されたときに通知を受け取ることができます。

TaskNotifier<T> は ObservableObject によって公開される特別な型で、ターゲットの Task インスタンスをラップして、このメソッドに必要な通知ロジックを有効化してくれます。TaskNotifier 型は、一般的なタスクのみを持っている場合、直接利用することもできます。

Note: SetPropertyAndNotifyOnCompletion メソッドは、Microsoft.Toolkit パッケージの NotifyTaskCompletion 型を置き換えるためのものです。この型が使用されていた場合、内部の Task or Task プロパティを置き換えることができ、SetPropertyAndNotifyOnCompletion メソッドを使用して値を設定、値の変更通知を発生させることができます。NotifyTaskCompletion 型によって公開されるすべてのプロパティは、Task インスタンスで直接利用できます。

Sample

private TaskNotifier myTask;

/// <summary>
/// Gets or sets the name to display.
/// </summary>
public Task MyTask
{
    get => myTask;
    private set => SetPropertyAndNotifyOnCompletion(ref myTask, value);
}

/// <summary>
/// Simulates an asynchronous method.
/// </summary>
public void ReloadTask()
{
    MyTask = Task.Delay(3000);
}
<StackPanel Spacing="8">
    <Button
        Content="Click me to load a Task to await"
        Click="{x:Bind ViewModel.ReloadTask}"/>
    <TextBlock Text="{x:Bind ViewModel.MyTask.Status, Mode=OneWay}"/>
</StackPanel>

参考

C# CommunityToolkit.Mvvm の学習3 RelayCommand + INotifyPropertyChanged

RelayCommand 属性

RelayCommand は、(属性を設定した)メソッドのリレーコマンドのプロパティを生成するための属性です。目的は、viewmodel の private メソッドをラップするコマンドを定義するために必要な定型文(ボイラープレートコード)を完全に削除することです。

Note: 属性を設定したメソッドを(RelayCommand として)動作させるためには partial クラスである必要があります。

APIs:

  • RelayCommand
  • ICommand
  • IRelayCommand
  • IRelayCommand
  • IAsyncRelayCommand
  • IAsyncRelayCommand
  • Task
  • CancellationToken

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

RelayCommand 属性は、次のように partial 型のメソッドに属性を付けることができます:

[RelayCommand]
private void GreetUser()
{
    Console.WriteLine("Hello!");
}

(上のコードから)次のようなコマンドが生成されます:

private RelayCommand? greetUserCommand;

public IRelayCommand GreetUserCommand => greetUserCommand ??= new RelayCommand(GreetUser);

Note: 生成されるコマンドの名前は、メソッド名に基づいて作成されます。ジェネレーターはメソッド名を利用しつつ、最後に "Command" の文字列を追加します。また "On" の接頭辞がある場合は、これを取り除きます。さらに、非同期メソッドの場合 "Async" 接尾辞も取り除きます。

Command パラメーター

RelayCommand 属性は、パラメーターを持つメソッドの Command の作成をサポートします。生成された Command は自動的に IRelayCommand<T> になり、代わりに同じ型のパラメーターを受け取ります:

[RelayCommand]
private void GreetUser(User user)
{
    Console.WriteLine($"Hello {user.Name}!");
}

以下のコードが生成されます:

private RelayCommand<User>? greetUserCommand;

public IRelayCommand<User> GreetUserCommand => greetUserCommand ??= new RelayCommand<User>(GreetUser);

作成した Command メソッドから、自動的に引数型を持つプロパティを生成します。

非同期コマンド (Asynchronous commands)

RelayCommandIAsyncRelayCommandIAsyncRelayCommand<T> インターフェースを介して、非同期メソッドへのラッピングもサポートしています。メソッドが Task 型を返すと自動的に処理されます。たとえば:

[RelayCommand]
private async Task GreetUserAsync()
{
    User user = await userService.GetCurrentUserAsync();

    Console.WriteLine($"Hello {user.Name}!");
}

以下のコードが生成されます:

private AsyncRelayCommand? greetUserCommand;

public IAsyncRelayCommand GreetUserCommand => greetUserCommand ??= new AsyncRelayCommand(GreetUserAsync);

メソッドがパラメーターを受け取る場合、結果の Command はジェネリックになります。メソッドが CancellationToken を持っている場合は特別で、キャンセルできるように伝搬されます。次のようなケースです:

[RelayCommand]
private async Task GreetUserAsync(CancellationToken token)
{
    try
    {
        User user = await userService.GetCurrentUserAsync(token);

        Console.WriteLine($"Hello {user.Name}!");
    }
    catch (OperationCanceledException)
    {
    }
}

生成されたコマンドはラップされたメソッドをトークンに渡すことになる。これによって、コンシューマーは IAsyncRelayCommand.Cancel を呼び出すだけでそのトークンをシグナルとして送りだすことができるので、保留中の処理を正しく停止することが可能です。

コマンドの有効化と無効化 (Enabling and disabling commands)

Command を無効化できることは、しばしば便利です。Command を無効にしておいて、後からその状態を実行可能かどうかを再度チェックできるようにすると、便利に使えることがよくあります。

これをサポートするために、RelayCommand 属性はプロパティを公開しています。このプロパティは、コマンドを実行できるかどうかを評価するために仕様するターゲット CanExecute プロパティ、または、メソッドを示すために使用できます:

[RelayCommand(CanExecute = nameof(CanGreetUser))]
private void GreetUser(User? user)
{
    Console.WriteLine($"Hello {user!.Name}!");
}

private bool CanGreetUser(User? user)
{
    return user is not null;
}

このメソッドはボタンが最初に UI からバインドされたときに呼び出されます。そのあと Command で IRelayCommand.NotifyCanExecuteChanged が実行されるたびに繰り返し呼び出しがされます。

Command を属性のプロパティにバインドしてその状態を制御する方法は次のとおり:

[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(SelectedUser))]
private User? selectedUser;
<!-- Note: this example uses traditional XAML binding syntax -->
<Button
    Content="Greet user"
    Command="{Binding GreetUserCommand}"
    CommandParameter="{Binding SelectedUser}"/>

この例では、生成された SelectedUser プロパティは、値が変更されるたびに GreetUserCommand.NotifyCanExecuteChanged() メソッドが呼び出されることになります。

UI (xaml) には、GreetUserCommand にバインドしている Button コントロールがあります。CanExecuteChanged イベントが発生するたびに、CanExecute メソッドが再度呼び出されます。

これによって、ラップされた CanGreetUser メソッドが評価されて、入力される User インスタンスxaml ではバインドした SelectedUser プロパティ)が null かどうかに基づいて、(つぎの新しい)ボタンの状態が返却されます。

つまり、(整理すると)SelectedUser が変更されるたびに、プロパティが値を持つかどうかに基づいて GreetUserCommand は有効になるかどうかの状態が決まります。これが、ここでのシナリオ(機能)の動作になります。

Note: Command は、CanExecute メソッド、または、プロパティの戻り値がいつ変更されたのかを自動的に認識していません。なので、IRelayCommand.NotifyCanExecuteChanged を呼び出して Command を無効化すること、CanExecute メソッドの再評価を要求すること、これらのコマンドにバインドされているコントロールの知覚的な状態を更新することは、開発者の責任です。

コマンドの同時実行の処理 (Handling concurrent executions)

コマンドが非同期である場合は、同時実行を許可するかどうかを決定する構成を常にできます。RelayCommand 属性を使う場合は AllowConcurrentExecutions プロパティを介して設定することができます。

この設定は、デフォルトでは false です。(この設定は)実行が保留されるまで、Command はその状態が無効であると通知することを意味しています。もしも、代わりに true を設定すると、任意の数の同時呼び出しを queue に設定することができます。

非同期処理の例外のハンドリング (Handling asynchronous exceptions)

非同期の RelayCommand が例外を処理する方法は2つあります。

  • await and rethrow (default): コマンド呼び出しの完了を await するとき、例外は同じ同期コンテキスト内で(そのまま)throw されます。これは通常、例外が throw されるとアプリケーションがクラッシュするだけで、通常の(同期をする)RelayCommand の例外動作と同じ挙動です。(例外が throw されると、アプリケーションは try-catch でもしない限りは、普通にクラッシュするということ)
  • flow exceptions to task scheduler(タスクスケジューラーに例外を流す): タスクスケジューラーに例外を渡すように RelayCommand を設定されている場合、例外は throw されてもアプリケーションはクラッシュしません。その代わりに、IAsyncRelayCommand.ExecutionTask を通じて TaskScheduler.UnobservedTaskException にバブリングされます。これによって、より高度なシナリオ(UI コンポーネントがタスクにバインドされ、操作の結果に応じて異なる結果を表示するなど)が可能になります。しかし、正しく(この機能を)使うには、さらに複雑になってしまいます。

デフォルトの振る舞いは、Command が例外を待ち受けて、再 throw をすることです。これは FlowExceptionsToTaskScheduler プロパティで(2つ目の)構成できます。

[RelayCommand(FlowExceptionsToTaskScheduler = true)]
private async Task GreetUserAsync(CancellationToken token)
{
    User user = await userService.GetCurrentUserAsync(token);

    Console.WriteLine($"Hello {user.Name}!");
}

この場合は、例外がアプリケーションをクラッシュさせることは無くなるので try-catch は必要ありません。他の無関係な例外も自動的に再 throw されなくなってしまうので、個々のシナリオごとにどのようなアプローチをするのかを注意深く決定して、適切なコードにする必要があります。

非同期処理のキャンセル (Cancel commands for asynchronous operations)

非同期 Command の最後のオプションは、キャンセルを要求する機能です。これは、非同期の RelayCommand をラップした ICommand の操作のキャンセル要求に使用できます。この Command は任意のタイミングで使用可能かどうかの反映をするために、状態を自動的に通知します。例えば、リンクされている Command が実行中でなければ、状態も実行可能ではないことを報告します。次のとおりです:

[RelayCommand(IncludeCancelCommand = true)]
private async Task DoWorkAsync(CancellationToken token)
{
    // Do some long running work...
}

DoWorkCancelCommand プロパティが生成されています。これを他の UI コンポーネントに bind することで、保留中の非同期処理を簡単にキャンセルすることができるようになります。

INotifyPropertyChanged 属性

INotifyPropertyChanged は、既存の型に MVVM サポートコードの利用を可能する属性です。

他の関連する属性 (ObservableObject, ObservableRecipient) と共にこの属性の目的は、これらの型の機能が必要な場合に開発者をサポートすることですが、ターゲットの型はすでに別の型から実装されているとします。

C# は多重継承ができないので、これらの属性は、(継承の)代わりに MVVM Toolkit のジェネレーターが(継承の)制限を回避して、別の型にコードを追加するために使用することができます。

Note: partial クラスの中である必要があります。これらの属性は、ターゲットの型が藤堂の型 (e.g. from) から継承できない場合のみに使用することを目的としています。継承が可能なら継承することをお勧めします。最終的なアセンブリに重複したコードが作成されることを避けられるので、バイナリサイズを削減できます。

APIs:

  • INotifyPropertyChanged
  • ObservableObject
  • ObservableRecipient

使い方 (How to use them)

この属性を使うことはとても簡単。partial クラスに追加するだけで、対応するコードが自動的にその型に対して生成されます。例えば:

[INotifyPropertyChanged]
public partial class MyViewModel : SomeOtherType
{    
}

これは、MyViewModel クラスに完全な INotifyPropertyChanged の実装を生成します。加えて、冗長性を減らすために仕様できる追加のヘルパー (SetProperty など) も共に生成します。以下、さまざまな属性の簡単な概要を挙げます:

  • INotifyPropertyChanged: インターフェースを実装し、プロパティを設定したり、イベントを発生させたりするためのヘルパーメソッドを追加します。
  • ObservableObject: ObservableObject 型のすべてのコードを追加します。概念的には INotifyPropertyChanged と等価で、主な違いは INotifyPropertyChanging を実装していることです。
  • ObservableRecipient: ObservableRecipient 型のすべてのコードを追加します。特に ObservableValidator を継承した型に追加することで、お互いを組み合わせることができます。

参考

C# CommunityToolkit.Mvvm の学習2 ObservableProperty

ObservableProperty の属性

ObservablePropery は、アノテーションされたフィールド(つまり属性)から observable プロパティを生成できる属性です。目的は、observable プロパティを定義するために必要なボイラープレートコードを減らすことです。

NOTE: 属性が機能するためには、INotifyPropertyChanged のインターフェースを継承した class の中である必要があります。

APIs:

  • ObservableProperty
  • NotifyPropertyChangedFor
  • NotifyCanExecuteChangedFor
  • NotifyDataErrorInfo
  • NotifyPropertyChangedRecipients
  • ICommand
  • IRelayCommand
  • ObservableValidator
  • PropertyChangedMessage
  • IMessenger

API 8.0 のリンクがまだありませんでした。2024 年 3 月現在は 8.2.2 が最新です。「LINK: 7.1.0

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

ObservableProperty 属性は、次のように「フィールド型」に注釈をつけるために使う:

[ObservableProperty]
private string? name;

上の属性は、つぎのような observable プロパティを生成します:

public string? Name
{
    get => name;
    set => SetProperty(ref name, value);
}

この書き方は、最適化された実行になるので、最終的な結果はより速くなります。

Note: 生成されるプロパティの名前は、フィールド名に基づいて作成されます。ジェネレーターは、フィールドの名前が lowerCamel, _lowerCamel, m_lowerCamel のいずれかであると仮定して、適切な .NET 命名規則に従うように変換します。生成されるプロパティは、常に public(アクセス修飾子)を持ちますが、field は任意のアクセス修飾子で宣言することが可能です。(もちろん private が推奨です)

Running code upon changes(変化に応じた実行コード)

実際には、生成されるコードはすこし複雑です。その理由は、通知のロジックにフックできる実装メソッドをいくつか公開しているので、プロパティが更新前/後に追加のロジックを実行できるからです。もし、必要なら、生成されるコードは(実際は)次のコードに似ています:

public string? Name
{
    get => name;
    set
    {
        if (!EqualityComparer<string?>.Default.Equals(name, value))
        {
            OnNameChanging(value);
            OnPropertyChanging();
            name = value;
            OnNameChanged(value);
            OnPropertyChanged();
        }
    }
}

partial void OnNameChanging(string? value);
partial void OnNameChanged(string? value);

これは、(これらの)2つのメソッドのいずれかを実装して、追加のコードを挿入できます:

[ObservableProperty]
private string? name;

partial void OnNameChanging(string? value)
{
    Console.WriteLine($"Name is about to change to {value}");
}

partial void OnNameChanged(string? value)
{
    Console.WriteLine($"Name has changed to {value}");
}

これら2つのメソッドのうちどちらか一方だけを実装することも、どちらも実装しないことも自由です。

partical メソッドはマニアックな機能なのでよく知らないなら「partial メソッドの拡張」なんかを参考に。

2つのメソッドが実装されない場合(あるいは、1つしか実装されない場合)、呼び出し全体がコンパイラによって削除されます。なので、この追加機能が必要ないケースの場合、パフォーマンスへの影響は全くありません。

Note: 生成メソッドは、実装コードはない「partial メソッド」です。実装することを選択した場合は、明示的なアクセス修飾子を指定できません。つまり、メソッドの実装も単なる partial メソッドして宣言しなければならず、それらは常に private アクセス修飾子を持つこと(持っていること)になります。明示的に(public や private の)アクセス修飾子を追加しようとしても、C# ではできないのでエラーの結果になります。(が、C# 9.0 で新しい partial が出てきたので混同に注意)

旧 partial が void 型のみで、新 partial が(なんらかの)型あり。新旧で別の機能と考えていたほうがよいくらいなので、やはり混同しないほうがいいと思います。

Notifying dependent properties(依存プロパティの通知)

Name プロパティが変更されるたびに通知を発生させたい FullName プロパティがあると仮定します。この場合 NotifyPropertyChangedFor 属性を使用することでもできるようになります:

[ObservableProperty]
[NotifyPropertyChangedFor(nameof(FullName))]
private string? name;

生成されるプロパティは、以下と同じです:

public string? Name
{
    get => name;
    set
    {
        if (SetProperty(ref name, value))
        {
            OnPropertyChanged("FullName");
        }
    }
}

Notifying dependent commands(依存コマンドの通知)

実行可否の状態が、プロパティの値に依存するコマンドがあると仮定します。これは、プロパティの値が変更されるたびにコマンドの実行可否の状態を再確認し直す必要があります。

言い換えれば ICommand.CanExecuteChanged を再実行することになる。この場合 NotifyCanExecuteChangedFor 属性を使用することでもできるようになります:

[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(MyCommand))]
private string? name;

生成されるプロパティは、以下と同じです:

public string? Name
{
    get => name;
    set
    {
        if (SetProperty(ref name, value))
        {
            MyCommand.NotifyCanExecuteChanged();
        }
    }
}

この機能は、コマンドは IRelayCommand で実装してないとダメです。(つまり、前提条件ありの組み合わせの機能)

Requesting property validation(プロパティの検証を要求する)

プロパティが ObservableValidator を継承する型で宣言されている場合、任意のバリデーション属性で修飾して、生成された setter にそのプロパティのバリデーションをトリガーするように要求することも可能です。これは NotifyDataErrorInfo 属性を使用することでもできるようになります:

[ObservableProperty]
[NotifyDataErrorInfo]
[Required]
[MinLength(2)] // Any other validation attributes too...
private string? name;

生成されるプロパティは、以下のようになります:

public string? Name
{
    get => name;
    set
    {
        if (SetProperty(ref name, value))
        {
            ValidateProperty(value, "Value2");
        }
    }
}

生成された ValidateProperty メソッドの呼び出しは、プロパティを検証して ObservableValidator オブジェクトの状態を更新します。

Note: 設計上、ValidationAttribute を継承する field 属性のみが生成されたプロパティに移ります。これは、データ検証のシナリオをサポートするためです。他のすべての field 属性は無視されます。(field に追加のカスタム属性を追加して、生成されたプロパティに適用することも今のところできない)もしも、(シリアライズを制御するためなどに)必要だったら、伝統的なプロパティを使ってください。

Sending notification messages(通知メッセージの送信)

プロパティが「ObservableRecipient」を継承する型で宣言されている場合、NotifyPropertyChangedRecipients 属性を使用してプロパティ変更に対してプロパティ変更メッセージを送信すうrコードを挿入するようにジェネレーターに指示することができます。これによって、(変更通知に)登録した受取者は変更に対して動的に反応すうることができます。次のコード:

[ObservableProperty]
[NotifyPropertyChangedRecipients]
private string? name;
public string? Name
{
    get => name;
    set
    {
        string? oldValue = name;

        if (SetProperty(ref name, value))
        {
            Broadcast(oldValue, value);
        }
    }
}

生成された Broadcast メソッドの呼び出しは、new PropertyChangedMessage を送信します。

参考

C# CommunityToolkit.Mvvm の学習1

WinUI3 でも利用できる MVVM として「MVVM Toolkit」の学習をはじめました。

その内容について、メモします。

MVVM Toolkit のサンプルプログラム

サンプルを実行する際に zip でダウンロードしてしまったときは、次のダウンロードが必要です。(git でやったほうが楽だったと思います)

MVVM-Samples から「msdocs-communitytoolkit」を見れば、必要なブランチのリンクになっているので、その zip をダウンロードします。そのあとは次のとおり:

  1. MVVM-Samples の zip を解凍
  2. msdocs-communitytoolkit に CommunityToolkit の zip の中身を解凍
  3. Visual Studio Installer で UWP を動作させるために必要なものをインストール
  4. 「MvvmSampleUwp.sln」を実行

これでもコンパイルをしてサンプルプログラムを実行できます。

以下は、サンプルプログラムに書かれている内容を(いつでも見直せるように)web 上にメモし直したものになります。

サンプルプログラムの本文はすべて英語なので、日本語訳にした際に雑訳(手抜き)があったり、省略した部分もあります。ご了承ください。「MS Learn」に掲載されている内容もありました。(あとから知りました)

Introduction to the MVVM Toolkit

MVVM Toolkit は、モダンで高速なモジュール MVVM ライブラリです。Windows Community Toolkit の一部であり、以下の原則に基づいて構築されています:

  • プラットフォーム、ランタイムから独立している(依存しない) .NET Standard 2.0、.NET Standard 2.1、.NET 6 (UI フレームワークに依存しない)
  • 簡単に使える&ピックアップ アプリケーションの構造、または、コーディングのパラダイム(枠組み)に厳しい要件がない
  • À la carte(アラカルト) 使用するコンポーネントを自由に選択できる。(アラカルトは、食堂などで客が自由に選んで選択できる一品料理のことで、自由に選択して利用できることを例えている)
  • リファレンスの実装 基本のクラスライブラリに含まれているインターフェースの実装を提供するが、インターフェースを直接使用する具体的な型はない

MVVM Toolkit は Microsoft によって、保守・公開されていて .NET Foundation の一部です。また Microsoft Store など Windows に組み込まれているいくつかのアプリケーションでも使用されています。

このパッケージ(MVVM Toolkit)は、.NET Standard をターゲットにしているので、どのプラットフォームのアプリケーションであっても利用できます:UWP, WinForms, WPF, Xamarin, Uno, .NET Native, .NET Core, .NET Framework, Mono など、どのランタイムでも使用できます。

これらはすべてで動作します。API のサーフェイスはすべて同じなので、共有ライブラリの構築に適しています。

一般に、静的ライブラリは、コンパイル時にリンクされるライブラリのこと。共有ライブラリは、プログラムの実行と同時にメモリ上に展開されるライブラリ。

加えて、MVVM Toolkit は .NET 6 のターゲットもあります。これは、.NET 6 上で実行する際により内部的な最適化を可能にしています。API サーフェイス(上述のとおり)同じなので、NuGet はプロジェクトのプラットフォームでどの API が利用できるのかを心配する必要はありません。

で、MVVM Toolkit パッケージはいつ使うべきなの?

MVVM Toolkit パッケージは MVVM パターンを使用してモダンなアプリケーションを構築するための実装を提供するための、標準的で自己完結型の軽量なコレクション(機能)にアクセスするために利用します。

通常、追加の外部参照を必要としません。多くのユーザーがアプリケーションを構築するには次の型だけで十分です:

  • CommunityToolkit.Mvvm.ComponentModel
    • ObservableObject
    • ObservableRecipient
    • ObservableValidator
  • CommunityToolkit.Mvvm.DependencyInjection
  • CommunityToolkit.Mvvm.Input
    • RelayCommand
    • RelayCommand
    • AsyncRelayCommand
    • AsyncRelayCommand
    • IRelayCommand
    • IRelayCommand
    • IAsyncRelayCommand
    • IAsyncRelayCommand
  • CommunityToolkit.Mvvm.Messaging
    • IMessenger
    • WeakReferenceMessenger
    • IRecipient
    • MessageHandler<TRecipient, TMessage>
  • CommunityToolkit.Mvvm.Messaging.Messages
    • ProperyChangedMessage
    • RequestMessage
    • AsyncRequestMessage
    • CollectionRequestMessage
    • AsyncCollectionRequestMessage
    • ValueChangedMessage

MVVM Toolkit パッケージは、可能な限り柔軟性を提供することを目的にしているので、開発者はどのコンポーネントを使うのか自由に選択することができます。

すべての型は疎結合であって、使用するものだけを含めればいいです。あなたのニーズにあわせて最適な方法で組み合わせてください。

MVVM Source generators

バージョン 8.0 から MVVM Toolkit には新しく Roslyn ソースジェネレータが含まれることになり MVVM を使用するコードを書く際のボイラープレート(繰り返し出てくる定型のコードブロックのこと)を大幅に削減しています。

これによって、observable プロパティやコマンドなどをセットアップする手間を簡単化できます。ソースジェネレーターについて知りたい人は「こちら」をご確認ください。これは、ソースジェネレーターがどのように動作するのか簡単化したものです。

コードを書いているときに、MVVM Toolkit のジェネレーターが(舞台裏で)追加のコードを生成します。このコードは、コンパイルされて、あなたのアプリケーションに含まれることになるので、最終的にはあなたがマニュアルで面倒なボイラープレートコードを書いた場合とまったく同じですが、その面倒な作業はすべてする必要ありません!🎉

例えば、observable プロパティをセットアップするなら:

private string? name;

public string? Name
{
    get => name;
    set => SetProperty(ref name, value);
}

単純な属性(ObservableProperty)で同じように表現できます:

[ObservableProperty]
private string? name;

コマンドを作ったら(ふつう)こうなる:

private void SayHello()
{
    Console.WriteLine("Hello");
}

private ICommand? sayHelloCommand;

public ICommand SayHelloCommand => sayHelloCommand ??= new RelayCommand(SayHello);

(RelayCommand)同じように表現できる:

[RelayCommand]
private void SayHello()
{
    Console.WriteLine("Hello");
}

.NET 6 からの新しい MVVM ソースジェネレーターを使えば、これらのことが可能になり、ほかにも多くのことができます!🙌

Note: ソースジェネレーターは、MVVM Toolkit の機能と独立して使用することができます。必要に応じて、ソースジェネレーターの書き方と以前の書き方を組み合わせたり、混在して使用することができます。例えば、ソースジェネレーターを徐々に使い始めて、すこしずつ冗長性を減らすのも自由だし、プロジェクト全体、または、アプリケーションで常にどちらかのアプローチを採用しなければならない、といった義務は無いということ。

次のドキュメントでは、MVVM ジェネレーターにどのような機能が含まれていて、どのように使用されているのかを説明しています:

  • CommunityToolkit.Mvvm.ComponentModel
    • ObservableProperty
    • INotifyPropertyChanged
  • CommunityToolkit.Mvvm.Input
    • RelayCommand

参考

この記事はすべて公開されています。この記事が「よかった」という方は、記事更新のためのサポートをお願いします。🚀

この続きを読むには
購入して全文を読む

Buffalo EasyMesh WiFi は1つの回線に複数設定しない(注意点)

家の WiFi 環境を EasyMesh にするときの注意点を知ったのでメモ。(知っている人からすると当たり前のことだと思うのですが)基本的には、EasyMesh は1回線につき1つの EasyMesh のみを設定すること、という記録です。

この記事の内容は 2024 年 2 月現在の状況です。WiFi はまだまだ移り変わるので事情が異なる恐れもあります。

下記の記事の動作テストは、次の機種を利用しました

WIFI

  • WSR-3200AX4S-BK
  • WEX-1800AX4EA

コントローラーの条件

たとえば、1階と2階で別々の EasyMesh を構築したいと思ってもそれはやらないほうがよいです。これは「Baffalo - EasyMesh よくある質問」に答えがあり、コントローラー(easymesh wifi 親機)は1台だけ、とあります。1階と2階に別々のコントローラーを設置することはできないので、よくない設定ということです。

この設定で次のトラブルが発生したため、注意が必要です。

トラブルの実例

EasyMesh のコントローラー(親機)は1台まで、というのは、詳細設定の EasyMesh 機能を「使用する」のチェックマークは1台だけしか(コントローラーに対しては)有効にしてはいけないです。ちなみに、この機能はデフォルトで ON です

仮に EasyMesh を使用しないコントローラー(WiFi 親機)も EasyMesh 機能を「使用する」のままにしていると EasyMesh を使用するコントローラーの調子が悪くなります。具体的には以下の症状が発生しました:

  • EasyMesh のコントローラー(親機) -> エージェント(子機)の接続設定はできるが、数メートルでエージェントの EasyMesh が途切れる
  • しばしばコントローラー(親機)の WiFi 通信に異常が発生する(機器の物理的な再起動で、コントローラーは再度 WiFi の利用ができる状態になってしまう)

また、EasyMesh はコントローラー(親機)1台に対して4台のエージェント(子機)しか接続できない、という物理的な制約があります。

WiFi のコントローラーは、ほとんどの場合1台で満足できるのですが、たとえとして3階の建物のケースを考えてみます。各階に2台ずつ設置しようとします。この場合は、合計6台が必要となってしまうので制約上不可能です。そこで1階だけ別の WiFi を設置することにします。すると、Buffalo のコントローラーはデフォルトで EasyMesh が ON なので、このまま運用すると全体の WiFi ネットワークを狂わせてしまうことになります。

なので、Buffalo の EasyMesh で構築できる網は建物の大きさによって物理的な限界があるようです。また物理的な1回線に対して1つの EasyMesh というのは2重ルーター(多重 NAT)も動作保証外だと「Buffalo チャットサポート」で回答を得ています。

対応策

EasyMesh を使用しないコントローラー(親機)は、詳細設定の EasyMesh 機能を「使用する」のチェックマークを OFF にしましょう。OFF にしたあとは、すべての WiFi を物理的に再起動しましょう。(再起動しないと EasyMesh はおかしいままでした)

また、コントローラー設定が正しく設定できている環境下で、エージェント(子機)の接続設定を再度実施しましょう。すでに接続設定をしてしまっている場合は、リセットをして再設定することをおススメします。(WEX-1800AX4EA は、再設定することで EasyMesh の通信が安定しました)

EasyMesh WiFi を構築する際は、建物に対してコントローラーとエージェントの最大(1+4)5台をどのような位置に設置していくのか、この構想が重要だと思いました。

WiFi は(スマートフォンの影響もあって)技術更新がはやい領域だと思うので、また5年~10年過ぎると全然違う事情になっているのだとは思いますが。

参考

WPF JumpList の使い方について

WPF のアプリを作っているときに JumpList を使おうと思ったときに、システムトレイアイコンのメニューとの使い方の違いを再認識しました。その内容をメモ。

JumpList

JumpList は Windows 7 から追加された windows の機能です。

JumpList とシステムトレイの右クリックメニューは、根本的に用途が異なります。JumpList は基本的にプログラムの起動を促すうごきをします

タスクバーにピン留めするという場合もあって、わかりやすい例だとアプリが実行されていない状態から JumpList を実行するケースもありえます。なので、実行中のアプリに対しての命令をメニューに含めようとすると不都合の生じる恐れがあります。

システムトレイのアプリケーションは、基本的にアプリケーションは(バックグラウンドかもしれませんが)実行状態であり、インスタンスがあります。なので、実行中のアプリに対しての命令をメニューに含めることが可能です。

.NET になった現在だと下記のコードで動きました。

var jumpList = new System.Windows.Shell.JumpList();
var jumpTask = new System.Windows.Shell.JumpTask()
{
    Title = "hello task",
};

jumpList.JumpItems.Add(jumpTask);
jumpList.Apply();

PresentationFramework.dll で動いているので初期の頃は dll の参照追加が必要だったり構築方法が変化する中で面倒な印象が残っています。現在は、特になにもしなくても JumpList を追加することができました。

簡単な機能なのでファイルを読み込む機能があり、履歴のような機能を追加するなら検討の余地がありそう。

使い方の例

Visual StudioVisual Studio Code も基本的には、ファイルを開く用途で使用しています。

ブラウザになると、特定のページを開くアクションをします。アプリケーションの起動+ページを開く動作なので上手い応用だと思いました。

一方で Notion のようなアプリケーションだと JumpList に対応していないのか、アプリケーションが不向きなのかはわかりませんが、利用がありません。なので必ずしも対応しなければいけない機能かといえば、そうでもなさそうです。

参考

WPF ToggleButton 注意点の整理

タイトルのとおり、WPF のコントール ToggleButton の使い方についてのメモです。ToggleButton は、通常のボタンに加えて IsChecked プロパティを持つコントールです。

IsChecked プロパティの動作で注意しないと VM 上で困ることがあったのでメモ。

ToggleButton のデザイン

ToggleButton にアイコンを表示したいときは XAML 上で Context に設定するのが手早い。このとき ToggleButton の色が変化する場合 Context に設定したコントールも色が変化しないと困る。

<ToggleButton x:Name="Toggle">
    <ui:SymbolIcon x:Name="Icon"
                    Padding="0" Margin="0"
                    Symbol="Edit32" 
                    Foreground="{Binding ElementName=Toggle, Path=Foreground}"
                    Filled="{Binding ElementName=Toggle, Path=IsChecked}">
    </ui:SymbolIcon>
</ToggleButton>

Foregournd と Filled を変更すると以下のようになって、ちょうどよいかもしれない。

ToggleButton のイベント

Clicked イベントを Command 化してもよいのですが IsChecked が ON/OFF のときで、それぞれ呼び出すイベントを切り替えたほうが便利。EventTrigger を利用した例を示す。

コード上で IsChecked を調べて if 文で2つで分けるのも同じだけど、メソッド名で整理できるだけ有利だと思う。

<ToggleButton>
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="Checked">
            <prism:InvokeCommandAction Command="{Binding OnCommand}" />
        </i:EventTrigger>
        <i:EventTrigger EventName="Unchecked">
            <prism:InvokeCommandAction Command="{Binding OffCommand}" />
        </i:EventTrigger>
    </i:Interaction.Triggers>
</ToggleButton>

コード上からの注意点

ToggleButton を ON や OFF に切り替えたいとき、Checked イベントや Unchecked イベントに Binding した各種コマンドを実行するだけだと、ビューのビジュアル上で整合性がとれなくなる。

OnCommand?.Invoke(); // IsChecked が切り替わらない

理由は、Toggle の ON/OFF のカラーが切り替わらないため。コード上から ON/OFF の切り替えを指示したいときは IsChecked プロパティを直接変更する必要があると思います。

<ToggleButton IsChecked="{Binding IsChecked}">
</ToggleButton>
IsChecked = true or false;

個人的に ToggleButton はあまり使わないこともあって ON/OFF 時のビジュアル変化が VM 上の不整合を起こすミスをしてもエラーにならないのは困る。仕様的なものなので、アプリケーションがクラッシュするようなバグにならないので気づくのに遅れる恐れがあると思う。

余談:添付プロパティで対応できるか?

過去の .NET Framework 3.5 ごろの WPF における ToggleButtonIsChecked は Binding に問題があったようです。そのせいか、私も Binding できないような記憶があって、別の方法を考えていました。

仮に添付プロパティで対応できないか検討します。(検討した内容を記録しただけの余談です)

これは問題があって、添付プロパティの値が切り替わったときにしか callback メソッドを実行できない。なので、初期化で苦労することになります。(添付プロパティは初期化メソッドの実行が無い!)

添付プロパティの値を Binding しておき Checked と Unchecked イベントが発生したときに添付プロパティの値を自動的に更新できるようにすればよいと思ったけど、初期値を true, false どちらの値でもよい、と考えるとイベントの割り当て初期化をどうするか、という話になる。

もうひとつ IsAttached を true にするような添付プロパティを用意しておけば、対応できるが冗長すぎる仕様になってしまう。個人的に添付プロパティで IsChecked を操作する意味はあまり無いと思った。

参考