原文:《Programming WPF》翻译 第9章 4.模板
对一个自定义元素最后的设计考虑是,它是如何连接其可视化的。如果一个元素直接从FrameworkElement中派生,这将会适当的生成它自己的可视化。(第7章描述了如何创建一个图形外观。)尤其是,如果你创建了一个元素,是为了提供一个特定的可视化表现,该元素应该完全控制这个可视化是如何管理的,一旦你编写了一个控件,通常你不会将一个图形硬编码到里面。
记住,一个控件的工作是提供行为。可视化是由控件模板提供的。这种可视化是由控件模板提供的。一个控件可能提供一组默认的可视化,而应允许这些可视化被替换,为了提供像内迁控件一样的弹性。(第五章描述了如何使用模板替换一个控件的可视化)符合这种方法的控件,这里可视化从控件中分离出来,通常引用到一个没有外观的控件。所有内迁到WPF的控件都是没有外观的。
当然,控件完全独立于其可视化是不可能的。任何控件将对模板必须满足的需求施加影响,如果控件操作正确。这些需求的程度随控件不同而不同。例如,Button有一个相当简单的需求——仅仅需要一个占位符放置标题或内容。Slider控件需要更广泛的需求:可视化必须提供两个按钮(增加和减少),“Thumb”,以及运行时Thumb上的一个跟踪。此外,它还需要能够响应点击和拖动在这些元素的任意一个,以及能够定位这个Thumb。
在任意控件类型和样式或模板之间有一个隐式的约定。这个控件允许它的外观通过替换可视化树的方式进行自定义,但是这棵树必须轮流提供代表这棵树的某些特征。这个约定的本性依赖于这个控件,内嵌控件使用一些不同的样式,紧紧依赖于它们的可视化结构。下面的部分描述了很多将控件与其模板联系在一起的方式
9.4.1属性别名
控件和模板间最松散的约定形式是控件简单的定义了公有属性,以及允许模板来决定哪一个属性在别名中可见。(参见第5章获取更多属性别名的信息。)这个控件并不关心
在控件中是什么。
这里有一个单行的约定:控件提供属性和命令,不需要返回值。尽管如此,如果必要的话,这样的控件仍能响应用户输入。事件路由允许事件从可视化向上冒泡到控件。控件能够处理这些事件而不需要知道任何关于可视化本性的信息。
为了支持这个模型,你所要做的是,使用本章先前描述的依赖属性机制,来实现这些属性。示例9-11显示了一个自定义控件,并且定义了一个单独的名为Foo的依赖属性,Brush类型。
依赖属性支持这个控件的用户在模板中提及,正如示例9-12所示。
示例9-12
<Grid>
<Rectangle Fill="{TemplateBinding Foo}" />
</Grid>
</ControlTemplate>
所有的依赖属性自动支持属性别名。这种情形下的“约定”是由一组你的控件提供的依赖属性暗示的。
9.4.2占位符
一些控件希望在模板中找到一个特定的占位符元素。这将要么采取该元素指定类型的形式,或者可以是一个元素标记了一个特定的属性。
控件通过派生于ContentControl支持内容模板,使用元素类型的方法。它们希望在模板中找到一个ContentPresenter元素。这是一个特殊意图的元素,它的工作是在其他内容中担当一个占位符。
实际上,这是一个松散的强迫性的约定。如果模板中没有ContentPresenter,ContentControl通常不会申诉。控件并不绝对依赖于表现的内容,为了放在那里起作用。或者你能到达另一个极端,以及放一些ContentPresenter在你的模板中,可以使子内容多次出现。
你不需要做任何特殊的事情来支持ContentPresenter的使用,只要你派生于ContentControl,它可以很好的工作。控件的用户能够编写一个模板,正如示例9-13所示。
示例9-13
<Grid>
<Rectangle Fill="White" />
<ContentPresenter />
</Grid>
</ControlTemplate>
9.4.3通过属性指定占位符
一些控件寻找用一个特定属性标记的元素。例如,派生于ItemsControl的控件,如ListBox和MenuItem,希望模板包括一个带有Panel.IsItemsHost属性设为true的元素。这标志了Panel将要扮演控件数据项目的宿主。ItemCOntrol使用附属属性取代占位符的原因是,允许你决定使用什么类型的Panel,作为数据项的宿主。(ItemControl还支持ItemsPresenter占位符元素的使用。这将使用于当样式不希望利用特定的panel类型的时候以及想要使用无论控件的默认panel是什么的时候)
为了实现使用此技术的控件,你需要定义一个自定义附属依赖属性,将其应用到占位符。这是一个Boolean属性。示例9-14注册了这样一个附属属性,并定义了通常的访问器功能。
示例9-14
public static DependencyProperty IsMyPlaceholderProperty;
static ControlWithPlaceholder( ) {
PropertyMetadata
isMyPlaceholderMetadata = new PropertyMetadata(false,
new PropertyInvalidatedCallback
(OnIsMyPlaceholderChanged));
IsMyPlaceholderProperty = DependencyProperty.RegisterAttached(
"IsMyPlaceholder", typeof(bool),
typeof(ControlWithPlaceholder), isMyPlaceholderMetadata);
}
public static bool GetIsMyPlaceholder(DependencyObject target) {
return (bool) target.GetValue(IsMyPlaceholderProperty);
}
public static void SetIsMyPlaceholder(DependencyObject target, bool value) {
target.SetValue(IsMyPlaceholderProperty, value);
}
注意到示例9-14为PropertyMetaData提供了一个PropertyInvalidatedCallBack。这指示了一个可以在任意时间调用的方法,这个附属属性可以在任意元素上被设置或修改。在这种方法中,我们的控件将发现哪个元素被设置为占位符,示例9-15显示了这个方法。
示例9-15
private static void OnIsMyPlaceholderChanged(DependencyObject target) {
FrameworkElement targetElement = target as FrameworkElement;
if (targetElement != null && GetIsMyPlaceholder(targetElement)) {
ControlWithPlaceholder containingControl =
targetElement.TemplatedParent as ControlWithPlaceholder;
if (containingControl != null) {
containingControl.placeholder = targetElement;
}
}
}
private FrameworkElement placeholder;
}
这个示例开始于检测属性被应用到派生于FrameworkElement的对象。记住我们希望这个属性会被应用到一个特定的控件模板内的UI元素,因此如果被应用到别的元素而不是FrameworkElement,我们这么做就得不到什么有用的东西。
其次,我们通过GetIsMyPlaceholder访问器方法检测了属性值,该方法是我们在示例9-14为附属属性定义的。这将是些微单独的,如果有人显示的设置这个属性为false,但是如果确实是这样,我们干脆不应该把元素作为占位符。
如果这个属性设置为true,我们继续获取目标元素的TemplatedParent属性。因为元素作为控件的模板一部分,这将返回可视化所属于的控件。(如果这个元素不是控件的成员,那么返回null。既然这个属性仅仅对模板中的元素有意义,如果没有模板化的父一级,我们就做不了任何事情。)我们还检查了父一级是一个控件类型的一个实例,而且忽略了属性,如果被应用到一个模板中的元素,在某种其它类型的控件模板中。
示例9-16显示了如何在一个控件模板中使用属性,来表明是哪一个元素在占位符中。
示例9-16
<Grid local:ControlWithPlaceholder.IsMyPlaceholder="true" />
</ControlTemplate>
一些控件希望有一种模板,提供一组详细明确的元素,来履行特定的角色在控件的标签中。例如,HorizontalSlider控件希望模板包含表示可拖动thumb的元素,这个可点击的跟踪,在thumb的任意一边,等等。模板需要指出哪一个元素是哪一个。这可以通过使用上述显示的技术,定义多个附属属性来实现。
当你写一个使用了占位符的控件时,你可能选择不执行这个约定。例如,如果模板的任意部分不见了,slider控件不会抱怨。一旦你只提供了要寻找的一些元素,这可以工作而不用抱怨。