WPF 中的路由模板选择






4.85/5 (23投票s)
探讨一种用于实现 DataTemplate 选择逻辑的强大技术。
引言
本文介绍了一个类,该类允许您将模板选择逻辑移出 DataTemplateSelector
的子类。使用此技术,您可以将 DataTemplate
资源键的知识封装到实际包含这些资源的区域。它还使得实现需要比 DataTemplateSelector
子类通常可用的应用程序状态更多的模板选择逻辑变得更加容易。此技术可以大大减少 Windows Presentation Foundation (WPF) 应用程序中的模板选择器类的数量,从而使其更易于扩展和维护。
背景
WPF 控件通常提供一种以编程方式选择 DataTemplate
来渲染数据对象的方法。此功能通过名称以“TemplateSelector
”后缀的属性暴露。例如,这包括 ContentControl
的 ContentTemplateSelector
属性,以及 ItemsControl
的 ItemTemplateSelector
。模板选择器是继承自 DataTemplateSelector
并重写 SelectTemplate
方法的类。
问题所在
通常,“模板选择器”类最终会包含硬编码的资源键,例如
public class MyTemplateSelector : DataTemplateSelector
{
public override DataTemplate SelectTemplate(
object item, DependencyObject container )
{
FrameworkElement elem = container as FrameworkElement;
Foo foo = item as Foo;
if( foo.Name == "Cowabunga" )
return elem.FindResource( "SomeDataTemplate" );
else
return elem.FindResource( "SomeOtherDataTemplate" );
}
}
这并不总是实现此类逻辑的理想方式。DataTemplateSelector
不能包含自己的资源,因此它引用的 DataTemplate
始终定义在某个其他元素的 Resources
集合中。在模板选择器中引用模板会复制资源键的知识。复制信息通常是一种糟糕的做法。如果更改了 DataTemplate
的资源键、引入了新模板或删除了现有模板,则必须相应地更新模板选择器类。最好是模板选择器不依赖于特定的资源键,这样它们就不会与使用它们的元素紧密耦合。
模板选择器并不总是实现某些类型模板选择逻辑的理想位置。在某些情况下,需要了解用户界面 (UI) 中其他元素的状态才能确定使用哪个模板。在模板选择器的 SelectTemplate
方法中执行的代码无法直接看到 UI 的其他部分。模板选择逻辑有时需要知道的比模板选择器本身所能知道的更多。
解决方案
我解决此问题的方法是让模板选择器将其任务委托给应用程序中更擅长确定使用哪个 DataTemplate
的其他部分。我创建了 RoutedDataTemplateSelector
类来实现这一目标。基本思想是,当需要选择 DataTemplate
时,RoutedDataTemplateSelector
会将事件冒泡到元素树,从需要模板的元素开始。处理该事件的任何人都将能够确定要使用的模板。
使用 RoutedDataTemplateSelector
可以防止大量 DataTemplateSelector
子类出现,每个子类都带有硬编码的资源键。相反,您可以将模板选择逻辑嵌入到同时包含要选择的 DataTemplate
和被模板化的元素的 Window
/Page
/UserControl
中。使用此方法的最终结果是,更改元素的资源不会对代码库产生很大的连锁反应,并且您的模板选择逻辑将有更多的运行时上下文可以使用。
使用 RoutedDataTemplateSelector
假设我们使用 ItemsControl
来显示 Person
对象列表,并且我们希望列表中的项显示交替的背景颜色。我们可以通过将两个 DataTemplate
应用于列表中的项来实现此目的,为每个连续的项切换模板。一个模板渲染带有一种颜色的项,另一个模板渲染带有不同颜色的项。它看起来可能像这样
正如在下面的摘要示例中所见,我们可以轻松地使用 RoutedDataTemplateSelector
实现此功能。
<Window ... >
<Window.Resources>
<DataTemplate x:Key="PersonTemplateEven">
<Border ... >
<TextBlock Text="{Binding Path=Name}" Background="LightBlue" />
</Border>
</DataTemplate>
<DataTemplate x:Key="PersonTemplateOdd">
<Border ... >
<TextBlock Text="{Binding Path=Name}" Background="WhiteSmoke" />
</Border>
</DataTemplate>
<jas:RoutedDataTemplateSelector x:Key="PersonTemplateSelector" />
</Window.Resources>
<Grid>
<ItemsControl
x:Name="personList"
HorizontalContentAlignment="Stretch"
ItemsSource="{Binding}"
ItemTemplateSelector="{StaticResource PersonTemplateSelector}"
Margin="3"
jas:RoutedDataTemplateSelector.TemplateRequested="OnTemplateRequested"
/>
</Grid>
</Window>
上面看到的 ItemsControl
标记使用了“附加事件”语法来指定当 RoutedDataTemplateSelector
的 TemplateRequested
路由事件在其上引发时应调用哪个方法。确定将 DataTemplate
应用于 Person
对象的该方法位于 Window
的代码隐藏文件中,如下所示
void OnTemplateRequested( object sender, TemplateRequestedEventArgs e )
{
// Get a reference to the Person object being templated.
Person person = e.DataObject as Person;
// This is one way to create "alternate row colors" in an ItemsControl.
ItemContainerGenerator generator = this.personList.ItemContainerGenerator;
DependencyObject container = generator.ContainerFromItem( person );
int visibleIndex = generator.IndexFromContainer( container );
string templateKey =
visibleIndex % 2 == 0 ?
"PersonTemplateEven" :
"PersonTemplateOdd";
// Specify the data template which should be used to render
// the Person object.
e.TemplateToUse = this.FindResource( templateKey ) as DataTemplate;
// Mark the event as "handled" so that it stops bubbling up
// the element tree.
e.Handled = true;
}
正如这个示例所示,选择使用哪个模板的逻辑位于包含被模板化的 ItemsControl
的 Window
中。这使得模板资源键只被它们所属的 Window
所知晓,并且可以轻松确定项在控件中的索引。如果此逻辑位于模板选择器中,那么它将很脆弱,并且更难确定项在控件中的索引。
工作原理
RoutedDataTemplateSelector
是一个不太复杂的类。它是 DataTemplateSelector
的一个子类,它公开了一个名为 TemplateRequested
的冒泡路由事件。当调用重写的 SelectTemplate
方法时,它会在要模板化的元素上引发该事件,并期望其逻辑树中的一个祖先指定要返回的 DataTemplate
。该类如下所示
public class RoutedDataTemplateSelector : DataTemplateSelector
{
/// <summary>
/// Represents the TemplateRequested bubbling routed event.
/// </summary>
public static readonly RoutedEvent TemplateRequestedEvent =
EventManager.RegisterRoutedEvent(
"TemplateRequested",
RoutingStrategy.Bubble,
typeof( TemplateRequestedEventHandler ),
typeof( RoutedDataTemplateSelector ) );
// This event declaration is only here so that the compiler allows
// the TemplateRequested event to be assigned a handler in XAML.
// Since DataTemplateSelector does not derive from UIElement it
// does not have the AddHandler/RemoveHandler methods typically
// used within an explicit event declaration.
[EditorBrowsable( EditorBrowsableState.Never )]
public event TemplateRequestedEventHandler TemplateRequested
{
add
{
throw new InvalidOperationException(
"Do not directly hook the TemplateRequested event." );
}
remove
{
throw new InvalidOperationException(
"Do not directly unhook the TemplateRequested event." );
}
}
/// <summary>
/// Raises the TemplateRequested event up the 'container' element's logical
/// tree so that the DataTemplate to return can be determined.
/// </summary>
/// <param name="item">The data object being templated.</param>
/// <param name="container">The element which contains the data.</param>
/// <returns>The DataTemplate to apply.</returns>
public override DataTemplate SelectTemplate(
object item, DependencyObject container )
{
// We need 'container' to be a UIElement because that class
// exposes the RaiseEvent method.
UIElement templatedElement = container as UIElement;
if( templatedElement == null )
throw new ArgumentException(
"RoutedDataTemplateSelector only works with UIElements." );
// Bubble the TemplateRequested event up the logical tree, starting at the
// templated element. This allows others to determine what template to use.
TemplateRequestedEventArgs args =
new TemplateRequestedEventArgs(
TemplateRequestedEvent, templatedElement, item );
templatedElement.RaiseEvent( args );
// Return the DataTemplate selected by the outside world.
return args.TemplateToUse;
}
}
该类的奇怪之处在于,它公开了一个用于 TemplateRequested
路由事件的 CLR 包装器事件,但使用它会导致异常。该包装器事件声明是为了使编译器在 XAML 中尝试为 TemplateRequested
分配处理程序时不会报告错误。由于 DataTemplateSelector
不继承自 UIElement
,因此它没有通常用于管理路由事件的 AddHandler
和 RemoveHandler
方法。实际情况是,如果执行此代码,将抛出异常
// This does not work!
RoutedDataTemplateSelector selector = new RoutedDataTemplateSelector();
selector.TemplateRequested += this.OnTemplateRequested;
相反,您应该使用这种方法
someElement.AddHandler(
RoutedDataTemplateSelector.TemplateRequestedEvent,
new TemplateRequestedEventHandler( this.OnTemplateRequested ) );
历史
- 2007 年 5 月 13 日 – 创建文章