WPFにおけるUserControlについて、依存関係プロパティに対しBindingを実施する方法の(暫定)ベストプラクティス。
前提知識
依存関係プロパティ Dependency Property
Binding Target
として公開できるプロパティ。
Binding Target
とは、下で言うText
のこと。
<TextBlock Text="{Binding WarningText}" />
ちなみに、"{Binding WarningText}"
の箇所はBinding Source
と呼称する。
依存関係プロパティは UserControlのDataContext(通常、コードビハインドでよい)にて以下のように定義する。
えげついボイラープレートだが、VisualStudioならpropdp
と入力すればスニペットが使用できる。
public string Text
{
get { return (string)GetValue(TextProperty); }
set { SetValue(TextProperty, value); }
}
public static readonly DependencyProperty TextProperty =
DependencyProperty.Register(nameof(Text), typeof(string), typeof(ToggleAndCheckButton));
ここで言うText
は、下に定義する依存関係プロパティTextProperty
に対する操作を包んだラッパーであり、また外部からxaml上などで呼び出される場合に公開されるプロパティとなる。
先述の通り、Binding Target
として機能させるためには、DependencyProperty
としての登録が必要なため、このような記述を要する(WPFのBindingシステムに依るもの)。
- CLRプロパティ(いわゆる、通常のプロパティ)が
BindingTarget
として指定されると実行時例外になる。
依存関係プロパティについての詳しい解説は割愛するのでググってください。
本題
今回作成したいUserControlは以下のようなものである。
一見何の変哲もない「チェックボックス付きボタン」だが、裏ではややこしいことをしている。XAMLが以下。
<ToggleButton Style="{StaticResource MaterialDesignOutlinedButton}"
IsChecked="{Binding IsChecked}"
Command="{Binding Command}">
<CheckBox IsChecked="{Binding IsChecked, RelativeSource={RelativeSource AncestorType=ToggleButton}}"
Command="{Binding Command}">
<TextBlock Text="{Binding Text}" />
</CheckBox>
</ToggleButton>
ざっくり流れを追うと、
- このUserControlは
ToggleButton
とCheckBox
の組み合わさったものである。 -
ToggleButton
とCheckBox
は、IsChecked
の状態を共有する。 -
ToggleButton
とCheckBox
は、共通のCommandを叩く。 -
Text
プロパティに設定されたものが、「ボタンのテキスト」として表示される。
以上の点を踏まえると、このUserControlは、依存関係プロパティとして以下の三項を外部へ公開し、Bindingを受け付けたいということになる。
bool IsChecked
ICommand Command
string Text
また、親の画面からは以下のように呼び出せることが期待されている。
<Window>
<local:MyControl IsChecked="{Binding MyControlIsChecked}"
Command="{Binding AlphaCommand}"
Text="{Binding AlphaText}"/>
</Window>
親の画面からUserControlに対し値を受け渡す、というのが一点。
その値が親のVMとBindingされている、というのがもう一点である。
特に、IsChecked
については双方向Bindingになることが予想される。
IsChecked
に焦点を当てて現状をざっくり図にするとこんな感じ。
UserControlの実装 ー XAML
いきなり重要なポイントが有る。一旦XAMLをご覧ください。
<UserControl
//中略//
x:Name="Root">
<ToggleButton Style="{StaticResource MaterialDesignOutlinedButton}"
IsChecked="{Binding IsChecked, ElementName=Root}"
Command="{Binding Command}"
CommandParameter="{Binding IsChecked, ElementName=Root}">
<CheckBox IsChecked="{Binding IsChecked, RelativeSource={RelativeSource AncestorType=ToggleButton}}"
Command="{Binding Command, ElementName=Root}"
CommandParameter="{Binding IsChecked, ElementName=Root}">
<TextBlock Text="{Binding Text, ElementName=Root}" />
</CheckBox>
</ToggleButton>
</UserControl>
先述の通り、例えばIsChecked
には、後ほど公開予定の依存関係プロパティ(のラッパー)をBindingしている。
重要なのは、UserControlにx:Name="Root"
と名前をつけること(名前はなんでもいい)、BindingのElementName
にそれを指定することの二点。
抜粋
<UserControl x:Name="Root">
<ToggleButton IsChecked="{Binding IsChecked, ElementName=Root}"
UserControlはDataContextを明示的に指示しない場合、呼び出し元から階層的に継承されているDataContextを遡って使用する(仮にそれをParentDataContext
と呼ぶ)。なのでElementName、つまりBindingSourceの参照先を指定しないと「ParentDataContext
にIsChecked
というプロパティはありません」と言われる。
しかし一方で、DataContextを自身に設定してしまうと(これを仮にChildDataContext
と呼ぶ)、今度は呼び出し側で不都合が起きる。
仮に以下のように親側で呼び出しを行っていた時、「ChildDataContext
にMyControlIsChecked
というプロパティはありません」と言われてしまうのである。
<local:MyControl IsChecked="{Binding MyControlIsChecked}" />
要は、
-
MyControlIsChecked
は親のDataContextに存在するので、継承されている親のDataContextをそのまま参照して貰う必要がある。 - 一方、UserControl内部でBindingされている
IsChecked
はUserControl自身のDataContext(今回はコードビハインド)が保持しているため、参照先を切り替えなくてはならない
というわけで、DataContextとしては親のものを継承しつつ、UserControl内のBindingSourceの捜索先だけをUserControlのDataContextに切り替えるために、上記の手続きを踏む必要がある。
UserControlの実装 ー コードビハインド
正直、ここは依存関係プロパティを定義するだけなので何も面白いことはない。
強いて強調するなら、繰り返しになるが依存関係プロパティとはBindingTargetであることをWPFのバインディングシステムに登録するためのものということを抑えておくといいかもしれないかも(僕は結構混乱した)。
public partial class MyControl : UserControl
{
public bool IsChecked
{
get { return (bool)GetValue(IsCheckedProperty); }
set { SetValue(IsCheckedProperty, value); }
}
public static readonly DependencyProperty IsCheckedProperty =
DependencyProperty.Register(nameof(IsChecked), typeof(bool), typeof(MyControl));
public ICommand Command
{
get { return (ICommand)GetValue(CommandProperty); }
set { SetValue(CommandProperty, value); }
}
public static readonly DependencyProperty CommandProperty =
DependencyProperty.Register(nameof(Command), typeof(ICommand), typeof(MyControl));
public string Text
{
get { return (string)GetValue(TextProperty); }
set { SetValue(TextProperty, value); }
}
public static readonly DependencyProperty TextProperty =
DependencyProperty.Register(nameof(Text), typeof(string), typeof(MyControl));
public MyControl()
{
InitializeComponent();
}
}
親側
あとは何も特別なことはない。普段通りBindingしてやればいいだけ。
<Window>
<local:MyControl IsChecked="{Binding MyControlIsChecked}"
Command="{Binding AlphaCommand}"
Text="{Binding AlphaText}"/>
</Window>
VMなりコードビハインドなりで、INotifyPropertyChanged
を実装したプロパティを書いてあげればいいと思います。