可样式化控件设计指南
本文档总结了一些设计控件时需要考虑的最佳实践,以便控件能够轻松地进行样式和模板化设置。 我们在处理内置 WPF 控件集的主题控件样式的同时,经过大量的试验和摸索,总结出这一组最佳做法。 我们了解到,成功的样式设置不仅依赖于样式本身,也取决于良好的对象模型设计。 本文档的目标受众是控件作者,而不是样式作者。
术语
“样式设置和模板化”指的是一套技术,让控件作者能够将控件的视觉表现委托给控件的样式和模板。 此技术套件包括:
样式(包括属性设置器、触发器和动画图板)。
资源。
控件模板。
数据模板。
有关样式设置和模板化的简介,请参阅 样式设置和模板化。
准备工作:了解控件
在跳转到这些准则之前,请务必了解并定义控件的常见用法。 样式设置公开一组通常不受约束的可能性。 旨在由许多开发人员在许多应用程序中广泛使用的控件面临着如下挑战:可以使用样式设置对控件的可视化外观进行广泛更改。 事实上,样式控件甚至可能完全不像控件作者所设想的那样。 由于样式设置提供的灵活性本质上是无限的,因此可以使用常见用法的想法来帮助你确定决策的范围。
若要了解控件的常见用法,最好考虑控件的价值主张。 你的控件能提供哪些其他控件无法提供的独特功能? 常见用法并不意味着任何特定的视觉外观,而是控件的理念和对其用法的合理期望集。 通过这种理解,你可以对组合模型和常见情况下控件的样式定义行为做出一些假设。 例如,对于 ComboBox,如果你了解其常见用法,并不意味着非常清楚特定的 ComboBox 是否有圆角,但是将由此清楚 ComboBox 可能需要一个弹出窗口和某种用来切换其开关状态的方法。
一般准则
不要严格执行模板协定。 控件的模板协定可能包含元素、命令、绑定、触发器,甚至是控件正常运行所需的或预期的属性设置。
尽可能少地减少合同。
围绕如下预期进行设计:在设计时(即,在使用设计工具时),控件模板通常处于不完整状态。 WPF 不提供“合成”状态基础架构,因此控件必须假设此类状态可能有效。
在没有遵循模板协定的任何方面时,不引发异常。 按照这一原则,当面板的子级太多或太少时,面板不应引发异常。
将外围功能纳入模板帮助器元素中。 每个控件应侧重于其核心功能和真正的价值主张,并由控件的常见用法定义。 为此,请在模板中使用组合和辅助元素来启用外围行为和可视化效果,即那些与控件核心功能无关的行为和可视化效果。 辅助元素分为三个类别:
独立帮助程序类型是以“匿名方式”用在模板中的可重用的公共控件或基元,这意味着帮助程序元素和带样式的控件无法互相识别。 从技术上讲,任何元素都可以是匿名类型,但在此上下文中,术语描述了封装专用功能以启用目标方案的类型。
基于类型的帮助程序元素是封装专用功能的新类型。 这些元素通常设计的功能范围比常见控件或基元要窄。 与独立的帮助程序元素不同,基于类型的帮助程序元素知道它们的使用上下文,并且通常必须与其所属模板的控件共享数据。
命名的帮助程序元素是控件应当能够在其模板中根据名称找到的常用控件或基元。 这些元素在模板中提供了一个已知的名称,使控件可以查找元素并以编程方式与之交互。 在任何模板中只能有一个具有给定名称的元素。
下表显示了当前控件样式使用的帮助元素(此列表并不详尽):
元素 类型 使用者 ContentPresenter 基于类型的 Button、CheckBox、RadioButton、Frame等(所有 ContentControl 类型) ItemsPresenter 基于类型的 ListBox、ComboBox、Menu等(所有 ItemsControl 类型) ToolBarOverflowPanel 命名的 ToolBar Popup 独立 ComboBox、ToolBar、Menu、ToolTip等 RepeatButton 命名的 Slider、ScrollBar等 ScrollBar 命名的 ScrollViewer ScrollViewer 独立 ListBox、ComboBox、Menu、Frame等 TabPanel 独立 TabControl TextBox 命名的 ComboBox TickBar 基于类型的 Slider 最小化帮助程序元素上所需的用户指定的绑定或属性设置。 帮助程序元素通常需要某些绑定或属性设置才能在控件模板中正常运行。 辅助元素和模板化控件应尽可能地设置这些配置。 设置属性或建立绑定时,应注意不要替代用户设置的值。 具体最佳做法如下:
命名的帮助程序元素应当由父级标识,而且父级应当针对帮助程序元素建立任何必需的设置。
基于类型的帮助程序元素应当直接针对自身建立任何必需的设置。 这样做可能需要帮助程序元素查找它在使用时的信息上下文,包括其
TemplatedParent
(它在使用时的模板的控件类型)。 例如,当用于 ContentControl 派生类型时,ContentPresenter 会自动将其TemplatedParent
的Content
属性绑定到其 Content 属性。独立的辅助元素无法以这种方式进行优化,因为根据定义,无论是辅助元素还是父元素都不了解另一个。
使用 Name 属性标记模板中的元素。 如果控件需要在样式中查找某个元素才能以编程方式访问它,则该控件应当使用
Name
属性和FindName
范例来进行查找。 当找不到元素时,控件不应引发异常,而是以无提示和正常方式禁用该元素所需的功能。使用最佳做法来表示样式中的控件状态和行为。 下面是用于在样式中表达控件状态更改和行为的最佳做法的有序列表。 你应该使用列表中能够实现你方案的第一项。
属性绑定。 示例:ComboBox.IsDropDownOpen 和 ToggleButton.IsChecked之间的绑定。
触发的属性更改或属性动画。 示例:Button 的悬停状态。
命令。 示例:ScrollBar中的 LineUpCommand / LineDownCommand。
独立辅助元素。 示例:TabControl中的 TabPanel。
基于类型的帮助程序类型。 示例:Button中的 ContentPresenter,Slider中的 TickBar。
命名的帮助程序类型中的冒泡事件。 如果侦听样式元素中的冒泡事件,应当要求生成该事件的元素能够进行唯一标识。 示例:ToolBar中的 Thumb。
自定义
OnRender
行为。 示例:Button中的 ButtonChrome。
谨慎使用样式触发器(而不是模板触发器)。 影响模板中元素属性的触发器必须在模板中声明。 影响控件上的属性的触发器(没有
TargetName
)可以在样式中声明,除非你知道更改模板还可能会损坏触发器。与现有样式模式保持一致。 很多时候,有多种方法可以解决问题。 请注意,并尽可能与现有的控件设计风格保持一致。 这对于派生自同一基类型的控件(例如,ContentControl、ItemsControl、RangeBase等)尤其重要。
在不重新模板化的情况下公开属性来启用常见自定义项方案。 WPF 不支持可插入/可自定义部件,因此控件用户只剩下两种自定义方法:直接设置属性或使用样式设置属性。 请记住,比较合适的做法是,设置数量有限的属性,使其面向极其常见的高优先级自定义项方案,否则的话,这些方案需要重新模板化。 下面是何时以及如何启用自定义方案的最佳做法:
极其常见的自定义项应当作为属性在控件上公开并由模板使用。
不太常见的(虽然不罕见)自定义项应作为附加属性公开,并由模板使用。
需要对已知但是极少见的自定义项重新模板化,这一点也是可接受的。
主题注意事项
主题样式应尽量在所有主题中保持一致的属性语义,但不作任何保证。 作为控件文档的一部分,控件应当具有一个描述其属性语义(即控件属性的“含义”)的文档。 例如,ComboBox 控件应定义 ComboBox中 Background 属性的含义。 控件的默认样式应尝试遵循该文档中在所有主题中定义的语义。 另一方面,控制用户应注意,属性语义可能会在不同的主题之间变化。 在某些情况下,给定属性在特定主题所需的视觉约束下可能无法表达。 (例如,经典主题没有一个边框,
Thickness
可以应用于多个控件。主题样式不需要在所有主题中具有一致的触发器语义。 由控件样式通过触发器或动画公开的行为可能因主题而异。 控件用户应注意,控件不一定采用相同的机制在所有主题中实现特定行为。 例如,一个主题可以使用动画来表达悬停行为,其中另一个主题使用触发器。 这可能会导致自定义控件上的行为保留出现不一致。 更改背景属性可能不会影响控件的悬停状态(如果该状态是使用触发器表示的)。然而,若悬停状态是通过动画实现的,改变背景属性可能无可挽回地中断动画,从而影响状态转换。
主题样式不需要在所有主题中具有一致的“布局”语义。 例如,默认样式不需要保证控件在所有主题中占用相同大小,或保证控件在所有主题中具有相同的内容边距/填充。