まめ - たんたんめん

備忘録 C# / WPF 多め

WPF DataTemplate.DataType にinterface を指定可能にするDataTemplateSelectorの実装

やりたいこと

ListViewやItemsControlでデータを並べる際に interface にってViewを切り替えたい。

ItemsControlのDataTemplate解決の仕組み

ListVIewやItemsControlが並べるデータのViewを指定する仕組みについて簡単に解説します。 ItemsControlではItemsSourceに含まれるデータの型によってViewを選択します。 型が指定されており、Keyが指定されていないResourceが対象となります。(<DataTemaplte TargetType = "{x:type hoge}">みたいなやつ)

ItemsControlなどではデータ型に一致するDataTemplateを検索します。この検索では基底クラスの検索も含みます。検索順はVisualTreeを上方向に辿って行って最終的にはApplication.Resourceの中まで検索します。 検索が失敗し見つからなかった場合は DataのToString()を画面上に表示させます。

今回の実装アプローチ

ItemsControlのDetaTemplateを解決する仕組みに乗っ取りつつinterfaceも検索対象とするようにします。( 優先度は 基底クラスよりは上げておきます。)

実装

標準的なDataTemplateSelectorの動きをしつつ DataTypeに interface が指定されている場合にResourceの検索対処いうを拡張します。 下記にコードを示します。

   /// <summary>
    /// interface が アサイン可能なテンプレートを選びます。
    /// </summary>
    public class InterfaceTemplateSelector : DataTemplateSelector
    {
        private static DataTemplate NotFoundDataTemplate;

        private DataTemplate TryGetNotFoundDataTemplate()
        {
            if (NotFoundDataTemplate is null)
            {
                NotFoundDataTemplate = new DataTemplate();

                var visualTree = new FrameworkElementFactory(typeof(TextBlock));
                visualTree.SetBinding(TextBlock.TextProperty , new Binding());
                
                NotFoundDataTemplate.VisualTree = visualTree;
            }

            return NotFoundDataTemplate;
        }
        
        
        public override DataTemplate SelectTemplate(object item, DependencyObject container)
        {
            var c = (FrameworkElement) container;
            
            if(item is null)
                return TryGetNotFoundDataTemplate();
            
            var interfaces = item.GetType().GetInterfaces();
            
            // step 1 実タイプと一致するデータテンプレートを見つけた場合
            var type = item.GetType();
            {
                if (c.TryFindResource(new DataTemplateKey(type)) is DataTemplate template)
                {
                    return template;
                }                
            }

            // step 2 インターフェースに一致するテンプレートを検索する
            foreach (var @interface in interfaces)
            {
                if (c.TryFindResource(new DataTemplateKey(@interface)) is DataTemplate template)
                {
                    return template;
                }
            }

            // step 3 基底タイプを検索し一致するテンプレートを検索する
            while (type.BaseType != null && type.BaseType != typeof(System.Object))
            {
                type = type.BaseType;
                if (c.TryFindResource(new DataTemplateKey(type)) is DataTemplate template)
                {
                    return template;
                }
            }
            
            return TryGetNotFoundDataTemplate();
        }
    }

MarkupExtensionを用意しておくとすっきり書けます。

    public class InterfaceTemplateSelectorExtension : System.Windows.Markup.MarkupExtension
    {
        public override object ProvideValue(IServiceProvider serviceProvider)
        {
            return new InterfaceTemplateSelector();
        }
    }

実際の使用例です。 インターフェース

    public interface IHoldClipDataContext
    {
        double StartValue { get; set; }
        double EndValue { get; set; }
    }

    public interface ITriggerClipDataContext
    {
        double Value { get; set; }
    }

ViewModel

    public class TriggerClipVm : Notification, ITriggerClipDataContext
    {
        public double Value { get; set; }
    }

    public class HoldClipVm : Notification, IHoldClipDataContext
    {
        public double StartValue { get; set; }
        public double EndValue { get; set; }
    }

xaml( 定義 )

    <DataTemplate DataType="{x:Type timelineControl:IHoldClipDataContext}">
        <timelineControl:HoldClip
            UseLayoutRounding="False"
            Scale="{Binding Scale , RelativeSource={RelativeSource AncestorType=timelineControl:TsTimeline}}"
            StartValue="{Binding StartValue}"
            EndValue="{Binding EndValue}"
            Width="{Binding CanvasActualWidth, RelativeSource={RelativeSource AncestorType=timelineControl:TsTimeline}}"
            Height="{Binding TrackHeight , RelativeSource={RelativeSource AncestorType=timelineControl:TsTimeline}}"
            Background="LightSlateGray" ClipToBounds="False" />
    </DataTemplate>

    <DataTemplate DataType="{x:Type timelineControl:ITriggerClipDataContext}">
        <timelineControl:TriggerClip
            UseLayoutRounding="False"
            Scale="{Binding Scale , RelativeSource={RelativeSource AncestorType=timelineControl:TsTimeline}}"
            Value="{Binding Value}"
            Width="{Binding CanvasActualWidth, RelativeSource={RelativeSource AncestorType=timelineControl:TsTimeline}}"
            Height="{Binding TrackHeight , RelativeSource={RelativeSource AncestorType=timelineControl:TsTimeline}}"
            ClipToBounds="False" />
    </DataTemplate>

xaml ( DataTempalteSelectorの指定 )

<ItemsControl ItemsSource="{Binding Clips}"
   ItemTemplateSelector="{timelineControl:InterfaceTemplateSelector}">

結果です。 TriggerClipは赤色、HoldClipはオレンジ色です。 interfaceによってViewが切り替わっているのが確認できました。 f:id:at12k313:20200409172427p:plain