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通りのコードが書かれています。 こちらも切り替えて動作を確認してみてください。