imani-cの日記

WPFスタイルとテンプレートの本をAmazonで出しました。ちょっととっつきにくいこれらについて知りたいお方は、覗いてみてください。「WPF スタイル」で検索すると、トップに出ます。gRPCの本もあります。「gRPC入門」で検索するとすぐ見つかります。

WPF - ContentTemplateをデータの値によって自動的に選ぶ

ContentControlにバインドされているオブジェクトの中身を思う通りの配置や書式で表示させたいときには、DataTemplateを使います。また、それをオブジェクトに応じて変えることもできます。こちらの記事ではバインドされたオブジェクトの種類の違い、つまり、クラスの違いにより適切なDataTemplateを選ぶ例を説明しました。 ここでは、オブジェクトに含まれるプロパティの値の違いにより切り替える方法を紹介します。例として、こちらの記事で使ったMyPetInfoクラスを利用して、体重により小型犬と大型犬に区別し、異なる表示させます。

目次

DataTemplateSelectorを使うXamlコード

Xamlでは選択ロジックを書けませんから、C#で選択コードを書かねばなりません。したがって、XamlからはC#で書いた選択コードを呼び出す必要があります。例を掲げます。

<Window x:Class="DataTemplateEtc.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:DataTemplateEtc"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">

    <StackPanel Visibility="Visible" Background="Silver">
        <StackPanel.Resources>
            <!-- 小型犬の表示 -->
            <DataTemplate DataType="{x:Type local:MyPetInfo}" x:Key="SmallDogInfoTemplate">
                <StackPanel>
                    <TextBlock Text="Dog information" FontSize="10"/>
                    <TextBlock Text="{Binding Name, StringFormat=Pet name is {0:s}.}"/>
                    <TextBlock Text="{Binding Weight, StringFormat=Pet weight is {0:0.0} kg.}"/>
                    <TextBlock Text="小さいぞ"/>
                </StackPanel>
            </DataTemplate>
            <!-- 大型犬の表示 -->
            <DataTemplate DataType="{x:Type local:MyPetInfo}" x:Key="LargeDogInfoTemplate">
                <StackPanel>
                    <TextBlock Text="Large dog information"/>
                    <TextBlock Text="{Binding Name, StringFormat=Pet name is {0:s}.}"/>
                    <TextBlock Text="{Binding Weight, StringFormat=Pet weight is {0:0.0} kg.}"/>
                    <TextBlock Text="抱っこするときには腰を痛めないよう気をつけましょう。"/>
                </StackPanel>
            </DataTemplate>
        </StackPanel.Resources>

        <!-- データ表示 -->
        <ContentControl Content="{Binding InfoToShow}">
            <ContentControl.ContentTemplateSelector>
                <local:PetTemplateSelector/>
            </ContentControl.ContentTemplateSelector>
        </ContentControl>
        <Button Content="Change dog info" Click="Button_Click_1"  FontSize="20" HorizontalAlignment="Center" />
    </StackPanel>
</Window>

リソースには、小型犬用と大型犬用の表示方法が定義されています。これらの違いは、最下部に表示されるメッセージです。

実際にデータを表示するContentControlでは、ContentTemplateSelectorとしてPetTemplateSelectorというC#のクラスを指定しています。 その前に置かれている「local」は、PetTemplateSelectorクラスが置かれている名前空間です。 このコードにより、ContentControlは、バインドされているInfoToShowプロパティを表示するとき、PetTemplateSelectorを呼び出して表示に使うDataTemplateを決定します。

テンプレート選択クラスPetTemplateSelector

選択ロジックは、DataTemplateSelectorクラスの派生クラスでなければなりません。ここでは体重により小型犬と大型犬を区別して異なるDataTemplateを使うようにすればよいので、以下のように書けます。

//! 表示用テンプレートを選ぶためのセレクタークラス。
public class PetTemplateSelector : DataTemplateSelector
{
    public override DataTemplate SelectTemplate(object item, DependencyObject container)
    {
        return ((MyPetInfo)item).Weight < 20.0)
            ? (DataTemplate)((FrameworkElement)container).FindResource("SmallDogInfoTemplate");
            : (DataTemplate)((FrameworkElement)container).FindResource("LargeDogTemplateName");
    }
}

SelectTemplate()の第1引数itemには、表示しようとしているオブジェクトが入っています。この例では、ContentControlがバインドしているInfoToShowプロパティの値です。 第2引数containerには、itemを表示しようとするコントロールが入っています。ここでは、ContentControlが表示用に作ったContentPresenterです。セレクターを作る立場からは、ContentControlと同じと考えて構わないと思います。 テンプレートを選ぶコード自体は、犬の体重が20kg未満か以上かで異なるテンプレートを返すだけの簡単なものです。テンプレートを取り出すコードが若干長いですが、キャストが多いだけで、実際には「containerが持つリソースから特定の名前のリソースを探して返す」というだけのものです。

サンプル

起動時には小型犬の情報を、画面上のボタンを押すと大型犬の情報を表示するサンプルプログラムを作りました。

f:id:imani-c:20220417154930p:plain
起動時の表示
f:id:imani-c:20220418104708p:plain
ボタン押下後の表示

コードビハインドを以下に掲載します。Xamlコードは、上記のとおりです。

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

namespace DataTemplateEtc
{
    public partial class MainWindow : Window, INotifyPropertyChanged
    {
        //! コンストラクタ。自身をDataContextを設定する。
        public MainWindow()
        {
            DataContext = this;
            InitializeComponent();
        }

        //! 表示情報。このプロパティの中身が表示される。
        public object InfoToShow { get; set; } = new MyPetInfo { Name = "ポチ", Weight = 8.5 };

        //! INotifyPropertyChangedの実装。
        public event PropertyChangedEventHandler? PropertyChanged;

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            // ペットの情報を表示情報にする。
            InfoToShow = new MyPetInfo { Name = "ダイスケ", Weight = 32.4 };

            // 表示情報が切り替えられたことを通知する。
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(InfoToShow)));
        }
    }

    //! 表示する情報を定義するクラス:ペット情報用
    public class MyPetInfo
    {
        public string? Name { get; set; }
        public double Weight { get; set; }
    }

    //! 表示用テンプレートを選ぶためのセレクタークラス。
    public class PetTemplateSelector : DataTemplateSelector
    {
        public override DataTemplate SelectTemplate(object item, DependencyObject container)
        {
            return ((MyPetInfo)item).Weight < 20.0
                ? (DataTemplate)((FrameworkElement)container).FindResource("SmallDogInfoTemplate")
                : (DataTemplate)((FrameworkElement)container).FindResource("LargeDogInfoTemplate");
        }
    }
}

上記コードのXaml文法エラー

上記コードをVisualStudio2022に入力すると、エラーが表示されます。

XDG0066 Object reference not set to an instance of an object.

このエラーに対処しなくても、ビルドできますし、動作にも支障がありません。しかし、エラーが残るのは気持ちが悪いものです。下記のように「d:ContentTemplate」を使って、デザイン時に利用するContentTemplateを追加で指定すると解消できます。

<ContentControl Content="{Binding InfoToShow}" d:ContentTemplate="{StaticResource SmallDogInfoTemplate}">