Enum と ListBox は使いよう【step: 10 .NET 5 WPF MVVM 入門 2020】

前回記事「ReactiveCollection 世代の ListBox 達【step: 9 .NET 5 WPF MVVM ReactiveCollection 入門 2020】」

 

前回は ListBox と ReactiveCollection を MVVM パターンでバインドする方法を紹介しましたが、予想以上に長くなってしまい、書く予定だった事が全て書けなかったので、今回は前回 step: 9 の続きとして再度 ListBox(ItemsControl を継承したコントロール)に表示するデータを Model 以外から供給する方法を紹介します。

尚、この記事は Visual Studio 2019 Community Edition で .NET5 以降 と C# + Prism + ReactiveProperty + MahApps.Metro + Material Design In XAML Toolkit を使用して、WPF アプリケーションを MVVM パターンで作成するのが目的なので、C# の文法や基本的なコーディング知識を持っている人が対象です。

Model 以外から表示データを供給

リスト系コントロールに表示するデータは管理の手間や修正時の変更しやすさから DB 等の永続化層から取得することが多いので、前回は Model で取得した永続化データを表示する例を紹介しましたが、リスト系コントロールの中でも ListBox や ComboBox 等は永続化されていないデータを表示したい場合もあると思います。

そこで、今回は Model 以外からリスト系コントロールへデータを供給する方法を紹介します。尚、本エントリは元々 step: 9 へ書く予定の内容だったので、step: 9 を読んで頂いている前提で書いています。

そのため、まだ読んで頂けていない場合は、先に step: 9 を読んで頂けると書いている内容が理解しやすいと思います。

View からデータを供給

先ず最初に、最近はスマホ等で見かける事も多い、fig. 1 のように数値をユーザに入力させるのではなく、選択させるような UI を作成するとします。

fig.1 XAML に直接データを記述した画面

一般的には、ListBox ではなく ComboBox(DropDownListBox)で作成する事が多いと思いますが、step: 9 でも紹介した通り、ListBox や ComboBox は ItemsControl を継承しているので同じ方法が使えます

View からデータを供給する方法の内、最も原始的なのは src. 1 のように XAML へ選択肢をベタ書きする方法です。

初心者向けの入門サイト等で見かける事も多い方法ですが、汎用性が低く採用すべきではありません。使う機会もほとんど無いはずなので、覚える必要もありません。使う機会がほとんど無いにもかかわらず、初心者向けに最初に紹介するのはいかがなものかと管理人個人的には思うので、初心者の人は即忘れて OK です。

サンプルアプリで起動時に表示する画面を切り替える

次章に入る前にサンプルアプリを動作させる場合の注意点を書いておきます。以降で紹介するサンプルは全て同一の exe ですが、src. 2 の MainWindowViewModel コンストラクタ内の処理をコメントで切り替えることで起動時に表示する View を切り替えています

表示する View を変更したい場合は、src. 2 の 19 ~ 24 行目のいずれかを有効にすれば typeof で指定している View が起動時に表示されます。

次章の場合は、22行目を有効にすると対象の View が表示されます。

ViewModel からデータを供給する

前章では比較のために XAML へ項目をベタ書きする方法を紹介しましたが、使用機会が少ない方法です。本来は src. 3 のように VM 側でデータを生成すべきだと思います。

実行すると fig. 2 のようになり、前章で紹介した画面とほとんど変わらない表示になります。

fig.2 VM で生成した連番をバインド

※ fig. 2 を表示するには MainWindowViewModel のコンストラクタで【SequencePage】の行を有効にします。

step: 9 で紹介した方法とほとんど変わりませんが、項目用の VM を作成していないと言う違いがあります。Model のデータをバインドする場合、項目用の VM 作成は必須だと思いますが、src. 3 のように VM でデータを生成していて、単一項目のみを表示するような場合は、項目用の VM まで作成する必要は無いとは思っています。

ですが、step: 9 で紹介した通り、複数選択可能な ItemsControl から選択項目を取得したい場合は、項目用 VM の作成は必須になることは忘れないでください。

但し、src. 3 のように連番の値であれば何でも VM で生成して良いと書いている訳ではありません。見た目上は連番であっても最小値や最大値を永続化層(DB 等)から取得したり、DateTime.Now から表示する値を算出する… 等ビジネスロジック的な算出仕様が存在する場合は、例え連番値であっても Model で生成すべきだと思いますし、正にそれが MVVM パターンの目的だとも言えます。

VM で生成して良いのは仕様に縛られない単純なデータのみであることは忘れないでください。

Enum 型をリスト系コントロールへ表示する

