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が切り替わっているのが確認できました。