上一篇談了普遍的 DataTemplate Selector 實作,這一篇聊點進階的玩意。
從上一篇延伸來的問題,如果在專案裡面有許多類似的情況,每次都要建立一個特定的 DataTemplate Selector 真的太麻煩,能不能畢其功於一役,撰寫一個適用範圍能夠擴大的 DataTemplate Selector;這正是這篇要展示的做法。
從前一篇可以想見是靠著 ResourceDictionary 的 key 來選擇所對應的 DataTamplate,既然那個 key 不過就是個字串,那何不把這個字串放在 View Model 身上。我採取的作法是將這個 key 利用 attribute 掛在 View Model 的型別宣告。基於懶惰的理由,我就不再另外設計 attribute class,而是直接利用現有的 DescriptionAttribute 的 Description 屬性儲存這個 key 值。
這個做法的另外一個事情就是將 DataTemplate 單獨放在一個 xaml 檔案中,所以我們會建立一個 Resource Dictionary (資源字典) file,內容如下:
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<DataTemplate x:Key="personTemplate">
<StackPanel Orientation="Horizontal" Margin="6">
<TextBlock Text="Name : "/>
<TextBlock Text="{Binding Name}"/>
<TextBlock Text="Age : " Margin="6,0,0,0"/>
<TextBlock Text="{Binding Age}"/>
</StackPanel>
</DataTemplate>
<DataTemplate x:Key="carTemplate">
<Border Margin="4">
<Border.Background >
<SolidColorBrush Color="{Binding Color}"/>
</Border.Background>
<StackPanel Margin="2">
<TextBlock Text="Brand"/>
<TextBlock Text="{Binding Brand}"/>
</StackPanel>
</Border>
</DataTemplate>
</ResourceDictionary>
[Description("personTemplate")]
public class Person : NotifyPropertyBase
{
private string _name;
public string Name
{
get => _name;
set => SetProperty(ref _name, value);
}
private int _age;
public int Age
{
get => _age;
set => SetProperty(ref _age, value);
}
}
[Description ("carTemplate")]
public class Car : NotifyPropertyBase
{
private string _brand;
public string Brand
{
get => _brand;
set => SetProperty(ref _brand, value);
}
private Color _color;
public Color Color
{
get => _color;
set => SetProperty(ref _color , value);
}
}
接著就是建立 DataTemplate Selector,這邊會在這個類別加上一個 Path 屬性,這個屬性的用途是為了指示 DataTemplate 所在的檔案來源。內部藉由反射取得 item 參數所屬型別的 DescriptionAttribute 中的 Description 屬性值來對應 Resource Dictionary 中的 key。
public class UniversalTemplateSelector : DataTemplateSelector
{
public string Path { get; set; }
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
var source = new ResourceDictionary { Source = new Uri(Path, UriKind.RelativeOrAbsolute) };
var attribute = item?.GetType().GetCustomAttribute<DescriptionAttribute>();
if (attribute != null && source.Contains(attribute.Description))
{
return source[attribute.Description] as DataTemplate;
}
return null;
}
}
在 View 上只要設定好這個 selector 的 Path,就可以直接使用了,這個作法能夠更靈活地抽換 DataTemplate。
<Window x:Class="DataTemplateSelectorSample002.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:vm="clr-namespace:DataTemplateSelectorSample002.ViewModels"
xmlns:local="clr-namespace:DataTemplateSelectorSample002"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.DataContext >
<vm:MainViewModel />
</Window.DataContext>
<Window.Resources >
<local:UniversalTemplateSelector Path="/Templates/TemplatesDictionary.xaml" x:Key="selector"/>
</Window.Resources>
<ListBox HorizontalContentAlignment="Stretch" ItemsSource="{Binding Data}" ItemTemplateSelector="{StaticResource selector}"/>
</Window>
詳細的範例程式碼可以點選此處。