wpfで独自のコントロールを作るには3通りのやり方があります。
- カスタムコントロール ... 既存のコントロールを改造(継承)する。
- ユーザーコントロール ... 複数の既存のコントロールを組み合わせて作る。
- 自作 ... Controlクラスを継承して1から作る。
今回はユーザーコントロールについてのお話。
ユーザーコントロールでDataContextを使うとき、
public partial class TestControl : UserControl { public TestControl() { InitializeComponent(); this.DataContext = new TestData(); } }
というようにthis.DataContextにインスタンスを設定してはいけません。 ユーザーコントロールを使う側にDataContextを変更されると挙動が変わってしまいます。
上に書いたとおり、ユーザーコントロールは複数のコントロールを組み合わせて作ります。 そのため必ずGridやStackPanelのようなコンテナとなるコントロールが含まれます。
<UserControl x:Class="UserControlDataContextTest.TestControl" ~その他属性 > <Grid Name="baseContainer"> ←例えばコレ ~中身~ </Grid> </UserControl>
そのコンテナのDataContextを使いましょう。
public partial class TestControl : UserControl { public TestControl() { InitializeComponent(); baseContainer.DataContext = new TestData(); } }
「なぜそんなことをするか?」と言うのは、フレームワークのバインド対象となるDataContextの探し方が関係します。
バインドソースが省略されたとき、バインド対象はDataContextになります。 対象となるDataContextは、まずBindingが書かれたコントロールのDataContextを見て、nullなら親コントロールを見て...というように順番に見て決定されます。 例えば次のようなxamlの場合、
<UserControl x:Class="UserControlDataContextTest.TestControl" ~その他属性 > <Grid> <TextBlock Text="{Binding DataText}"/> </Grid> </UserControl>
まずTextBlockのDataContextをチェック。 それがnullならば親のGridのDataContextをチェック。 それがnullならばさらに親のUserControlのDataContextをチェックの順です。
では、ユーザーコントロールを使う側はどうでしょう?
<Window xmlns:app="clr-namespace:UserControlDataContextTest" ~その他属性 > <app:TestControl TextB="{Binding DataText}"/> </Window>
まずはユーザーコントロール(app:TestControl)のDataContextをチェック ... ユーザーコントロール内部でUserControl.DataContextが設定されていた場合ここで引っかかりますね。 使う側でWindow.DataContextが別に設定されていても、そちらはバインドに使用されません。 ユーザーコントロール内部で設定したDataContextの方が使用されてしまいます。
そうなるのはWindow側でUserControl.DataContextを触らなかったときのお話。 Window側でユーザーコントロールのDataContextが書きかえられると、今度はユーザーコントロール内部のバインドが崩れてしまいます。
ユーザーコントロール内部でコンテナのDataContextを使った場合は、そういう副作用はなくなります。 使う側から見て自分の設定したWindow.DataContextがふさがれる事がなくなり、ユーザーコントロールから見てWindowに中身を荒らされる事はなくなります。
なので必要な場合はコンテナのDataContextを使いましょう。
- xamlでUserControl要素の直下にあるコンテナにbaseContainerという名前を付ける。
- UserControlのコンストラクタでInitializeComponentの後に「baseContainer.DataContext = なんたら」と書く。
これ定石。
...このことに気付いたの、前のネタを投稿した後なんですよね。 もうちょっと早ければ。
以下、上の動作を確認するためのサンプルコードです。 TestControlという名前のユーザーコントロールを作ります。 TestControl.xamlは、
<UserControl x:Class="UserControlDataContextTest.TestControl" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" mc:Ignorable="d" d:DesignHeight="48" d:DesignWidth="300" > <Grid Name="baseContainer"> <Grid.ColumnDefinitions> <ColumnDefinition Width="auto"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition/> <RowDefinition/> </Grid.RowDefinitions> <TextBlock Grid.Column="0" Grid.Row="0">A :</TextBlock> <TextBlock Grid.Column="1" Grid.Row="0" Text="{Binding DataText}"/> <TextBlock Grid.Column="0" Grid.Row="1">B :</TextBlock> <TextBlock Grid.Column="1" Grid.Row="1" Name="textBlockB"/> </Grid> </UserControl>
AにはデータバインドでTestControl内部のDataContextから「DataText」というプロパティを探してきて表示します。 Bは使う側のコントロールから渡された値を表示します。
TestControl.xaml.csです。
// TestControl.xaml.cs using System.Windows; using System.Windows.Controls; namespace UserControlDataContextTest { public partial class TestControl : UserControl { public TestControl() { InitializeComponent(); //this.DataContext = new TestData("UserControlのDataContext"); baseContainer.DataContext = new TestData("UserControlのDataContext"); } public static readonly DependencyProperty TextBProperty = DependencyProperty.Register( "TextB", typeof(string), typeof(TestControl), new FrameworkPropertyMetadata( new PropertyChangedCallback(TestControl.OnTextBChanged) ) ); public string TextB { get { return (string)GetValue(TextBProperty); } set { SetValue(TextBProperty, value); } } private static void OnTextBChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args) { TestControl thisCtrl = (TestControl)obj; thisCtrl.textBlockB.Text = (string)args.NewValue; } } }
TextBというプロパティを用意して、使う側でバインドできるようにしています。 TextBが変更されたらtextBlockBに反映します。
DataContextに設定しているTestDataクラスはコレです。
// TestData.cs namespace UserControlDataContextTest { public class TestData { public TestData(string text) { DataText = text; } public string DataText { get; set; } } }
DataTextというプロパティがあるだけです。 このプロパティでどのDataContextが使われているのか確かめます。
使う側のMainWindow.xamlのコードです。
<Window x:Class="UserControlDataContextTest.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:app="clr-namespace:UserControlDataContextTest" Title="MainWindow" Height="80" Width="300" > <app:TestControl x:Name="testControl" TextB="{Binding DataText}"/> </Window>
バインド対象はもちろん、TestControlで使われているDataContextではなくMainWindowが持つDataContextです。
// MainWindow.xaml.cs using System.Windows; namespace UserControlDataContextTest { public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); this.DataContext = new TestData("MainWindowのDataContext"); //testControl.DataContext = new TestData("MainWindowのDataContext"); } } }
サンプルコードをそのまま実行すると、AにはUserControlのDataContext、BにはMainWindowのDataContextと表示されます。 TestControlのコンストラクタには、定石通りにコンテナのDataContextを使うコードと、コメントアウトされたthis.DataContextを使うコードが書かれています。 切り替えて動作を確認してみてください。
MainWindowのコンストラクタにも2通りのコードが書かれています。 こちらも切り替えて動作を確認してみてください。