前章では自前で生成した単純な連番をリスト系コントロールへ表示する方法を紹介したので、本章では Enum 型をリスト系コントロールへ表示する方法を紹介します。

以降で紹介する内容は、WPF UI Gallery case: 1-1 MahApps.Metro の HamburgerMenu から Prism の RequestNavigate で画面を切り替えるで紹介した内容と全く同じです。

WPF UI Gallery は元々ほとんど構想を練らずノリで始めてしまった連載なので、本筋とは関係ない内容まで混ざってしまっていましたが、WPF UI Gallery のリライトに備えて本来あるべきエントリに移動します。

そのため、読んだことがある人もいるかもしれませんがご了承ください。

そして以降で紹介する内容は、WPF UI Gallery case: 1-1 でも書きましたが、Prime のコントリビュータとしてもお馴染みの、Brian Lagunas さんのブログ記事『A Better Way to Data Bind Enums in WPF – BRIAN LAGUNAS』を丸パクリ 非常に参考にさせていただいています。

上の記事は ComboBox を例に書かれていますが、step: 9 でも紹介した通り、ListBox も同じ ItemsControl を継承したコントロールなので、ここでは ListBox を例に紹介しています。

大きく分類すれば、ここで紹介する方法も View からデータを供給する方法に分類されますが、別枠として紹介します。

ObjectDataProvider で Enum 型をバインドする

Brian Lagunas さんの『A Better Way to Data Bind Enums in WPF – BRIAN LAGUNAS』でも紹介されている通り、最初に ObjectDataProvider を仲介する方法を紹介しますが、Brian Lagunas さんが記事を書かれた 2015 年 2 月時点から Framework 自体が変わっている(.NET Framework → .NET Core → .NET5)ので記事に書かれている通りの方法ではエラーになりました。

src. 4 は Brian Lagunas さんの記事で紹介されているサンプルです。

src. 4 をそのまま書こうとしても、3 行目の『x:Type sys:Enum』が入力できず、若干苦労しました。.NET5 や .NET Core 3.1 以降では、多少記述が変わったようです。

そして、ここからは Brian Lagunas さんの記事に合わせて、ObjectDataProvider を利用して Enum 型を ItemsControl へ表示する方法を紹介します。(.NET5 版)まずは、src. 5 のような Enum(列挙)型を用意します。

src. 5 を見て『日本語の型とか気持ち悪い!』と感じる人が多いのも分かりますし、管理人も元々『気持ち悪い!』派でしたが、列挙型に関してだけは『アリかも…』に変わってきています。業務系のシステム開発では業界特有や、その顧客でしか使用しない用語も多く、翻訳するには厳しい場面に多く出会いますが、用語をそのまま列挙子として定義すれば読みやすく、間違いにくいのも確かなので、最近では『アリかも…』と思っています。

とは言え、src. 5 の『所属組織 Enum 型』を取得するメソッド名を『Get所属組織』なんて名前にするのは反対派でもありますが…(最低でも GetShozokuSosiki ならアリかも)ソースコード上で一目見れば何を返しているのか分かるのはやはり強いと感じています。ただ、ここでは日本語をインタフェース名に使用する是非を論じたい訳ではないので、src. 5 の列挙型については我慢して読んでください。

src. 5 の列挙型を ObjectDataProvider で表示するには src. 6 のように指定します。

src. 6 のように ObjectDataProvider を使用すると、クラスのメソッドを呼び出してその戻り値をバインドできます。7 行目で呼び出している GetValues はクラスメソッドなので、本来は src. 4 のように ObjectType へ Enum を指定すべきだと思いますが、現状の .NET5 や .NET Core では src. 6 のように実際に表示したいネイティブの型しか指定できなくなったようです

このエントリのサンプルコードは全て .NET5 で書いているので、.NET Core では試していませんが、Stack Overflow の『mscorlib in .net core is missing in v3.1』 では .NET Core 3.1 をターゲットした場合の方法としてリプが付いているので、.NET Core でも同じ記述ができるはずです。

そして、src. 6 の ObjectDataProvider を XAML に書くだけで XAML のプレビューにも表示されますが、実行すると fig. 3 のようになります。

fig.3 ObjectDataProvider でバインド

※ fig. 3 を表示するには MainWindowViewModel のコンストラクタで【EnumBindingPage】の行を有効にします。

fig. 3 画面最下部の選択値は XAML 上で直接バインドしていますが、VM へ【所属組織 列挙型】のプロパティを追加して ListBox.SelectedValue 等をバインドすれば値の取得・設定もできますし、step: 7 で紹介した Model との双方向バインディングも可能です。(※ サンプルコード内では Debug.WriteLine しています)

