オルタナティブ・ブログ > IT's my business >

IT業界のコメントマニアが始めるブログ。いつまで続くのか?

(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>
    ...

Xaml16

Comment(0)