HierarchicalDataTemplateと固定の項目

2025年1月7日火曜日

C# WPF

記事のカテゴリー: C#、.NET 9、WPF

TreeView、Menu、ContextMenuなど、階層的なデータを表示するときに使用するHierarchicalDataTemplateですが、メインの階層的なデータとは別に固定の項目を表示する方法を考えます。例えば、お気に入りを表示するメニューに「お気に入りに追加する」という項目を追加する、みたいな話です。

1. スタイルセレクターを作る

まず、それぞれの項目にテンプレートを設定するためのスタイルセレクターを作ります。今回はセパレーター(メニュー項目の区切り)を使うためにスタイルセレクターを採用しますが、セパレーターが必要なければDataTemplateSelectorでいいかもしれません。

C#:

public class FavoriteStyleSelector : StyleSelector
{
    // 「お気に入りに追加する」項目を表す文字列
    public const string AddFavorite = "AddFavorite";

    // セパレーターを表す文字列
    public const string Separator = "Separator";

    // それぞれの項目に設定するスタイル
    public Style? AddFavoriteStyle { get; set; }
    public Style? SeparatorStyle { get; set; }
    public Style? FavoriteStyle { get; set; }

    public override Style SelectStyle(object item, DependencyObject container)
    {
        return item select
        {
            // 「お気に入りに追加する」項目の場合
            AddFavorite => AddFavoriteStyle ?? throw new InvalidOperationException(),            
            // セパレーターの場合
            Separator => SeparatorStyle ?? throw new InvalidOperationException(),            
            // お気に入り (Favorite クラス) の場合
            Favorite => FavoriteStyle ?? throw new InvalidOperationException(),
            _ => base.SelectStyle(item, container);
        };
    }
}

2. コンバーターを作る

マルチバインディングを使ってお気に入りと固定の項目を1つのコレクションにまとめようと思います。そこでマルチバインディング用のコンバーターを作ります。

ちなみに、XAMLで様々な要素を合わせたコレクションを定義できるCompositeCollectionを使うことも検討したのですが、お気に入りを動的に生成したときに不都合が出たので採用をやめました。

C#:

public class FlattenConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        return values.SelectMany((value) =>
        {
            return (value is IEnumerable valueAsEnum) ? valueAsEnum.Cast<object>() : [];
        }).ToArray();
    }

    [return: MaybeNull]
    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
        return null;
    }
}

3. XAMLでスタイルを定義

FavoriteStyleSelector、FlattenConverterを使ってメニューにお気に入りと固定の項目を表示します。

まず、マルチバインディングでお気に入りと固定の項目を1つのコレクションにまとめて、メニュー項目のItemsSourceプロパティに設定します。そして、同じメニュー項目のItemContainerStyleSelectorプロパティにFavoriteStyleSelectorを設定して、お気に入りに追加、セパレーター、お気に入りのそれぞれに対応するスタイルを定義します。例では省きましたが、コマンドなどもこのスタイルでバインドすることになります。

XAML:

<Window
    ...
    xmlns:system="clr-namespace:System;assembly=mscorlib"
    xmlns:local="clr-namespace:WpfApp1"
    ...>
    

XAML:

<MenuItem>
    <MenuItem.ItemsSource>
        <!-- お気に入りと固定の項目をまとめる -->
        <MultiBinding>
            <Binding>
                <Binding.Source>
                    <x:Array Type="system:Object">
                        <x:Static Member="local:FavoriteStyleSelector.AddFavorite" />
                        <x:Static Member="local:FavoriteStyleSelector.Separator" />
                    </x:Array>
                </Binding.Source>
            </Binding>
            <Binding Path="Favorites" />
        </MultiBinding>
    </MenuItem.ItemsSource>
    <MenuItem.ItemContainerStyleSelector>
        <local:FavoriteStyleSelector>
            <!-- 「お気に入りに追加する」項目のスタイル -->
            <local:FavoriteStyleSelector.AddFavorite>
                <Style TargetType="MenuItem">
                    <Setter Property="Header" Value="お気に入りに追加する" />
                </Style>
            </local:FavoriteStyleSelector.AddFavoriteStyle>
            <!-- セパレーターのスタイル -->
            <local:FavoriteStyleSelector.SeparatorStyle>
                <Style TargetType="MenuItem">
                    <Setter Property="Template">
                        <Setter.Value>
                            <ControlTemplate>
                                <Separator
                                    Style="{DynamicResource {x:Static MenuItem.SeparatorStyleKey}}" />
                            </ControlTemplate>
                        </Setter.Value>
                    </Setter>
                </Style>
            </local:FavoriteStyleSelector.SeparatorStyle>
            <!-- お気に入りのスタイル -->
            <local:FavoriteStyleSelector.FavoriteStyle>
                <Style TargetType="MenuItem">
                    <Setter Property="HeaderTemplate">
                        <Setter.Value>
                            <HierarchicalDataTemplate 
                                DataType="local:Favorite" 
                                ItemsSource="{Binding Items}">
                                <TextBlock Text="{Binding Name}" />
                            </HierarchicalDataTemplate>
                        </Setter.Value>
                    </Setter>
                </Style>
            </local:FavoriteStyleSelector.FavoriteOperationStyle>
        </local:FavoriteStyleSelector>
    </MenuItem.ItemContainerStyleSelector>
</MenuItem>