このように ObjectDataProvider を使用するのも悪くありませんし、問題なく動作しますが、画面に 2 つ以上の ListBox(ComboBox)があって、それぞれが別々の列挙型を表示する場合を考えると、表示する列挙型の数だけ Resources を追加する必要があって記述量も増えるので、もう少し簡易的に書く方法があります!(Brian Lagunas さん談)

『Brian Lagunas さん談』と書きつつもかなり管理人の意見が入っていますが『A Better Way to Data Bind Enums in WPF – BRIAN LAGUNAS』にも上に近い内容が書かれているので、引き続き丸パクリ 参考にしてより良い方法も紹介します。

Enum を一覧表示するためのマークアップ拡張

A Better Way to Data Bind Enums in WPF – BRIAN LAGUNAS』では ObjectDataProvider を使用する方法とは別に、マークアップ拡張を利用して列挙型をリスト化するロジックを全てカプセル化する方法も紹介されています。

Enum 表示用のマークアップ拡張を紹介する前に、まずはマークアップ拡張について簡単に紹介します。

マークアップ拡張とは

XAML は元々 XML がベースの言語なので、複雑な構造のオブジェクトでもタグをいくつもネストすれば表現できますが、記述量が増大すると言う問題点もあります。その上、それがよく使うオブジェクトの場合、同じような記述を何度も繰り返す必要があるので、想像しただけでもうんざりします。そこで XAML ではマークアップ拡張と言う仕組みを導入して、本来なら大量のタグを書かなければならない所を簡潔に記述できるようにしています。

マークアップ拡張は『{ }』で囲んだ中に記述するよう定義されていて、この連載でもよく紹介する【{Binding …}】や【{StaticResource …}】等もマークアップ拡張です。マークアップ拡張の実体は System.Windows.Markup.MarkupExtension を継承したクラスなので、自作する事もできます。

Brian Lagunas さんはこのマークアップ拡張を利用して Enum 型をリスト表示する方法を紹介しています。

Enum 型をリスト表示するマークアップ拡張

以降で紹介するマークアップ拡張は他のプロジェクトにも使い回せるので、使い回ししやすいように新規のプロジェクト(elfViewsLib プロジェクト)を追加して、その中に作成しています。

src. 7 が作成したマークアップ拡張です。

src. 7 は『A Better Way to Data Bind Enums in WPF – BRIAN LAGUNAS』で紹介されているサンプルコードではなく、最近 Brian Lagunas さんが配信している『How to Bind an Enum to a ComboBox in WPF – YouTube』で紹介されているコードを参考にしています。

src. 7 はコンストラクタのパラメータに渡された Enum 型を GetValues しているだけのシンプルな実装なので改めて解説が必要な処理は無いと思いますが、1 つだけ補足すると、MarkupExtension を継承したクラスはクラス名の末尾を【Extension】にするのが慣例なので、それに従った名前にしています。

src. 7 の EnumBindingSourceExtension を作成したら、部分 View を格納しているプロジェクトへ参照を追加して src. 8 のようにバインディングを設定します。

ObjectDataProvider を使用した src. 6 と比べてリソースを追加する必要がなく、記述がシンプルになったと思います。src. 8 のように表示する Enum 型が 1 つだけの場合は src. 6 とあまり変わらないように感じるかもしれませんが、画面上に複数のリスト系コントロールがあり、それぞれで Enum 型を表示する場合はリソースの指定が不要な src. 8 の方が記述量が少なく間違えにくいと言えます。

ObjectDataProvider を使用した場合と同じく 12 行目を記述するだけで XAML のプレビューにも表示され、fig. 4 のように実行結果も同じです。

fig.4 EnumBindingSourceExtension で Enum 型を表示する

※ fig. 4 を表示するには MainWindowViewModel のコンストラクタで【EnumMarkupPage】の行を有効にします。

EnumBindingSourceExtension から Enum 型を表示した場合も、ObjectDataProvider を使用した場合と同じく選択項目は ListBox.SelectedValue 等で取得できます

列挙子の代わりに説明を表示する

ここまで Enum 型をリスト系コントロールへ表示する方法を紹介してきましたが、表示項目には列挙子がそのまま表示されています。.NET 標準の Enum 型や、NuGet 等から取得した DLL 形式で提供されるパッケージへ定義されている Enum 型の場合は、列挙子を表示するしかありませんが、src. 5 『所属組織 Enum 型』のように自作の Enum 型の場合は、列挙子とは別に設定した文字列を表示することもできます

例えば、src. 5 の『所属組織 Enum 型』へ src. 9 のように Description 属性を追加します。

所属組織 Enum 型は元々わざとらしく列挙子の末尾にコード No. を付加していますが、画面に表示する場合は省略したい場合もあると思います。そんな場合は src. 9 のように列挙子に付加した Description 属性(DescriptionAttribute)を表示することもできます。

