WPFのユーザコントロール周りについて調べた話

WPFを使ってサクッと簡単なGUIツールを作ろうとしたのですが、慣れないことをしたせいでかなり苦労しました。C言語とは比べ物にならない難しさを目の当たりにして心が折れそうです。

WPFで何度かアプリを作ろうとしたことがありますが、高確率で面倒になって挫折しています。知っていることといえば、ウィンドウのDataContextにViewModelを登録することデータバインディングができ、UIとロジックの連携をうまくやってくれる、ということでしょうか。変なことをやろうとすると知識不足が露呈して苦労します。

今回はその苦労した内容の話です。覚えた内容を忘れそうなのでメモっておきます。

目標

テキストボックスとボタンをセットにしたコントロールを数千回使いたかったので、ユーザコントロールなるものを導入してコントロールを再利用することにしました。

完成いめーじは、次のように利用できるコントロールです。

<MyControl Base="100" Offset="0" />
<MyControl Base="100" Offset="2" />
<MyControl Base="100" Offset="4" />
<MyControl Base="100" Offset="6" />

コントロール内部にはラベル・テキストボックス・ボタンなどがあります。親が設定したパラメータを使ってボタンを押したときの処理を切り替えようという作戦です。

<UserControl>
    <StackPanel Orientation="Horizontal">
        <Textbox Text="{Binding Address}" />
        <Button Command="{Binding SendCommand}">
    </StackPanel>
</UserControl>

DependencyProperty

XAMLからコントロールに値を設定するには、依存関係プロパティなるものを使うようです。MSDNの長ーい説明は読み飛ばしてしまいましたが、ようするに通常のc#のプロパティ(CLRプロパティ)よりも高機能なプロパティで、XAMLから値を読み書きするにはこれを使う必要があるとのこと。

XAMLのコードビハインドに、次のような命名則でDPを記述します。DPの読み書きにはSetValue(), GetValue()を使うのですが、なにかと不便なので一般的にはCLRプロパティも併せて実装するようです。

public static readonly DependencyProperty BaseProperty =
    DependencyProperty.Register(
        "Base", typeof(int), typeof(MyControl),
        new FrameworkPropertyMetadata(0, OnBasePropertyChanged));

public string Base
{
    get { return (int)GetValue(BaseProperty); }
    set { SetValue(BaseProperty, value); }
}

XAMLからセットした値はどのように受け取ればよいのでしょうか。初めはこのCLRプロパティのセッターが呼ばれるときに値を読みだそうとしたのですが、どうやらXAML経由で値をセットする場合は直接Set/GetValueされるため、CLRプロパティは使われません。DPの登録時の第4引数にPropertyChanged時の処理を設定できるので、これを使ってバインディングされた値を受け取ります。

受け取った値をViewModelにセットすれば、XAMLから受け取った値をVMで使えます。

DataContext = this?

至るところで言われていますが、ユーザコントロールの内部でバインディングするために次のように書いてはいけません。

public MyControl()
{
    InitializeComponent();
    DataContext = this;
}

ユーザコントロールの利用者が

<MyControl Base="100" Offset="0" />

ではなく

<MyControl Base="{Binding MyBase}" Offset="0" />

としたときに、コントロール自身が自前のデータコンテキストを設定していると、コントロールの利用者が意図したデータコンテキストが使用できないからです。

で、調べる限り様々な方法があって混乱しましたが、ひとまず次のように書きました。

<UserControl>
    <StackPanel Orientation="Horizontal"
                x:Name="ControlGroup"> 
        <Textbox Text="{Binding Address}" />
        <Button Command="{Binding SendCommand}">
    </StackPanel>
</UserControl>
public MyControl()
{
    InitializeComponent();
    ControlGroup.DataContext = this;
}

このStackPanelの所有者はユーザコントロールなので、これをどう扱ってもコントロールの利用者には影響が出ません。良し悪しはさておき、この方法は初学者には分かりやすいと思います。

おわり

ユーザコントロールを作成したときに登場する依存関係プロパティとその値の受け取り方、データコンテキストの正しい設定方法などを学びました。

実は最初はユーザコントロールではなくカスタムコントロールを使用していました。カスタムコントロールのXAMLはResourceDictionaryとして扱われ、UIはControlTemplateとして記述されています。ResourceDictionaryもControlTemplateもTemplateBindingも理解するまで時間がかかりそうだったので今回はユーザコントロールに変更してお茶を濁しました。WPFはよくわからない概念が多くて大変です。余裕ができたら使ってみたい気もしますが、それはまた今度。