かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

WPF4.5入門 その47 「コンテンツモデル」

WPFの重要なコントロールの1つにContentControlクラスがあります。このクラスは、Contentプロパティに設定された単一の要素を表示するという機能を提供するコントロールです。「WPFのコンセプト」でも紹介しましたが、このコントロールが、要素を表示する際の詳細なロジックを以下に示します。

  • ContentTemplateにDataTemplateが設定されている場合、ContentプロパティにContentTemplateを適用した結果を表示します。
  • ContentTemplateSelectorにDataTemplateSelectorが設定されている場合、ContentプロパティにContentTemplateSelectorが返したDataTemplateを適用した結果を表示します。
  • Contentプロパティに設定された値の型に紐づけられたDataTemplateがある場合、そのDataTemplateを適用した結果を表示します。
  • ContentプロパティがUIElement型の場合、そのまま表示されます。(UIElementにすでに親がいる場合は例外が出ます)
  • Contentプロパティに設定された値の型に紐づけられたTypeConverterでUIElementに変換するものがある場合は、変換した結果を表示します。
  • Contentプロパティに設定された値の型に紐づけられたTypeConverterでString型に変換するものがある場合はString型に変換してTextBlockにラップして表示します。
  • Contentプロパティに設定された値の型がXmlElementの場合は、InnerTextプロパティの値をTextBlockにラップして表示します。
  • Contentプロパティに設定された値をToStringした結果をTextBlockにラップして表示します。

複雑なロジックですが、端的にいうと、可能な限りUIElementに変換できるか試した後に、ダメだったら文字列型にしてTextBlockに格納して表示するというロジックになります。このような処理をContentControlが行ってくれるおかげで、ContentControlを継承するButtonクラスやLabelクラスやListBoxItemクラスで、以下のような直観的なプログラミングが可能になっています。

Buttonクラスに文字列を表示する場合は以下のように文字列を設定できます。

this.button.Content = "こんにちは世界";

Buttonクラス内にButtonを表示する場合も以下のように直接Buttonを設定できます。

this.button.Content = new Button { Content = "ボタンの中のボタン" };

DataTemplate

ContentControlクラスのContentTemplateプロパティに設定可能なDataTemplateについて説明します。DataTemplateは、主にContentプロパティにオブジェクトが設定されている場合に、どのようにそのオブジェクトを表示するかを定義します。以下にListBoxクラスのItemTemplate(ListBoxItemのContentプロパティに適用されるDataTemplate)を使ったプログラム例を示します。

<Window x:Class="DataTemplateSample01.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:DataTemplateSample01"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <ListBox x:Name="listBox">
            <ListBox.ItemTemplate>
                <DataTemplate DataType="{x:Type local:Person}">
                    <Border BorderBrush="Red" BorderThickness="1" Padding="5">
                        <StackPanel Orientation="Horizontal">
                            <Label Content="Name" />
                            <TextBlock Text="{Binding Name}" VerticalAlignment="Center"/>
                            <Label Content="Age" />
                            <TextBlock Text="{Binding Age}" VerticalAlignment="Center"/>
                        </StackPanel>
                    </Border>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
    </Grid>
</Window>

ListBoxのItemsSourceプロパティに設定するオブジェクトは以下のように定義しています。

namespace DataTemplateSample01
{
    public class Person
    {
        public string Name { get; set; }
        public int Age { get; set; }
    }
}

ListBoxのItemsSourceプロパティに適当な値を設定して実行すると以下のようになります。

f:id:okazuki:20140825164016p:plain

この例では、ListBoxの中にDataTemplateを定義していますが、通常はWindowやApp.xamlの中のResourcesにDataTemplateを定義します。こうすることで複数個所で同一のオブジェクトの見た目を再利用することができるようになります。DataTemplateの定義をWindowのResourcesに移動させた場合のコード例を以下に示します。

<Window x:Class="DataTemplateSample01.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:DataTemplateSample01"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <DataTemplate x:Key="PersonTemplate" DataType="{x:Type local:Person}">
            ...省略...
        </DataTemplate>
    </Window.Resources>
    <Grid>
        <ListBox x:Name="listBox" ItemTemplate="{StaticResource PersonTemplate}" />
    </Grid>
</Window>

DataTemplateをResourcesに移動して、Resourcesのオブジェクトを参照するためのStaticResourceマークアップ拡張でItemTempalteにDataTemplateを設定しています。Resourcesに定義されたDataTemplateは、x:Keyを指定せずにDataTypeだけ設定したときに、デフォルトでその型のDataTemplateとして使われるという動きをします。そのため、上記の記述はx:Keyを使わずに以下のように書くことも出来ます。