ListBox の項目へ src. 9 で追加した Description 属性を表示するには src. 10 のような TypeConverter を継承したクラスを作成します。

この連載ではまだ取り上げていませんが、コンバータと言えばバインド時に指定する【Converter プロパティ】を思い浮かべるかもしれません。src. 10 で作成したコンバータは TypeConverter を継承したクラスなので、XAML へ指定する IValueConverter を継承したクラスとは別物です。TypeConverter を継承したクラスは XAML へ設定するのではなく XAML に表示するクラスに属性として指定すると言う違いがあります。src. 10 では EnumConverter を継承していますが、EnumConverter は TypeConverter を継承したクラスです。

コンバータについては管理人も未だに詳しく調べていないので、多くは語れませんが『IValueConverterとTypeConverterの違い – tocsworld』等で両者の違いが紹介されています。

上の記事に書かれているように、TypeConverter は XAML パーサーが使用するコンバータなので、TypeConverter が設定されている型を XAML へ表示する際に自動的に適用されます。

そのため、src. 10 で作成した EnumDescriptionTypeConverter は Model 系のクラスから使用するクラスと言う位置付けなので、src. 7 で紹介した EnumBindingSourceExtension 用のプロジェクトではなく、新規に追加した別プロジェクト(elfConverterAttributes プロジェクト)内に作成しています。

そして、Model のプロジェクト(SampleModels)へ elfConverterAttributes プロジェクト(プロジェクト名を elfTypeConverers にすれば良かったと後悔しています…)への参照を追加して、src. 11 のように所属組織 Enum 型へ EnumDescriptionTypeConverter を設定します。

src. 11 のように所属組織 Enum 型へ TypeConverter 属性として指定して実行すると、fig. 5 のように Description 属性に設定した文字列が表示されるようになります。

fig.5 EnumDescriptionTypeConverter を使用した場合の表示

fig. 5 のように Description 属性が指定されている列挙子は Description に指定した文字列、Description 属性が指定されていない列挙子は、列挙子がそのまま表示されているのが分かると思います。尚、書くまでもなく Description 属性を指定しても ListBox.SelectedValue は所属組織 Enum 型の値のままです。

そして、上に書いた通り EnumDescriptionTypeConverter は XAML パーサーが自動的に使用するので、本章のように EnumBindingSourceExtension を使用した場合だけでなく ObjectDataProvider を使用した場合も fig. 5 と同じ表示になります

EnumBindingSourceExtension についての補足

このエントリの元ネタにさせていただいた『A Better Way to Data Bind Enums in WPF – BRIAN LAGUNAS』ではここまでしか紹介されていませんが『How to Localize Enums in C# – YouTube』では同じく Brian Lagunas さんが Description 属性を継承した LocalizedDescription 属性を作成して、表示文字列をローカライズする方法も紹介されています。

ローカライズについてはやるなら別途書きたいと思っているので、ここでは Brian Lagunas さんの配信動画の紹介までに留めます。そのため、Enum のローカライズについて知りたい場合は、上で紹介した Brian Lagunas さんの動画や、かずきさんが書かれた『Enum をローカライズして WPF でコンボボックスなどにバインドする方法 – Qiita』等を見てください。

かずきさんの記事は Brian Lagunas さんの動画で紹介されている方法とは別の方法を紹介されています。

最後に念のため補足しておきますが、本章で紹介した方法を使う場合、Description 属性は必須ではありません。別の属性を使用しても構いませんし、別の属性を自作しても構いません。EnumDescriptionTypeConverter から返す文字列が設定できる属性なら何でも OK です。(その際、EnumDescriptionTypeConverter はクラス名を変えるべきだと思います)

あとがき的な

この step: 10 までで MVVM パターンでデータをバインドする基本的な方法は紹介できたと思います。本来であれば、内容を Prism に戻して RequestNavigate や IDialogService についての記事も新たに加えたい所ですが、その辺りの話は WPF Prism episode シリーズで書いた事とあまり変わらない内容になりそうなので、.NET5 WPF Prism MVVM 入門 2020 は一旦休止して、WPF UI Gallery のリライトを進める予定です。

ただ、WPF UI Gallery のリライトと言っても方針として決めているだけなので、内容的に .NET5 WPF Prism MVVM 入門 2020 で先に紹介した方が良い場合はこっちの連載を先に公開する場合もあります。

まあ、こっちの連載を完全に止めるつもりじゃないんだな… 程度に思ってください。

 

尚、今回もこのエントリで紹介したサンプルコードは GitHub リポジトリ に上げています。

 

 

 

おすすめ

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください