sh1’s diary

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

デジタル庁 デザインシステムの整理

デジタル庁が公開している「デザインシステム」の内容を自分用に整理しました。

デザインシステムとは? (手引き)

デザインシステムとは「あるべきデザインを一貫性を持って、ユーザーに提供するための仕組み」のこと。

デジタル庁のデザインシステムは、使い勝手のよい(行政サービスを)効率よく構築するためのガイドライン or コード or テンプレート or コミュニティを提供する。

現在時点は、ソースコードとコミュニティは未提供。

デザインシステムが必要になった背景は、各府省庁が個別にウェブサービスを提供しているが、デザインがバラバラでユーザーはその都度の対応になっている。

なので、「アクセシビリティ」「ユーザビリティ」を担保したデザインパーツを再利用することで、一貫性のあるデザインを効率よく実現する。

  • 使いやすさの向上
  • 開発効率の向上
  • 改善サイクルの迅速化を図る
  • 課題やサービスの改善に集中する

調達の「前」にデザインの検討をはじめることで、その後の開発プロセスや試験を効率化して、品質を向上することができる。

カラースタイル

個々のサイト/サービスで使用されるスタイルガイドに必要なカラースキームの構築を目的とする。カラーが持つ意味や機能など情報伝達上の役割をユーザーが明確に認識できるカラースキームを構築すること。

カラー設計の原則として、利用者目線に立つことが重要。横断した統一性が重要。(ソフトウェア、マニュアルを含めるなど)

スタイルとは、サービス/ウェブサイトが目指している姿勢/態度/印象を表現する最小単位のルールを定義したもの。具体的には、カラースキーム、角丸半径、フォントサイズ、行間などの情報が含まれる。

デザイントーク

視覚的なデザインの属性に名前を付けたエンティティのこと。

ハードコードされた値に名前を与えることで、データが可読性を備えて、デザイナーとエンジニア間の共通言語として機能する。

プリミティブトークンは具体的な値(16進数のカラーコードなど)に名前を与えて、セマンティックトークンによって、さらにコンテクストを示す。

キーカラー

カラースキームを構築する上で必要なカラーを同じ色相の階調からキーカラーとして選択する。

  • プライマリーカラー : 主要となる色。(複数も可)ロゴ/ヘッダーといった重要なアクセント、ブランド要素に使用する。メインの背景色とのコントラスト比は少なくとも3:1以上を維持できる色であること。
  • セカンダリーカラー : プライマリーカラーを補完。
  • ターシャリーカラー : プライマリーカラー、セカンダリーカラーを補完。
  • バックグラウンドカラー : 背景に選ばれるカラー。テキストやコンテンツと良好なコントラストを提供する必要がある。

コントラスト比は、白い部分と黒い部分の輝度の差を比率で示したもの。

共通カラー

白から黒のグレーの階調(ニュートラルカラー)をベースに構築。

ページの共通の要素、テキスト、境界線、背景、UI の構成パーツなどに使用される。

  • テキストと背景色のコントラスト比は 4.5:1 以上を保つこと。
  • 枠線などは 3:1 以上のコントラスト比。
  • 純粋なデザイン要素とのコントラスト比は考慮する必要はない。

以下は諧調のテーブル:

  • white : #FFFFFF
  • gray-050 : #F2F2F2
  • gray-100 : #E6E6E6
  • gray-200 : #CCCCCC
  • gray-300 : #B3B3B3
  • gray-400 : #999999
  • gray-420 : #949494
  • gray-500 : #7F7F7F
  • gray-536 : #767676
  • gray-600 : #666666
  • gray-700 : #4D4D4D
  • gray-800 : #333333
  • gray-900 : #1A1A1A
  • black : #000000

機能カラー

UI でのユーザーのインタラクションの状態をより明確にする役割。

ボタンなどキーカラーで一貫性を維持するものと、テキストリンクカラーなどオンライン上で習慣化されたカラーを使用する場合がある。

リンクカラーは、コントラスト比 4.5:1 以上を確保する。Web の文脈では伝統的な習慣として青 (default) と紫 (visited) を使用する。

アクセントカラー

特定の要素に注意を引くためのカラー。多用されない程度に使用する。特定の意味を持ったハイライト表現や CTA ボタンに使うことで抑揚を抑えて重要性を示すことができる。

CTA = call to action のこと。行動喚起。読者を具体的な行動に誘導する仕組みで CTA をしっかり目立たせることで、行動を誘導する/できる。

セマンティックカラー

セマンティック (semantic) とは「意味の、語義の、意味論の、意味的な」などといった意味を持つ形容詞。

なので、デザインスタイルや UI の種別を問わず、特定の意味や目的が割り当てられたカラーのこと。これらのカラーは特定の情報/文脈で意味を伝えるので、情報伝達上の機能的な意味を持つ。

例えば、緑といえば成功で赤ならエラーといった、一貫性のある特定の意味を持った視覚的な共通言語のこと。

ニュートラルカラー

背景、ボーダーなど、デザイン要素に共通のカラーとして使用する。

プリミティブカラー 2.0

カラースタイルの基盤となる基本的な色で構成される。統一感のあるカラースキームを作成するために利用する。

各ベースカラーは広範なカラー設定を導入するための色相とメイドの段階で構成される。

緑は例。いろんな色を基本にプリミティブカラーを作ることができる。デジタル庁デザインシステムが独自に指定してカラーパレットをプリミティブカラーとして指定しています。

角の形状

ボタンやカードなどのコンポーネントに適用する。画面に視覚的な抑揚を生み出して、コンポーネントの機能理解を促進したり、認識しやすくさせる。

角の形状は、まるみの量に基づいて算出した径をコンポーネントに割り当てること。5つのスタイルを基本の目安とする。

  • 角丸なし
  • 角丸半径 8
  • 角丸半径 16
  • 角丸半径 32
  • 角丸半径:高さ or 横幅 50%

同一の半径スタイルを適用しても、図形の大きさに応じて印象は、図形が小さいときに角丸の印象は強く、図形が大きいときに角丸の印象はなくなっていく。サイズにあわせて、形状とスタイルを設定する。(例:小ボタン 16, 中ボタン 20, 大ボタン 24)

エレベーション

コンポーネントの高さの度合い。高さを表す表現には、ドロップシャドウとオーバーレイシェードがある。

オーバーレイシェードは、コンポーネントの上に重ねて表示するために、下になるコンポーネントとの間に敷き詰められる背景のこと。オーバーレイは「上に置く、上塗りする、うすく覆う、重ね合わせる」といった意味。

ほとんどの要素の高さはレベル0です。高さレベルに応じたスタイル1~8を適用します。

画像のアスペクト比(+レイアウト)

動画やイメージ画像のアスペクト比は 16:9 です。写真や一部のスクリーンなどは 3:2 を使用する。人物紹介等は 1:1 です。バナーは国際標準バナーサイズなど。

