LoginSignup
1
3

[WPF]UserControlに対してBindingする

Last updated at Posted at 2023-09-05

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は以下のようなものである。
Pasted image 20230905151539.png

一見何の変哲もない「チェックボックス付きボタン」だが、裏ではややこしいことをしている。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はToggleButtonCheckBoxの組み合わさったものである。
  • ToggleButtonCheckBoxは、IsCheckedの状態を共有する。
  • ToggleButtonCheckBoxは、共通の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に焦点を当てて現状をざっくり図にするとこんな感じ。
binding.jpg

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の参照先を指定しないと「ParentDataContextIsCheckedというプロパティはありません」と言われる。

しかし一方で、DataContextを自身に設定してしまうと(これを仮にChildDataContextと呼ぶ)、今度は呼び出し側で不都合が起きる。
仮に以下のように親側で呼び出しを行っていた時、「ChildDataContextMyControlIsCheckedというプロパティはありません」と言われてしまうのである。

<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を実装したプロパティを書いてあげればいいと思います。

1
3
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
3