複数のデータをバインドしたいから独自型をつくりたい!
WPFのユーザーコントロールを作るとき、そのユーザーコントロールへのデータバインディングは、ユーザーコントロールの利用者となる親画面の
- ViewModelに
ReactiveProperty
を生やして - Viewに配置したユーザーコントロールのプロパティにバインド
というような実装ができるといいですよね。ユーザーコントロールにバインドするデータ本体は親画面のModelで作るわけなのでViewModelから画面に表示する形に加工してバインドするのが自然です。
しかし、ユーザーコントロールへのバインドは既存のTextBlock
のText
プロパティにセットするのとはワケが違います。ユーザーコントロールは複数の操作を簡略化したり複数のコントロールをまとめるために作るからです。そのため、ユーザーコントロールにバインドしたい型はint
やstring
のようなちゃちなものではなく、適切な名前をつけたプロパティ名に対して自分で定義したclass, record
をバインドしたいと思うのが普通でしょう。
そこでやり方を検索すると「依存関係プロパティを生やしてバインド用のプロパティを作ります」という説明を見かけますが、別にそんなものなくても独自型のバインドはできます。本稿では2つのやり方でのバインド方法を説明するとともに、利点と欠点について説明したいと思います。
前提
画面の実装は以下とします。
- ユーザーコントロールに依存関係プロパティを作る場合、コードビハインドで実装します。
- 親画面はMVVMです。
-
<Nullable>enable</Nullable>
です。ターゲットフレームワークはnet5.0-windows
です。 -
ReactiveProperty
を使います。本稿はReactiveProperty
くんを知ってる前提で書いています
作業の前に、バインドしたいデータの型を作っておきましょう。ここでは簡単のためユーザーコントロールはデータを表示するだけとします。表示するだけならメンバの変更が発生しないので普通のrecord
型で十分です(親のVMでReactiveProperty<T>
を持ってバインドすればよいため)。
もしユーザーコントロールで書き込みしたり、親のVMでメンバを書き換えたりする場合はINotifyPropertyChanged
を実装したクラスにするのを忘れないでください。MVVMフレームワークのPrismを使っているならBindableBase
を継承していつものやり方をすればいいです。
public record MyControlData(string Id, int Count, double? Point);
また、画面にバインドするプロパティをViewModelに定義しておきます。
public ReactiveProperty<MyControlData> MyData { get; } = new(new("", 0, null));
依存関係プロパティを作る方法
ユーザーコントロールにDependencyProperty
型のプロパティを作る必要があります。これはボイラープレートコードなので、そのまま以下の形式で覚えてしまえばよいでしょう。
ここではユーザーコントロールをUserControl1
、画面からバインドする依存関係プロパティの名前をValue
とします。また、以下のコードはコードビハインド(UserControl1.xaml.cs
)に書きます。MVVMアプリであっても、極端に複雑なことをしていなければユーザーコントロールのコードはすべてコードビハインドに書いたほうがいいです。
public partial class UserControl1 : UserControl
{
public UserControl1()
{
InitializeComponent();
}
// 依存関係プロパティの定義に必要なDependencyProperty(ボイラープレート)
// わかりやすくするため名前は プロパティ名+Property にする
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register(
nameof(Value), // プロパティ名
typeof(MyControlData), // バインドするデータの型
typeof(UserControl1), // 自分自身の型
new PropertyMetadata( // 初期値をPropertyMetadata経由でつっこむ
new MyControlData("", 0, null))
);
// 実際にバインドするプロパティ
// 上で定義したDependencyPropertyにあーだこーだしてアクセスさせる
public MyControlData Value
{
get => (MyControlData)GetValue(ValueProperty);
set => SetValue(ValueProperty, value);
}
}
UserControl1
上に配置したコントールにはValue
経由で各メンバにアクセスしてバインドします。ただし、UpdateSourceTrigger
の設定は忘れないでください。これを忘れると、大本のMyData
プロパティの値を変更してもユーザーコントロールに変更が通知されません。
<TextBlock Text="{Binding Value.Id, UpdateSourceTrigger=PropertyChanged, RelativeSource={RelativeSource AncestorType={x:Type local:UserControl1}}}" />
<TextBlock Text="{Binding Value.Count, UpdateSourceTrigger=PropertyChanged, RelativeSource={RelativeSource AncestorType={x:Type local:UserControl1}}}" />
<TextBlock Text="{Binding Value.Point, UpdateSourceTrigger=PropertyChanged, RelativeSource={RelativeSource AncestorType={x:Type local:UserControl1}}}" />
ここまでできたら、あとは利用者のViewのxamlでいつもどおりバインドするだけです。
<!-- 自分で作ったプロパティ(ここではValue)にReactivePropertyをバインド -->
<local:UserControl1 Value="{Binding MyData.Value}" />
もう1つの方法:DataContext
実装量としてはこちらの方法が少ないです。UserControl2.xaml.cs
のコードビハインドへの実装は完全に不要なためです。まず、バインドする型MyControlData
を定義後、ユーザーコントロールのxamlで各コントロールに以下のようにバインドを設定します。例によってUpdateSourceTrigger
の設定は忘れないようにしてください。
なお、混同を防ぐためこの方法のコントロールはUserControl2
とします。
<!-- バインドするMyControlData型の各メンバをそのまま設定する -->
<TextBlock Grid.Row="0" Text="{Binding Id, UpdateSourceTrigger=PropertyChanged}" />
<TextBlock Grid.Row="1" Text="{Binding Count, UpdateSourceTrigger=PropertyChanged}" />
<TextBlock Grid.Row="2" Text="{Binding Point, UpdateSourceTrigger=PropertyChanged}" />
その後、利用者の画面側のViewのxamlに以下のように記述します。
<Window x:Class="MyNamespace.MainWindow"
~~ 名前空間とかいろいろ設定 ~~
>
<!-- 先頭でResourcesブロックにバインド内容を登録する -->
<Window.Resources>
<DataTemplate DataType="{x:Type local:MyControlData}"> <!-- バインドする独自型 -->
<local:UserControl2 /> <!-- ユーザーコントロール名 -->
</DataTemplate>
</Window.Resources>
<!-- 依存関係プロパティでは新しく作ったプロパティにバインドしていた -->
<!-- この方法ではDataContextにバインドする -->
<local:UserControl2 DataContext="{Binding MyData.Value}" />
</Window>
これだけで独自型のバインド設定は完了です。簡単ですね。
※ 利用者のViewとViewModelの接続をどのようにしているかは皆さんの利用しているMVVMフレームワークによると思いますが、もし試しにViewModelを使わずコードビハインドでViewを定義する場合、画面のコンストラクタでInitializeComponent()
の実行後にDataContext = this;
とするのを忘れないようにしてください。
利点と欠点
方法が複数あるということは、使いどころ、利点と欠点というものが存在します。
- DataContextを使う場合、ユーザーコントロールのxamlのバインドで扱えるのはDataContextに設定する型のみ
- データを加工した値等を画面表示に使いたい場合、その独自型のメンバとして画面表示用の値を持つプロパティやメソッドを実装し、それをバインドする必要がある。つまり、「データを画面表示用に加工する」責務はその独自型が担うことになる
- DataContextは1こしか設定できないため、違う型を複数指定できない。それらを持つ
object[]
にするとか、複数の型をまとめる型を更に作る必要がある
- 依存関係プロパティは複数作れる
- 異なる2つの型のデータを別々の依存関係プロパティにすることで、それぞれバインドすることができる
-
データを画面表示内容に加工する責務はユーザーコントロールのView(コードビハインドで完結させない場合はViewModel)になる
- 独自型は単なるデータとして扱い、コードビハインドで
ReactiveProperty
を作って独自型をINotifyPropertyChanged
にすれば、「コンストラクタでそれぞれの依存関係プロパティをObserveProperty().Select(~~).ToReadOnlyReactivePropety()
したプロパティをxamlでバインド」という、普段からよくやっている方法を採れる
- 独自型は単なるデータとして扱い、コードビハインドで
- 実装はやや面倒くさい
どうやって使い分けるか
私の場合は、データクラス側をいろんなところで使い回すかどうかで考えるようにしています。いろんなところで共有する型の場合、依存関係プロパティにしてデータの加工はユーザーコントロールの責任にすることで、データの利用者(ユーザーコントロール)によって見せ方を変えるといったことができるためです。
もちろん、あるユーザーコントロール専用のデータクラスにするならDataContextで十分でしょう。そのような場合はユーザーコントロールの内部クラスにしたくなりますがxamlでは入れ子のメンバーをバインドできないため内部クラスにはできないことに注意してください。
ユーザーコントロールだろうとなんだろうと必ず表示用データは親画面のViewModelで加工する(#,##0のような低次元のフォーマットはxaml)、というルールとするのもアリだと思います。この場合はユーザーコントロールと画面表示値(string
やenum
)の構造体が必ず1:1になり、Modelが管理する生データを親画面のViewModelで構造体に変換してバインドするようなイメージになると思います。
余談
ユーザーコントロールって単語長すぎ!読みにくいしタイプするの疲れる