スクリーン幅: 1024px

  • マージン : > 32px
  • ガター : 32px
  • グリッド : 48px

マージンとは、最も外側の左右の領域のこと。ガターは、要素とコンテンツを分離するスペースのこと。グリッドは、基本的なレイアウト構造で、行と列に整理する。カラムグリッドやモジュラーグリッドなどがある。

ブレークポイントは、レスポンシブ/アダプティブの切り替えになる大きさのこと。

デスクトップは、ビューポートの範囲 768 px 以上を推奨する。

余白

見出しの上、見出しと本文の間に使うスペーシングは右の表を基本とする。

H の上 H と Body の間
H1 64px 24px
H2 64px 24px
H3 40px 24px
H4 40px 16px
H5 40px 16px
H6 24px 16px

バリエーションは 8px のスペーシングを基本とする。大きなサイズのスペーシングはフィボナッチ数列を用いて定義する。小さなサイズのスペーシングは2で割り定義する。

フォント

フォントは「Noto Sans Japanese」を採用。

見出し

サイズ px weight line-height letter-spacing
XXL 57 Regular 1.4 0.04em
XL 45 Regular 1.4 0.04em
L 36 Regular 1.4 0.04em
L mobile 32 Medium 1.5 0.04em
M 32 Regular 1.5 0.04em
M mobile 28 Medium 1.5 0.04em
S 28 Regular 1.5 0.04em
S mobile 24 Medium 1.5 0.04em
XS 24 Regular 1.5 0.04em
XS mobile 20 Medium 1.5 0.04em
XXS 20 Regular 1.5 0.04em
XXS mobile 16 Medium 1.7 0.04em

その他のテキストスタイル

サイズ px weight line-height letter-spacing 用途
本文 L 16 Regular 1.7 0.04em デフォルトの本文テキスト。
本文 M 14 Regular 1.7 0.04em 文量が多いとき、表示領域が限られているときに使用する。
ラベル L 14 Regular 1.5 0.04em デフォルトのラベルテキスト。
ラベル M 12 Regular 1.5 0.04em 文量が多いとき、表示領域が限られているときに使用する。
補足 L 14 Regular 1.7 0.04em デフォルトの補足テキスト。
補足 M 12 Regular 1.7 0.04em 文量が多いとき、表示領域が限られているときに使用する。
ボタン 16 Bold 1.5 0.04em ボタン中のテキスト。

参考

C# CommunityToolkit.Mvvm 8.2 の確認

.NET Community Toolkit.Mvvm の記事を10回に分けて更新してきましたが、今回は .NET Blog に掲載された「Announcing .NET Community Toolkit 8.2! Faster generators, code fixers, performance improvements and more!」の内容をメモしました。

和訳は、ざっくりです。DeepL なども使っていますが全部ではないので、間違いもあると思います。楽したくて「読点 "、"」 を「句点 "。"」によく置きかえて訳しています。

.NET Community Toolkit 8.2!

.NET Community Toolkit 8.2 のリリースについて ooficial launch を発表できることをうれしく思います!

新しいバージョンは、MVVM Toolkit のソースジェネレータ、および、runtime の両方のパフォーマンス改善、生産性を向上させるための新しい code fixer、ユーザーから提案のあった新しい機能などを含んでいます。

.NET Community Toolkit をより良いものにするために助けてくれるみんなに感謝します!🎉

.NET Community Toolkit には何がありますか?👀 (What’s in the .NET Community Toolkit? 👀)

.NET Community Toolkit には、以下のライブラリが含まれています:

  • CommunityToolkit.Common
  • CommunityToolkit.Mvvm (also known as “Microsoft MVVM Toolkit”)
  • CommunityToolkit.Diagnostics
  • CommunityToolkit.HighPerformance

これらのコンポーネントは、Microsoft Store や Photo App など Windows に同梱されているいくつかのアプリでもよく使われています。🚀

.NET Community Toolkit の歴史の詳細については、以前の 8.0.0 の「記事」をご覧ください。

以下は、.NET Community Toolkit 8.2 の新しいリリースに含まれている主な変更点についてのかみ砕いた内容 (break down) です。

Custom attributes [RelayCommand] 🤖

MVVM Toolkit 8.1.0 リリースで行わた作業をフォローアップして、MVVM Toolkit 8.2.0 では(GitHub で提案のあった)RelayCommand を使用する際にカスタム属性のサポートが含まれます。

C# の構文を活かして field:***property:*** といった、カスタム属性のターゲットを示します。これによって、RelayCommand を使用して MVVM command を生成する際に、生成されるすべてのメンバーの属性を制御できるようになりました。

例えば、JSON シリアライズをサポートする必要のある viewmodelを使用しているとします。生成されるプロパティを明示的に無視する (ignore) 場合、特に便利です。

次のようにサポートできます:

[RelayCommand]
[property: JsonIgnore]
private void DoWork()
{
    // Do some work here...
}

後ろでは、次のようになっています:

private RelayCommand? _doWorkCommand;

[JsonIgnore]
public IRelayCommand DoWorkCommand => _doWorkCommand ??= new RelayCommand(DoWork);

生成された DoWorkCommand プロパティは、(予想通りの)属性を持っています! もちろん、コンストラクタや名前付きパラメーターを持つ属性もサポートします。加えて、field:***property:*** 属性の2つを組み合わせて使うこともできます。🙌

新しい source generator に関するドキュメントはすべて「ここ」で見ることができます。また、ビデオがいいなら James Montemagno が「いくつか」作っています。

新しい ObservableProperty の変更フック⚗️

MVVM で比較的一般的なシナリオは、例えば、(現在選択されているユーザーやネストされた viewmodel を表すような)"selected item" の observable プロパティを持つことです。

プロパティの値が変更されたとき、古いインスタンスと新しいインスタンスに対して、何らかの調整をしなければならないことは珍しいことではありません。例えば "selected" プロパティの設定やイベントの購読などがそうです。

以前の場合では、ObservableProperty を使用するのは、理想的とはいえないシナリオでした。なぜなら、設定されている新/旧の値に対して必要な状態変更をするための、このようなロジックを楽に挿入するために必要な機能を持っていなかったためです。

この(持っていない状態を)修正するために、MVVM Toolkit 8.2 のリリースから、すべての ObservableProperty のフィールドに対して、生成される2つの新しいプロパティの変更フックがあります。

例えば、次のようなコードを考えてみます:

[ObservableProperty]
private DocumentViewModel _selectedDocument;

これは、次のようなコードが生成されます:

public DocumentViewModel SelectedDocument
{
    get => _selectedDocument;
    set
    {
        if (!EqualityComparer<DocumentViewModel>.Default.Equals(_selectedDocument, value))
        {
            DocumentViewModel? oldValue = _selectedDocument;
            OnNameChanging(value);
            OnNameChanging(oldValue, value);
            OnPropertyChanging();
            _selectedDocument = value;
            OnNameChanged(value);
            OnNameChanged(oldValue, value);
            OnPropertyChanged();
        }
    }
}

partial void OnSelectedDocumentChanging(DocumentViewModel value);
partial void OnSelectedDocumentChanged(DocumentViewModel value);

partial void OnSelectedDocumentChanging(DocumentViewModel? oldValue, DocumentViewModel newValue);
partial void OnSelectedDocumentChanged(DocumentViewModel? oldValue, DocumentViewModel newValue);

2つの新しい "OnPropertyNameChaning" メソッドと "Changed" メソッドが生成されていることに注目ください。

2つは、コードを挿入するために使いやすいフックを提供しています。各プロパティの変更イベントでトリガーされて、設定されている新/旧の両方の値を変更することができます。

例えば、以下のように使用できます:

partial void OnSelectedDocumentChanging(DocumentViewModel? oldValue, DocumentViewModel newValue)
{
    if (oldValue is not null)
    {
        oldValue.IsSelected = false;
    }

    newValue.IsSelected = true;
}

必要なことはこれだけです! viewmodel から OnSelectedDocumentChanging の呼び出しがされます。ObservableProperty には、このサポート機能が組み込まれています。(partial に注意)

Note: MVVM Toolkit は、コードジェネレーターだけで最適化をするために、上記のメソッドを使用しているかどうかを自動的に検出しています。(実際のコード上に利用されていない)実装されていない(生成予定だった)メソッドの呼び出しはコンパイラによって削除されます。なので、この機能は完全に pay for play です!(使うときにだけ支払う、のような意味だと思います)

MVVM Toolkit の Code Fixer (MVVM Toolkit code fixers 📖)

MVVM Toolkit の以前のリリースで、2つの新しい診断アナライザーを追加しました。 これは、ObservableProperty でマークされたフィールドに誤ってアクセスしたとき、または、ObservableProperty と同様の属性を持つ型を宣言したときに、(継承可能なときは)警告を生成します。

8.2 のリリースでは、これら2つのアナライザーに built-in の code fixer が追加されます。

警告を出すたびに、インテリセンスの電球にカーソルを合わせてコード修正を選択すれば、コードを正しい形に戻すための変更作業を自動的に適用できるようになりました! また、一括修正もサポートしているので、ワンクリックでエラーを修正することもできます!✨

[ObservableProperty]
private string? _name;

[RelayCommand]
private void ResetName()
{
    // 修正前 (警告が出る)
    _name = null;

    // インテリセンスによる指摘で修正できる
    Name = null;
}

この機能では Visual Studio の新しい code fixer の UI を確認しました。変更のプレビューやターゲットスコープに修正を適用するオプションが表示される、といった内容でした。

MVVM Toolkit ソース・ジェネレーターの最適化 (MVVM Toolkit source generator optimizations 🛫)

毎回のリリースと同じように、MVVM Toolkit 8.2 でもソースジェネレーターのパフォーマンス改善が含まれています。

今回は、incremental pipelines を最適化してメモリ使用量を最小化したこと、同時実行時に不要なオブジェクトは永続しないようにすることにフォーカスしました。改善するために行われた内容 PR をいくつか紹介します。

  • Move remaining diagnostics to analyzers (#581): MVVM Toolkit の診断機能が(プロセス外で同時実行できる)diagnostic analyzer に移動した。これにより incremental pipelines からいくつか Roslyn シンボルが削除されて、パフォーマンスが向上。

  • Resolve symbols early in analyzers (#587): 必要なアナライザーのシンボルはすべて、最初のコールバックのセットアップ中に解決されるようになった。各コンパイルインスタンスでのコールバック実行が高速化した。

Other changes and improvements🚀

略。

リリースの全変更履歴は「ここ」。

今日からはじめろ! (Get started today! 🎉)

すべてのソースコードGitHub に、ドキュメントは MS Learn に、完全な API リファレンスは .NET API ブラウザの web サイトにある。

Happy coding! 💻

参考

レガシーコードとどう付き合うか

レガシーコードとどう付き合うか

Amazon

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

C# CommunityToolkit.Mvvm の学習9 collections

ObservableGroup<TKey, TElement> and ReadOnlyObservableGroup<TKey, TElement>

ObservableGroup<TKey, TElement>, ReadOnlyObservableGroup<TKey, TElement> は、それぞれ ObservableCollection, ReadOnlyObservableCollection を継承したカスタム observable collection 型です。グループ化のサポートも提供しています。

連絡先のリストを表示するなど、グループ化されたアイテムのコレクションを UI に binding する場合に特別便利です。

Sample

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++;
}
<Grid Height="480">
    <Grid.Resources>

        <!--  Shared menu flyout for all contacts  -->
        <MenuFlyout x:Key="ContactMenuFlyout">
            <MenuFlyoutItem
                Command="{x:Bind ViewModel.DeleteContactCommand}"
                CommandParameter="{Binding}"
                Text="Remove contact">
                <MenuFlyoutItem.Icon>
                    <SymbolIcon Symbol="Delete" />
                </MenuFlyoutItem.Icon>
            </MenuFlyoutItem>
        </MenuFlyout>

        <!--  SemanticZoom grouped sourc  -->
        <CollectionViewSource
            x:Name="PeopleViewSource"
            IsSourceGrouped="True"
            Source="{x:Bind ViewModel.Contacts, Mode=OneWay}" />

        <!--  Contact template  -->
        <DataTemplate x:Key="PersonListViewTemplate" x:DataType="contacts:Contact">
            <Grid ContextFlyout="{StaticResource ContactMenuFlyout}">
                <Grid.RowDefinitions>
                    <RowDefinition Height="*" />
                    <RowDefinition Height="*" />
                </Grid.RowDefinitions>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="Auto" />
                    <ColumnDefinition Width="*" />
                </Grid.ColumnDefinitions>
                <Ellipse
                    x:Name="Ellipse"
                    Grid.RowSpan="2"
                    Width="32"
                    Height="32"
                    Margin="6"
                    HorizontalAlignment="Center"
                    VerticalAlignment="Center">
                    <Ellipse.Fill>
                        <ImageBrush ImageSource="{x:Bind Picture.Url}" />
                    </Ellipse.Fill>
                </Ellipse>
                <TextBlock
                    Grid.Column="1"
                    Margin="12,6,0,0"
                    x:Phase="1"
                    Style="{ThemeResource BaseTextBlockStyle}"
                    Text="{x:Bind Name}" />
                <TextBlock
                    Grid.Row="1"
                    Grid.Column="1"
                    Margin="12,0,0,6"
                    x:Phase="2"
                    Style="{ThemeResource BodyTextBlockStyle}"
                    Text="{x:Bind Email}" />
            </Grid>
        </DataTemplate>
    </Grid.Resources>

    <!--  Loading bar  -->
    <muxc:ProgressBar
        HorizontalAlignment="Stretch"
        VerticalAlignment="Top"
        Background="Transparent"
        IsIndeterminate="{x:Bind ViewModel.LoadContactsCommand.IsRunning, Mode=OneWay}" />

    <!--  Contacts view  -->
    <SemanticZoom>
        <SemanticZoom.ZoomedInView>
            <ListView
                ItemTemplate="{StaticResource PersonListViewTemplate}"
                ItemsSource="{x:Bind PeopleViewSource.View, Mode=OneWay}"
                SelectionMode="Single">
                <ListView.GroupStyle>
                    <GroupStyle HidesIfEmpty="True">
                        <GroupStyle.HeaderTemplate>
                            <DataTemplate x:DataType="collections:IReadOnlyObservableGroup">
                                <TextBlock
                                    FontSize="24"
                                    Foreground="{ThemeResource SystemControlHighlightAccentBrush}"
                                    Text="{x:Bind Key}" />
                            </DataTemplate>
                        </GroupStyle.HeaderTemplate>
                    </GroupStyle>
                </ListView.GroupStyle>
            </ListView>
        </SemanticZoom.ZoomedInView>
        <SemanticZoom.ZoomedOutView>
            <GridView
                HorizontalAlignment="Stretch"
                ItemsSource="{x:Bind PeopleViewSource.View.CollectionGroups, Mode=OneWay}"
                SelectionMode="Single">
                <GridView.ItemTemplate>
                    <DataTemplate x:DataType="ICollectionViewGroup">
                        <Border Width="80" Height="80">
                            <TextBlock
                                HorizontalAlignment="Center"
                                VerticalAlignment="Center"
                                FontSize="32"
                                Foreground="{ThemeResource SystemControlHighlightAccentBrush}"
                                Text="{x:Bind Group.(collections:IReadOnlyObservableGroup.Key)}" />
                        </Border>
                    </DataTemplate>
                </GridView.ItemTemplate>
            </GridView>
        </SemanticZoom.ZoomedOutView>
    </SemanticZoom>
</Grid>

ObservableGroup<TKey, TElement> features

ObservableGroup<TKey, TElement> および ReadOnlyObservableGroup<TKey, TElement> の主な特徴は次のとおりです:

  • コレクションにアイテムが追加/削除/変更されたときに同じ通知のサポートを提供します。
  • IGrouping<TKey, TElement> インターフェースを実装します。インスタンスで動作するすべての既存 LINQ 拡張機能の引数としてインスタンスを使用できます。
  • MVVM Toolkit のいくつかのインターフェースを実装します。(IReadOnlyObservableGroup, IReadOnlyObservableGroup, IReadOnlyObservableGroup<TKey, TElement>)コレクションの型のインスタンスに対する異なるレベルの抽象化を可能にします。これは、部分的な型情報しか利用できない、または、利用できるデータテンプレートで役に立ちます。

ObservableGroupedCollection<TKey, TElement> and ReadOnlyObservableGroupedCollection<TKey, TElement>

ObservableGroupedCollection<TKey, TElement> および ReadOnlyObservableGroupedCollection<TKey, TElement> は、各項目がグループ化された observable collection の型です。また ILookup<TKey, TElement> も実装しています。

ObservableGroupedCollection<TKey, TElement> features

ObservableGroupedCollection<TKey,TElement> および ReadOnlyObservableGroupedCollection<TKey,TElement> には、主に以下の特徴があります:

  • ObservableCollection および ReadOnlyObservableCollection を継承しています。ObservableGroup<TKey, TElement> および ReadOnlyObservableGroup<TKey, TElement> のように、アイテム(この場合はグループ)が追加/削除/変更されたときの通知も提供します。
  • ILookup<TKey, TElement> インターフェースを実装します。LINQ の相互運用性を高めています。
  • ObservableGroupedCollectionExtensions 型の API を通じて、コレクション内のグループやアイテムを簡単に操作するための追加のヘルパーメソッドを提供しています。

collections の章は現在おそらく web 上では公開されていません。内容も確認程度のことなので、新しく重要な情報はそれほどないように思いました。

参考

レガシーコードとどう付き合うか

レガシーコードとどう付き合うか

Amazon

C# CommunityToolkit.Mvvm の学習8 IoC

IoC (Inversion of control)

MVVM パターンを使用するアプリケーションのコードベースのモジュール性を高めるに利用できる一般的なパターンは、なにか「制御の反転 (Inversion of control)」を使用することです。

もっとも一般的なソリューションのひとつは、「依存性の注入 (dependency injection)」です。バックエンドの class に注入されるサービスの作成で、依存関係を注入します。(viewmodel のコンストラクターにパラメータとして渡す)

これにより、サービスを使用するコードは、実装の詳細に依存しません。また、これらのサービスの具体的な実装を交換することが簡単になります。また、このパターンは、サービスを通じて抽象化することで、プラットフォーム固有の機能をバックエンドのコードで実装できるようにすることもできます。

MVVM Toolkit は、このパターンを使うことを簡単にするような仕組みの API を提供していません。Microsoft.Extensions.DependencyInjection パッケージのような、専用ライブラリがすでに存在するからです。

このライブラリは、完全な機能を備えた強力な DI の API を提供し、簡単にセットアップ/使用できる IServiceProvider として機能します。以下のガイドでは、ライブラリを参照して MVVM パターンを使ってアプリケーションにまとめる例を示します。

APIs:

サービスの構成と解決 (Configure and resolve services)

最初のステップは IServiceProvider を宣言します。(通常はスタートアップのタイミングで)必要なすべてのサービスを初期化します。例えば、UWP は次のとおり(他のフレームワークでも同じようにセットアップできる):

public sealed partial class App : Application
{
    public App()
    {
        Services = ConfigureServices();

        this.InitializeComponent();
    }

    /// <summary>
    /// 現在使用している <see cref="App"/> インスタンスを取得します。
    /// </summary>
    public new static App Current => (App)Application.Current;

    /// <summary>
    /// アプリケーションサービスを解決するための <see cref="IServiceProvider"/> インスタンスを取得します。
    /// </summary>
    public IServiceProvider Services { get; }

    /// <summary>
    /// アプリケーションのサービスを設定します。
    /// </summary>
    private static IServiceProvider ConfigureServices()
    {
        var services = new ServiceCollection();

        services.AddSingleton<IFilesService, FilesService>();
        services.AddSingleton<ISettingsService, SettingsService>();
        services.AddSingleton<IClipboardService, ClipboardService>();
        services.AddSingleton<IShareService, ShareService>();
        services.AddSingleton<IEmailService, EmailService>();

        return services.BuildServiceProvider();
    }
}

この例では、アプリケーションの起動時に Services プロパティが初期化されます。すべてのアプリケーションのサービスと viewmodel が登録されます。また、アプリケーション内の他の view から Services プロパティに簡単にアクセスできるようにするための Current プロパティもまた用意してあります。たとえば:

IFilesService filesService = App.Current.Services.GetService<IFilesService>();

// Use the files service here...

ここで重要なことは、各サービスがプラットフォーム固有の API を使っている可能性が非常に高いこと、という点です。

しかし、私たちのコードで使っているインターフェースによって、それらはすべて抽象化しています。プラットフォーム固有の具体的な操作は、(抽象化された)インスタンスの操作を実行するだけなので、気にする必要はありません。

コンストラクタの注入 (Constructor injection)

強力な機能として "constructor injection" があります。これは DI プロバイダーがリクエストした型のインスタンス生成を自動的に解決できること(登録されたサービス間の間接的な依存関係において)を意味しています。次のサービスの例を考えてみます:

public class FileLogger : IFileLogger
{
    private readonly IFilesService FileService;
    private readonly IConsoleService ConsoleService;

    public FileLogger(
        IFilesService fileService,
        IConsoleService consoleService)
    {
        FileService = fileService;
        ConsoleService = consoleService;
    }

    // Methods for the IFileLogger interface here...
}

このコードの例は IFileLogger インターフェースを実装した FileLogger です。これは IFilesServiceIConsoleService を必要としています。

"constructor injection" は DI サービスプロバイダーが自動的に(そのコードの部分で必要とする)サービスを集めてくる操作のことを意味しています:

/// <summary>
/// アプリケーションのサービスを設定します。
/// </summary>
private static IServiceProvider ConfigureServices()
{
    var services = new ServiceCollection();

    services.AddSingleton<IFilesService, FilesService>();
    services.AddSingleton<IConsoleService, ConsoleService>();
    services.AddSingleton<IFileLogger, FileLogger>();

    return services.BuildServiceProvider();
}

// constructor injection を使ってロガーサービスを取得する
IFileLogger fileLogger = App.Current.Services.GetService<IFileLogger>();

DI サービスプロバイダは必要なサービスがすべて登録されているかどうかを自動的にチェックします。そして登録されている IFileLogger の実装された型のコンストラクタを呼び出して、返却するインスタンスを渡します。

Viewmodel は "constructor injection" て、どうなの? (What about viewmodels?)

サービスプロバイダは名前に "service" とついていますが、実際はどんなクラスのインスタンスでも解決することができるので viewmodel も含まれます!

"constructor injection" を含めて、ここまでで説明してきたことと同じコンセプトが適用できます。

ContactsViewModel 型があったとして、IContactsService および IPhoneServiceインスタンスをコンストラクタで使用する、とします。この場合は、次のような ConfigureServices メソッドを持つことが可能です:

/// <summary>
/// Configures the services for the application.
/// </summary>
private static IServiceProvider ConfigureServices()
{
    var services = new ServiceCollection();

    // Services
    services.AddSingleton<IContactsService, ContactsService>();
    services.AddSingleton<IPhoneService, PhoneService>();

    // Viewmodels
    services.AddTransient<ContactsViewModel>();

    return services.BuildServiceProvider();
}

ContactsView で以下のように データコンテキストを割り当てします:

public ContactsView()
{
    this.InitializeComponent();
    this.DataContext = App.Current.Services.GetService<ContactsViewModel>();
}

「What about viewmodels?」はサンプルプログラムには存在しません。「MS Learn」にありました。

参考

レガシーコードとどう付き合うか

レガシーコードとどう付き合うか

Amazon

C# CommunityToolkit.Mvvm の学習7 Messenger

Messenger

IMessenger インターフェースは、異なるオブジェクト間でメッセージを交換するために使用できる型の契約です。これは、参照される型への強参照を保持することなく、アプリケーションの異なるモジュールとモジュールを分離するために便利です。また、トークンで一意に識別される特定のチャンネルにメッセージを送信したり、アプリケーションの異なるセクションで異なる Messenger を持つことも可能です。

MVVM Toolkit は2つの実装を提供します。WeakReferenceMessengerStrongReferenceMessenger です。

WeakReferenceMessenger は、内部的に弱参照を使用して、受信者 (recipient) に自動的なメモリ管理を提供しています。StrongReferenceMessenger は、強参照を使用して、不要になったときに開発者がマニュアルで受信者の登録を解除する必要があります。(登録の解除方法についての詳細は後述)しかし、その代わりにパフォーマンスが(両者の比較として)向上して、メモリ使用量がずっと少なくなります。

APIs:

  • Messenger
  • WeakReferenceMessenger
  • StrongReferenceMessenger
  • IRecipient
  • MessageHandler<TRecipient, TMessage>
  • ObservableRecipient
  • RequestMessage
  • AsyncRequestMessage
  • CollectionRequestMessage
  • AsyncCollectionRequestMessage

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

IMessenger を実装した型は、受信者(メッセージの受信者)とその登録をしたメッセージの型との間のリンクを、相対的なメッセージハンドラを使って維持しています。

どのオブジェクトでも、メッセージハンドラを使用して、与えられたメッセージの型の受信者として登録することができます。IMessenger インスタンスが、(なにかの型の)メッセージを送信するために使用する毎に呼び出されます。

また、複数のモジュールが衝突を起こすことなく同じ型のメッセージを交換できるように、特定の通信チャンネル(それぞれ一意のトークンによって式ベルされるもの)を介してメッセージを送信することも可能です。トークンなしで送信されたメッセージは、デフォルトの共用チャンネルを使用しています。

Messenger は IRecipient<TMessage> インターフェースを使う方法と MessageHandler<TRecipient, TMessage> デリゲートをメッセージハンドラとして使う方法があります。前者は RegisterAll エクステンションを1回呼び出すだけですべてのハンドラを登録することができます。宣言されたすべてのメッセージハンドラの受信者が自動的に登録されます。後者は、より柔軟性が必要な場合、または、単純なラムダ式をメッセージハンドラとして使用する場合に役立ちます。

WeakReferenceMessengerStrongReferenceMessenger は、パッケージに組み込まれたスレッドセーフな実装の Default プロパティも公開しています。必要であれば、複数の Messenger インスタンスも作成することも可能です。例えば、アプリケーションの異なるモジュール(同じプロセスで実行されている複数のウィンドウ)に DI サービスプロバイダで異なる Messenger を挿入する場合などです。

Note: WeakReferenceMessenger は使いやすく、また MvvmLight ライブラリの Messenger の同じ動作をするため、MVVM Toolkit の ObservableRecipient 型でも使用されるデフォルトの型になります。

送信 - 送信と受信メッセージ (Sending messages - Sending and receiving messages)

例を挙げます:

// Create a message
public class LoggedInUserChangedMessage : ValueChangedMessage<User>
{
    public LoggedInUserChangedMessage(User user) : base(user)
    {        
    }
}

// Register a message in some module
WeakReferenceMessenger.Default.Register<LoggedInUserChangedMessage>(this, (r, m) =>
{
    // ここでメッセージを処理します。引数の r は受信者で m は入力メッセージです。
    // 入力として渡された受信者を使用することで、ラムダ式は "this" をキャプチャしないので、パフォーマンスが向上します。
});

// Send a message from some other module
WeakReferenceMessenger.Default.Send(new LoggedInUserChangedMessage(user));

このメッセージタイプが単純な Messaging アプリケーションで使用されて、現在のログインしているユーザーのユーザー名とプロフィール画像が表示されているヘッダー、会話リストが表示されているパネル、そして、現在の会話からメッセージが選択されている場合は、別のパネルが表示されている例を想像してみてください。

ここでは3つの viewmodel のケースとして、それぞれ HeaderViewModelConversationsListViewModelConversationViewModel によって(例を)実装されているとします。

このシナリオの例では、ログイン操作が完了した後、HeaderViewModel から LoggedInUserChangedMessage メッセージが送信されます。他の viewmodel は、それに対してハンドラを登録するとよいでしょう。例えば、ConversationsListViewModel は新しいユーザーの会話のリストを読み込み、ConversationViewModel は現在の会話が存在する場合は、それを閉じます。

IMessenger インスタンスは、登録されたすべての受信者にメッセージを配信します。受信者は特定の型のメッセージを購読できるようになっている点に注意してください。MVVM Toolkit によって提供されるデフォルトの IMessenger の実装できは、継承されたメッセージの型は登録されません。

受信者はメッセージの受信が不要になったとき、登録を解除してメッセージの受信を停止します。登録解除の方法は、メッセージの型、登録トークン、受信者ごとに解除できます:

// Unregisters the recipient from a message type
// メッセージの型から解除
WeakReferenceMessenger.Default.Unregister<LoggedInUserChangedMessage>(this);

// Unregisters the recipient from a message type in a specified channel
// 指定したチャンネルのメッセージ型から解除
WeakReferenceMessenger.Default.Unregister<LoggedInUserChangedMessage, int>(this, 42);

// Unregister the recipient from all messages, across all channels
// すべてのチャンネル/メッセージから解除
WeakReferenceMessenger.Default.UnregisterAll(this);

Warning: 前述したように、WeakReferenceMessenger 型を使用する場合、受信者を追跡するために弱参照を使用するので、厳密には(登録解除の)必要はありません。(とはいえ)パフォーマンスを向上させるために、受信者の登録を解除することは良い習慣です。
一方で、StrongReferenceMessenger 型の実装では、登録された受信者を追跡するために強参照を使用しています。これはパフォーマンス上の理由から、メモリーリークを避けるために、登録された受信者はそれぞれマニュアルで登録解除する必要があります。受信者が登録されている限り、使用中の StrongReferenceMessenger インスタンスはアクティブな参照を保持し続けて、GCインスタンスを回収できないようにしています。
この処理の設定はマニュアルで行うか ObservableRecipient を継承して行うことができます。ObservableRecipient はデフォルトで受信者が非アクティブになったときには、自動的にすべてのメッセージ登録を削除します。(これについては ObservableRecipient の「ドキュメント」を参照してください)

IRecipient<TMessage> インターフェースを使ってメッセージハンドラを登録することもできます。この場合、各受信者は与えられたメッセージの型に対応するインターフェースを実装すること、メッセージを受信するときに呼び出される Receive(TMessage) メソッドを実装する必要があります。

// Create a message
public class MyRecipient : IRecipient<LoggedInUserChangedMessage>
{
    public void Receive(LoggedInUserChangedMessage message)
    {
        // ここでメッセージを処理
    }
}

// 特定メッセージの登録の例
WeakReferenceMessenger.Default.Register<LoggedInUserChangedMessage>(this);

// 宣言されたハンドラをすべて登録する例
WeakReferenceMessenger.Default.RegisterAll(this);

// 他のモジュールからメッセージを送信する例
WeakReferenceMessenger.Default.Send(new LoggedInUserChangedMessage(user));

Sample

public UserSenderViewModel SenderViewModel { get; } = new UserSenderViewModel();

public UserReceiverViewModel ReceiverViewModel { get; } = new UserReceiverViewModel();

// Simple viewmodel for a module sending a username message
public class UserSenderViewModel : ObservableRecipient
{
    private string username = "Bob";

    public string Username
    {
        get => username;
        private set => SetProperty(ref username, value);
    }

    public void SendUserMessage()
    {
        Username = Username == "Bob" ? "Alice" : "Bob";

        Messenger.Send(new UsernameChangedMessage(Username));
    }
}

// Simple viewmodel for a module receiving a username message
public class UserReceiverViewModel : ObservableRecipient
{
    private string username = "";

    public string Username
    {
        get => username;
        private set => SetProperty(ref username, value);
    }

    protected override void OnActivated()
    {
        Messenger.Register<UserReceiverViewModel, UsernameChangedMessage>(this, (r, m) => r.Username = m.Value);
    }
}

// A sample message with a username value
public sealed class UsernameChangedMessage : ValueChangedMessage<string>
{
    public UsernameChangedMessage(string value) : base(value)
    {
    }
}
<StackPanel Spacing="8">

    <!--Sender module-->
    <Border BorderBrush="{ThemeResource SystemChromeBlackLowColor}" BorderThickness="1" CornerRadius="4" Padding="8">
        <StackPanel Spacing="8">
            <TextBlock Text="{x:Bind ViewModel.SenderViewModel.Username, Mode=OneWay}"/>
            <Button
                Content="Click to send a message!"
                Click="{x:Bind ViewModel.SenderViewModel.SendUserMessage}"/>
        </StackPanel>
    </Border>

    <!--Receiver module-->
    <Border BorderBrush="{ThemeResource SystemChromeBlackLowColor}" BorderThickness="1" CornerRadius="4" Padding="8">
        <StackPanel Spacing="8">
            <TextBlock Text="{x:Bind ViewModel.ReceiverViewModel.Username, Mode=OneWay}"/>
        </StackPanel>
    </Border>
</StackPanel>

リクエストメッセージの使用 (Request messages - Using request messages)

Messenger インスタンスのもうひとつの便利な機能は、あるモジュールから別のモジュールに値をリクエストするのにも使えることです。そのため、パッケージには RequestMessage<T> クラスが含まれています:

// Create a message
public class LoggedInUserRequestMessage : RequestMessage<User>
{
}

// Register the receiver in a module
WeakReferenceMessenger.Default.Register<MyViewModel, LoggedInUserRequestMessage>(this, (r, m) =>
{
    // "CurrentUser" は viewmodel の private メンバーであると仮定します。
    // デリゲート内で "this" をキャプチャするのを避けること。(パフォーマンスが向上)
    m.Reply(r.CurrentUser);
});

// Request the value from another module
User user = WeakReferenceMessenger.Default.Send<LoggedInUserRequestMessage>();

RequestMessage<T> クラスは LoggedInUserRequestMessage が含まれている User オブジェクトへの変換を可能にする暗黙のコンバーターを含んでいます。また、このコンバーターはメッセージに対するレスポンスが受信されているかどうかをチェックして、レスポンスが受信されていない場合は例外を throw します。

レスポンスの保証なしでリクエスト・メッセージを送信することも可能です。返却されたメッセージをローカル変数に格納して、レスポンスの値が利用可能かどうかをマニュアルで確認するだけです。そうすることで Send メソッドが返却されたときにレスポンスを受信していなくても、自動的に例外は発生しません。

(上述クラスの定義のある)同じ名前空間には、他のケース用の基本的なリクエスト・メッセージもあります。

  • AsyncRequestMessage
  • CollectionRequestMessage
  • AsyncCollectionRequestMessage

ここでは、非同期リクエスト・メッセージの使い方を説明します:

// Create a message
public class LoggedInUserRequestMessage : AsyncRequestMessage<User>
{
}

// Register the receiver in a module
WeakReferenceMessenger.Default.Register<MyViewModel, LoggedInUserRequestMessage>(this, (r, m) =>
{
    // Task<User> を返信
    m.Reply(r.GetCurrentUserAsync());
});

// 別のモジュールから値をリクエストする
// リクエストに対して await を直接することができる
User user = await WeakReferenceMessenger.Default.Send<LoggedInUserRequestMessage>();

Sample

// ユーザー名のメッセージのリクエストの応答モジュールのためのシンプルな viewmodel
// viewmodel が使用されているときは IsActive を true にすること!
public class UserSenderViewModel : ObservableRecipient
{
    public string Username { get; private set; } = "Bob";

    protected override void OnActivated()
    {
        Messenger.Register<UserSenderViewModel, CurrentUsernameRequestMessage>(this, (r, m) => m.Reply(r.Username));
    }
}

private string username;

public string Username
{
    get => username;
    private set => SetProperty(ref username, value);
}

// 現在のユーザー名を要求するメッセージを送信して、プロパティを更新
public void RequestCurrentUsername()
{
    Username = WeakReferenceMessenger.Default.Send<CurrentUsernameRequestMessage>();
}

// Resets the current username
public void ResetCurrentUsername()
{
    Username = null;
}

// 現在のユーザー名を取得するためのリクエストメッセージ
public sealed class CurrentUsernameRequestMessage : RequestMessage<string>
{
}
<StackPanel Spacing="8">
    <TextBlock Text="{x:Bind ViewModel.Username, Mode=OneWay}"/>
    <Button
        Content="Click to request the username!"
        Click="{x:Bind ViewModel.RequestCurrentUsername}"/>
    <Button
        Content="Click to reset the local username!"
        Click="{x:Bind ViewModel.ResetCurrentUsername}"/>
</StackPanel>

参考

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>

参考

C# CommunityToolkit.Mvvm の学習5 ObservableValidator

ObservableValidator

ObservableValidator は INotifyDataErrorInfo インターフェースを実装した base クラスで、他のアプリケーションモジュールに公開されたプロパティの検証をサポートします。

ObservableObject も継承しています。INotifyPropertyChanged も INotifyPropertyChanging も実装されています。プロパティ変更通知とプロパティ検証の両方をサポートする必要がある、あらゆる種類のオブジェクトの起点として使用できます。

APIs:

  • ObservableValidator
  • ObservableObject

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

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

  • INotifyDataErrorInfo の基本実装を提供して、ErrorsChanged イベントとその他の必要な API を公開します。
  • (ベースとなる ObservableObject クラスによって提供されるものの上に)SetProperty の overload を提供して、プロパティを自動的に検証し、値を更新する前に必要なイベントを発生させる機能を提供します。
  • TrySetProperty の overload は SetProperty に似ています。しかし、バリデーションが成功した場合のみ、プロパティを更新し、さらに検査するために(もし発生していたら)生成されたエラーを返す機能を備えています。
  • ValidateProperty メソッドを公開しています。これは特定のプロパティの値が更新されていないにも関わらず、そのバリデーションは(そのプロパティの)代わりに更新された別のプロパティの値に依存して、マニュアルで更新をトリガーしたいときに便利です。
  • ValidateAllProperties メソッドを公開しています。このメソッドは、現在のインスタンスに存在するすべての public プロパティ(少なくとも 1つの [ValidationAttribute] が適用されている)の検証を自動的に実行します。
  • ユーザーが再入力するようなフォームに binding されたモデルをリセットするときのために ClearAllErrors メソッドを公開しています。
  • プロパティのバリデーションに仕様する ValidationContext を初期化するために、さまざまなパラメーターを渡すことができます。これは、正しく動作させるために追加のサービス/オプション を必要とするような、カスタム バリデーションを使用したい場合に有用です。

単純な例

以下は、変更通知と検証の両方に対応するプロパティを実装する方法の例です:

public class RegistrationForm : ObservableValidator
{
    private string name;

    [Required]
    [MinLength(2)]
    [MaxLength(100)]
    public string Name
    {
        get => name;
        set => SetProperty(ref name, value, true);
    }
}

ObservableValidator クラスによって公開されている SetProperty メソッドを呼び出しています。(従来の SetProprty から)追加されている true に設定されている bool 値のパラメーターは、値が更新されたときにプロパティも検証するかどうかを示す値です。

ObservableValidator は、プロパティに適用されたすべてのチェック項目(の属性)を、新しい値に対して自動的に検証を実行します。ErrorsChanged を登録すれば GetErrors(string) メソッドを使用して、変更された各プロパティのエラーのリストを取得できます。

Sample

public partial class ValidationFormWidgetViewModel : ObservableValidator
{
    private readonly IDialogService DialogService;

    public ValidationFormWidgetViewModel(IDialogService dialogService)
    {
        DialogService = dialogService;
    }

    public event EventHandler? FormSubmissionCompleted;
    public event EventHandler? FormSubmissionFailed;

    [ObservableProperty]
    [Required]
    [MinLength(2)]
    [MaxLength(100)]
    private string? firstName;

    [ObservableProperty]
    [Required]
    [MinLength(2)]
    [MaxLength(100)]
    private string? lastName;

    [ObservableProperty]
    [Required]
    [EmailAddress]
    private string? email;

    [ObservableProperty]
    [Required]
    [Phone]
    private string? phoneNumber;

    [RelayCommand]
    private void Submit()
    {
        ValidateAllProperties();

        if (HasErrors)
        {
            FormSubmissionFailed?.Invoke(this, EventArgs.Empty);
        }
        else
        {
            FormSubmissionCompleted?.Invoke(this, EventArgs.Empty);
        }
    }

    [RelayCommand]
    private void ShowErrors()
    {
        string message = string.Join(Environment.NewLine, GetErrors().Select(e => e.ErrorMessage));

        _ = DialogService.ShowMessageDialogAsync("Validation errors", message);
    }
}
<StackPanel Spacing="16">

    <!--  Text forms  -->
    <controls:ValidationTextBox
        HeaderText="Enter your first:"
        PlaceholderText="First name"
        PropertyName="FirstName"
        Text="{x:Bind ViewModel.FirstName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
    <controls:ValidationTextBox
        HeaderText="Enter your last name:"
        PlaceholderText="Last name"
        PropertyName="LastName"
        Text="{x:Bind ViewModel.LastName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
    <controls:ValidationTextBox
        HeaderText="Enter your email address:"
        PlaceholderText="Email"
        PropertyName="Email"
        Text="{x:Bind ViewModel.Email, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
    <controls:ValidationTextBox
        HeaderText="Enter your phone number:"
        PlaceholderText="Phone number"
        PropertyName="PhoneNumber"
        Text="{x:Bind ViewModel.PhoneNumber, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />

    <!--  Submit command  -->
    <Button Command="{x:Bind ViewModel.SubmitCommand}" Content="Submit" />

    <!--  Popups  -->
    <Grid>
        <muxc:InfoBar
            x:Name="SuccessInfoBar"
            Title="Success"
            Message="The form was filled in correctly."
            Severity="Success">
            <interactivity:Interaction.Behaviors>
                <interactions:EventTriggerBehavior EventName="FormSubmissionCompleted" SourceObject="{x:Bind ViewModel}">
                    <interactions:ChangePropertyAction
                        PropertyName="IsOpen"
                        TargetObject="{x:Bind SuccessInfoBar}"
                        Value="True" />
                    <interactions:ChangePropertyAction
                        PropertyName="IsOpen"
                        TargetObject="{x:Bind FailureInfoBar}"
                        Value="False" />
                </interactions:EventTriggerBehavior>
            </interactivity:Interaction.Behaviors>
        </muxc:InfoBar>
        <muxc:InfoBar
            x:Name="FailureInfoBar"
            Title="Error"
            Message="The form was filled in with some errors."
            Severity="Error">
            <muxc:InfoBar.ActionButton>
                <Button Command="{x:Bind ViewModel.ShowErrorsCommand}" Content="Show errors" />
            </muxc:InfoBar.ActionButton>
            <interactivity:Interaction.Behaviors>
                <interactions:EventTriggerBehavior EventName="FormSubmissionFailed" SourceObject="{x:Bind ViewModel}">
                    <interactions:ChangePropertyAction
                        PropertyName="IsOpen"
                        TargetObject="{x:Bind SuccessInfoBar}"
                        Value="False" />
                    <interactions:ChangePropertyAction
                        PropertyName="IsOpen"
                        TargetObject="{x:Bind FailureInfoBar}"
                        Value="True" />
                </interactions:EventTriggerBehavior>
            </interactivity:Interaction.Behaviors>
        </muxc:InfoBar>
    </Grid>
</StackPanel>

カスタム検証のメソッド (Custom validation methods)

プロパティを検証する際に、viewmodel が追加のサービス、データ、その他 API にアクセスする場合があります。シナリオと必要とされる柔軟性のレベルに応じて、プロパティにカスタムバリデーションを追加する様々な方法があります。

ここでは CustomValidationAttribute 型を使用して、プロパティの追加検証を実行するために特定のメソッドを呼び出す方法の例を示します:

public class RegistrationForm : ObservableValidator
{
    private readonly IFancyService service;

    public RegistrationForm(IFancyService service)
    {
        this.service = service;
    }

    private string name;

    [Required]
    [MinLength(2)]
    [MaxLength(100)]
    [CustomValidation(typeof(RegistrationForm), nameof(ValidateName))]
    public string Name
    {
        get => this.name;
        set => SetProperty(ref this.name, value, true);
    }

    public static ValidationResult ValidateName(string name, ValidationContext context)
    {
        RegistrationForm instance = (RegistrationForm)context.ObjectInstance;
        bool isValid = instance.service.Validate(name);

        if (isValid)
        {
            return ValidationResult.Success;
        }

        return new("The name was not validated by the fancy service");
    }
}

この例の場合、static な ValidateName メソッドを作成して viewmodel に挿入されたサービスを通じて Name プロパティの検証を行います。

このメソッドは(引数で)Name プロパティの値と ValidationContext インスタンスを受け取ります。この ValidationContext には、viewmodel のインスタンス、検証対象のプロパティの名前、オプションで使用中 or 設定可能なサービスプロバイダ、カスタムフラグなどが含まれます。

この例の場合、RegistrationForm インスタンスを(引数の)ValidationContext から取得して、そこから必要なサービスを使用してプロパティを検証しています。このカスタム検証は、他の属性で指定された検証の後に実行される点に注意してください。

なので、カスタム検証メソッドとと既存の検証の属性を自由に組み合わせることが可能です。

カスタム検証の属性 (Custom validation attributes)

カスタム検証をするもうひとつの方法は、カスタムした ValidationAttribute を実装することです。(この実装は)override した IsValid メソッドに検証のロジックを挿入することです。

この方法では、同じ属性を複数の(コード上の)場所で再利用することがとても簡単になるので、上記のアプローチと比べて柔軟性が増します。例として、同じ viewmodel 内の別のプロパティに対してプロパティの検証をしてみることにします。最初のステップは、カスタムした GreaterThanAttribute を定義することです。

public sealed class GreaterThanAttribute : ValidationAttribute
{
    public GreaterThanAttribute(string propertyName)
    {
        PropertyName = propertyName;
    }

    public string PropertyName { get; }

    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        object
            instance = validationContext.ObjectInstance,
            otherValue = instance.GetType().GetProperty(PropertyName).GetValue(instance);

        if (((IComparable)value).CompareTo(otherValue) > 0)
        {
            return ValidationResult.Success;
        }

        return new("The current value is smaller than the other one");
    }
}

つぎは、作成したカスタム属性を viewmodel に追加します:

public class ComparableModel : ObservableValidator
{
    private int a;

    [Range(10, 100)]
    [GreaterThan(nameof(B))]
    public int A
    {
        get => this.a;
        set => SetProperty(ref this.a, value, true);
    }

    private int b;

    [Range(20, 80)]
    public int B
    {
        get => this.b;
        set
        {
            SetProperty(ref this.b, value, true);
            ValidateProperty(A, nameof(A));
        }
    }
}

この例の場合だと、(比較をする)2つの値は、数値としての特性があり、お互いに特定の関係になければならない。(AはBより大きくなければならない)A プロパティに GreaterThanAttribute を追加して、 B の Setter に ValidateProperty メソッドの呼び出しを追加しています。

再利用可能なカスタム検証の属性を持つことで、アプリケーションの他の viewmodel でも役に立つというメリットを得ることができます。検証ロジックは viewmodel の定義自体から完全に切り離されるため、このアプローチはコードのモジュール化としても役に立ちます。

参考