<Window x:Class="DataTemplateSample01.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:DataTemplateSample01"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <DataTemplate DataType="{x:Type local:Person}">
            ...省略...
        </DataTemplate>
    </Window.Resources>
    <Grid>
        <ListBox x:Name="listBox" />
    </Grid>
</Window>

DataTrigger

DataTemplateには、データの値に応じて表示の見た目を切り替えるロジックを書くことが出来ます。例えばPersonクラスを拡張して40歳以上の場合trueを返すプロパティを追加します。

namespace DataTemplateSample01
{
    public class Person
    {
        public string Name { get; set; }
        public int Age { get; set; }
        public bool IsOver40 { get { return this.Age >= 40; } }
    }
}

このIsOver40プロパティがtrueの時は、枠線の色を青色にするというようなことがDataTriggerを使って実現できます。DataTriggerはDataTemplateのTriggersプロパティに設定できて以下のように記述します。

<DataTemplate DataType="{x:Type local:Person}">
    <Border x:Name="border" BorderBrush="Red" BorderThickness="1" Padding="5">
        <StackPanel Orientation="Horizontal">
            <Label Content="Name" />
            <TextBlock Text="{Binding Name}" VerticalAlignment="Center"/>
            <Label Content="Age" />
            <TextBlock Text="{Binding Age}" VerticalAlignment="Center"/>
        </StackPanel>
    </Border>
    <DataTemplate.Triggers>
        <DataTrigger Binding="{Binding IsOver40}" Value="True">
            <Setter TargetName="border" Property="BorderBrush" Value="Blue" />
        </DataTrigger>
    </DataTemplate.Triggers>
</DataTemplate>

DataTriggerのBindingの値とValueの値が等しいとき、DataTriggerの中に定義したSetterが実行されます。Setterはこの例では1つだけしか設定していませんが、複数指定できます。

実行するとAgeプロパティが40を超えると枠線が青色になっていることが確認できます。

f:id:okazuki:20140825164203p:plain

DateTemplateSelector

DataTemplateSelectorは、条件に応じてDataTemplateを切り替える仕組みです。DataTemplateSelectorは、C#でDataTemplateSelectorクラスを継承して作成します。DataTemplateSelectorクラスを継承して、SelectTemplateメソッドで、状況に応じて適切なDataTemplateを返します。

以下の例では、PersonクラスのAgeプロパティが40より小さい場合はResourcesからPersonTemplate1を検索して返して、Ageプロパティが40以上の場合はResourcesからPersonTemplate2を検索して返すという処理を行っています。

using System.Windows;
using System.Windows.Controls;

namespace DataTemplateSample02
{
    public class PersonDataTemplateSelector : DataTemplateSelector
    {
        public override DataTemplate SelectTemplate(object item, DependencyObject container)
        {
            var p = (Person)item;
            if (p.Age < 40)
            {
                // Ageが40より小さければPersonTemplate1
                return (DataTemplate)((FrameworkElement)container).FindResource("PersonTemplate1");
            }
            else
            {
                // Ageが40以上ならPersonTemplate2
                return (DataTemplate)((FrameworkElement)container).FindResource("PersonTemplate2");
            }
        }
    }
}

PersonTemplate1と2は、WindowのResourcesに以下のように定義しています。

<!-- NameとAgeを表示 -->
<DataTemplate x:Key="PersonTemplate1" DataType="{x:Type local:Person}">
    <StackPanel Orientation="Horizontal">
        <Label Content="Name" />
        <TextBlock Text="{Binding Name}" VerticalAlignment="Center"/>
        <Label Content="Age" />
        <TextBlock Text="{Binding Age}" VerticalAlignment="Center"/>
    </StackPanel>
</DataTemplate>
<!-- Nameだけ表示 -->
<DataTemplate x:Key="PersonTemplate2" DataType="{x:Type local:Person}">
    <StackPanel Orientation="Horizontal">
        <Label Content="Name" />
        <TextBlock Text="{Binding Name}" VerticalAlignment="Center"/>
    </StackPanel>
</DataTemplate>

DataTemplateSelectorは、ListBoxではItemTemplateSelectorプロパティに設定します。ButtonクラスのようなContentControlを継承しているクラスに指定する場合はContentTemplateSelectorプロパティを使用します。

<ListBox x:Name="listBox">
    <ListBox.ItemTemplateSelector>
        <local:PersonDataTemplateSelector />
    </ListBox.ItemTemplateSelector>
</ListBox>

ListBoxに適当なPersonクラスのリストを設定して実行した結果は以下のようになります。適用されているテンプレートが変わっていることが確認できます。

f:id:okazuki:20140825164327p:plain

過去記事