(XAML#16)「バインディングと RelativeSource」
昨日の値コンバーターで挙げた例でもそうですが、これまで、バインディングの対象となるコントロールは ElementName で明示的にコントロールの名前を指定していました。しかし、このように名前が必要となるのは使いにくい場合があります。たとえば、昨日の例で複数の StackPanel が同じような構成になっている場合、以下のように、それぞれのコントロールに異なる名前を付けてバインディングしなければなりません。
[XAML]
<Window x:Class="WpfApp1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApp1"
Title="MainWindow" Height="325" Width="525">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<StackPanel>
<CheckBox x:Name="check1" Content="Visibility #1" />
<TextBlock Text="Sample Text" Visibility="{Binding ElementName=check1, Path=IsChecked,
Converter={x:Static local:BoolVisibilityConverter.Converter}}" />
</StackPanel>
<StackPanel>
<CheckBox x:Name="check2" Content="Visibility #1" />
<TextBlock Text="Sample Text" Visibility="{Binding ElementName=check2, Path=IsChecked,
Converter={x:Static local:BoolVisibilityConverter.Converter}}" />
</StackPanel>
</Grid>
</Window>
より複雑な構造を持つ場合、コピー&ペーストしているときに名前を間違えてしまうかもしれません。同じ構造であれば、そのままコピーできるほうが望ましいでしょう。
このような場合に、「相対的」に参照先を指定できるようにするのが RelativeSource という指定です。上記で RelativeSource を使うのは、少し後回しにして、より単純な例で説明します。
次の記述は、テキストボックスに入力した文字列(Text プロパティ)を、そのまま ToolTip として表示するという記述です。
[XAML]
<TextBox Text="(input here...)" ToolTip="{Binding RelativeSource={RelativeSource Self}, Path=Text}" />
RelativeSource には Self(自分自身)、FindAncestor(AncestorType や AncestorLevel で指定した型に合う祖先)、PreviousData(直前にバインディングされていたデータ)、TemplatedParent(自分をテンプレートに組み込んだ親)などのオプションを指定することができます。ここでは、Self を指定して、自分自身のコントロールにバインディングし、Path で Text プロパティを参照しているため、ToolTip プロパティは常に Text プロパティと連動することになります。
FindAncestor は、自分自身を配置している親(祖先)を検索して、AncestorType オプションで指定した型に合うものを探してくれます。たとえば、次のように記述すると Slider の Value プロパティは、配置されている親をたどって Border を探し、その高さを Slider の位置に合わせて変更できます(Border の背景を黄色にしているのは、サイズの変化をわかりやすくするためです)。
[XAML]
<Border Background="Yellow" VerticalAlignment="Top">
<Slider Maximum="100" Minimum="40"
Value="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Border}}, Path=Height}" />
</Border>
次のように、途中に別のコンテナがあったとしてもその親をたどって Border を見つけてくれます(見つからない場合はバインディングが無効になります)。
[XAML]
<Border Background="Yellow" VerticalAlignment="Top">
<GroupBox Header="Group">
<Slider Maximum="100" Minimum="40"
Value="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Border}}, Path=Height}" />
</GroupBox>
</Border>
さて、最初の例に戻ります。この例では TextBlock は自分が配置されている親ではなく、隣に並んで配置されている CheckBox にバインディングしています。RelativeSource は祖先は探してくれますが、兄弟や子どもを探すという指定はありません。しかし、同じ構造を持っているなら、自分の親を探して、その子ども(Children)を参照するということはできます。
上記の例は、次のような記述で置き換えることができます。
<StackPanel>
<CheckBox Content="Visibility" />
<TextBlock Text="Sample Text" Visibility="{Binding RelativeSource={RelativeSource FindAncestor,
AncestorType={x:Type StackPanel}}, Path=Children[0].(CheckBox.IsChecked),
Converter={x:Static local:BoolVisibilityConverter.Converter}}" />
</StackPanel>
ここで Path 指定は Children プロパティの最初に配置されている StackPanel を取り出すために、最初の要素(Children[0])を取り出し、CheckBox 型にキャストした後で IsChecked プロパティを参照する((CheckBox.IsChecked))という意味の指定になっています。こうすれば、同じ構造を持つ複数のコントロールがあっても、コントロール名を使わずにバインディングできます(当然、コントロールの順序を入れ替えると、正しく動作しなくなります)。
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<StackPanel Grid.Row="0">
<CheckBox Content="Visibility #1" />
<TextBlock Text="Sample Text" Visibility="{Binding RelativeSource={RelativeSource FindAncestor,
AncestorType={x:Type StackPanel}}, Path=Children[0].(CheckBox.IsChecked),
Converter={x:Static local:BoolVisibilityConverter.Converter}}" />
</StackPanel>
<StackPanel Grid.Row="1">
<CheckBox Content="Visibility #2" />
<TextBlock Text="Sample Text" Visibility="{Binding RelativeSource={RelativeSource FindAncestor,
AncestorType={x:Type StackPanel}}, Path=Children[0].(CheckBox.IsChecked),
Converter={x:Static local:BoolVisibilityConverter.Converter}}" />
</StackPanel>
</Grid>
コントロール名を使わないことで、スタイルとして定義することもできるようになります。上記の記述をスタイルを使って置き換えると、次のようになります。
<Window.Resources>
<Style x:Key="MyTextBlockStyle" TargetType="{x:Type TextBlock}">
<Setter Property="Visibility" Value="{Binding RelativeSource={RelativeSource FindAncestor,
AncestorType={x:Type StackPanel}}, Path=Children[0].(CheckBox.IsChecked),
Converter={x:Static local:BoolVisibilityConverter.Converter}}" />
</Style>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<StackPanel Grid.Row="0">
<CheckBox Content="Visibility #1" />
<TextBlock Text="Sample Text" Style="{StaticResource MyTextBlockStyle}" />
</StackPanel>
<StackPanel Grid.Row="1">
<CheckBox Content="Visibility #2" />
<TextBlock Text="Sample Text" Style="{StaticResource MyTextBlockStyle}" />
</StackPanel>
</Grid>
...