可样式化控件的设计准则Guidelines for Designing Stylable Controls

本文档概述在设计可方便地样式化和模板化的控件时需要考虑的一组最佳做法。This document summarizes a set of best practices to consider when designing a control which you intend to be easily stylable and templatable. 在为内置的 WPFWPF 控件集处理主题控件样式时,我们通过大量试验和错误总结出了这组最佳做法。We came to this set of best practices through a lot of trial and error while working on the theme control styles for the built-in WPFWPF control set. 我们已经认识到,成功的样式设置不只是设计完善的对象模型的功能,也是样式本身的功能。We learned that successful styling is as much a function of a well-designed object model as it is of the style itself. 本文档面向控件作者,而不是样式作者。The intended audience for this document is the control author, not the style author.


“样式设置和模板化”是一组技术,控件作者可以通过该组技术将控件的可视化特性延迟到控件的样式和模板。"Styling and templating" refer to the suite of technologies that enable a control author to defer the visual aspects of the control to the style and template of the control. 这组技术包括:This suite of technologies includes:

  • 样式(包括属性资源库、触发器和演示图板)。Styles (including property setters, triggers, and storyboards).

  • 资源。Resources.

  • 控件模板。Control templates.

  • 数据模板。Data templates.

有关样式设置和模板化简介,请参阅样式设置和模板化For an introduction to styling and templating, see Styling and Templating.

准备工作:了解控件Before You Start: Understanding Your Control

在开始阅读这些准则之前,请务必了解并定义了控件的常见用法。Before you jump into these guidelines, it is important to understand and have defined the common usage of your control. 样式设置公开一组通常不受约束的可能性。Styling exposes an often unruly set of possibilities. 旨在由许多开发人员在许多应用程序中广泛使用的控件面临着如下挑战:可以使用样式设置对控件的可视化外观进行广泛更改。Controls that are written to be used broadly (in many applications, by many developers) face the challenge that styling can be used to make far-reaching changes to the visual appearance of the control. 实际上,带样式的控件甚至可能并非控件作者的本意。In fact, the styled control may not even resemble the control author's intentions. 由于样式设置在本质上可以提供无限的灵活性,因此可以使用“常见用法”这一概念来帮助你限制自己的决定。Since the flexibility offered by styling is essentially boundless, you can use the idea of common usage to help you scope your decisions.

若要了解控件的常见用法,最好考虑控件的价值主张。To understand your control's common usage, it's good to think about the value proposition of the control. 你的控件能够在表中提供哪些无法由其他控件提供的内容?What does your control bring to the table that no other control can offer? 常见用法并不表示任何特定的可视化外观,而是表示控件的基本原理和一组有关其用法的合理预期。Common usage does not imply any specific visual appearance, but rather the philosophy of the control and a reasonable set of expectations about its usage. 了解到这一点,就可以对控件在一般情况下的撰写模型和样式定义行为进行一些假设。This understanding allows you to make some assumptions about the composition model and the style-defined behaviors of the control in the common case. 例如,在中 ComboBox ,了解常见用法不会给您提供有关特定是否 ComboBox 有圆角的任何信息,但它可让您深入了解 ComboBox 可能需要一个弹出窗口,并通过某种方式来确定其是否处于打开状态。In the case of ComboBox, for example, understanding the common usage won't give you any insight about whether a particular ComboBox has rounded corners, but it will give you insight into the fact that the ComboBox probably needs a pop-up window and some way of toggling whether it is open.

通用准则General Guidelines

  • 不严格实施模板协定。Do not strictly enforce template contracts. 控件的模板协定可能包含元素、命令、绑定和触发器,甚至还可以包含必需的或为了使控件正常工作而应当使用的属性设置。The template contract of a control might consist of elements, commands, bindings, triggers, or even property settings that are required or expected for a control to function properly.

    • 最大限度地减少协定。Minimize contracts as much as possible.

    • 围绕如下预期进行设计:在设计时(即,在使用设计工具时),控件模板通常处于不完整状态。Design around the expectation that during design time (that is, when using a design tool) it is common for a control template to be in an incomplete state. WPFWPF 不提供“正在撰写”状态的基础结构,因此,控件必须围绕这样的状态可能有效这一预期来生成。does not offer a "composing" state infrastructure, so controls have to be built with the expectation that such a state might be valid.

    • 在没有遵循模板协定的任何方面时,不引发异常。Do not throw exceptions when any aspect of a template contract is not followed. 按照这一原则,当面板的子级太多或太少时,面板不应引发异常。Along these lines, panels should not throw exceptions if they have too many or too few children.

  • 将外围功能分解成模板帮助程序元素。Factor peripheral functionality into template helper elements. 每个控件都应当将重点放在其核心功能和真正的价值主张上,而且每个控件都应当由控件的常见用法定义。Each control should be focused on its core functionality and true value proposition and defined by the control's common usage. 为此,请使用模板中的撰写和帮助程序元素实现外围行为和可视化(即,那些不构成控件核心功能的行为和可视化)。To that end, use composition and helper elements within the template to enable peripheral behaviors and visualizations, that is, those behaviors and visualizations that do not contribute to the core functionality of the control. 帮助程序元素分为三类:Helper elements fall into three categories:

    • 独立 帮助程序类型是以“匿名方式”用在模板中的可重用的公共控件或基元,这意味着帮助程序元素和带样式的控件无法互相识别。Standalone helper types are public and reusable controls or primitives that are used "anonymously" in a template, meaning that neither the helper element nor the styled control is aware of the other. 在技术上,任何元素都可以是匿名类型,但是在此上下文中,该术语描述了那些封装专用功能以实现目标方案的类型。Technically, any element can be an anonymous type, but in this context the term describes those types that encapsulate specialized functionality to enable targeted scenarios.

    • 基于类型的 帮助程序元素是封装专用功能的新类型。Type-based helper elements are new types that encapsulate specialized functionality. 通常,这些元素在设计上比通用控件或基元的功能范围要窄。These elements are typically designed with a narrower range of functionality than common controls or primitives. 与独立帮助程序元素不同的是,基于类型的帮助程序元素能够识别它们的使用上下文,而且通常必须与包含它们所属模板的控件共享数据。Unlike standalone helper elements, type-based helper elements are aware of the context in which they are used and typically must share data with the control to whose template they belong.

    • 命名的 帮助程序元素是控件应当能够在其模板中根据名称找到的常用控件或基元。Named helper elements are common controls or primitives that a control expects to find within its template by name. 这些元素在模板中具有一个已知的名称,这使得控件能够找到这些元素并以编程方式与之交互。These elements are given a well-known name within the template, making it possible for a control to find the element and interact with it programmatically. 在任何模板中,都只能有一个具有给定名称的元素。There can only be one element with a given name in any template.

    下表显示了由目前的控件样式使用的部分帮助程序元素列表:The following table shows helper elements employed by control styles today (this list is not exhaustive):

    元素Element 类型Type 使用者Used by
    ContentPresenter 基于类型的Type-based Button、、、等 CheckBox RadioButton Frame (所有 ContentControl 类型) Button, CheckBox, RadioButton, Frame, and so on (all ContentControl types)
    ItemsPresenter 基于类型的Type-based ListBox、、等 ComboBox Menu (所有 ItemsControl 类型) ListBox, ComboBox, Menu, and so on (all ItemsControl types)
    ToolBarOverflowPanel 名为Named ToolBar
    Popup 独立Standalone ComboBoxToolBar 、、等 Menu ToolTipComboBox, ToolBar, Menu, ToolTip, and so on
    RepeatButton 名为Named SliderScrollBarSlider, ScrollBar, and so on
    ScrollBar 名为Named ScrollViewer
    ScrollViewer 独立Standalone ListBoxComboBox 、、等 Menu FrameListBox, ComboBox, Menu, Frame, and so on
    TabPanel 独立Standalone TabControl
    TextBox 名为Named ComboBox
    TickBar 基于类型的Type-based Slider
  • 最大限度地减少帮助程序元素所必需的、特定于用户的绑定或属性设置Minimize required user-specified bindings or property settings on helper elements. 通常,帮助程序元素需要某些绑定或属性设置才能在控件模板中正确工作。It is common for a helper element to require certain bindings or property settings in order to function properly within the control template. 帮助程序元素和模板化控件应当尽可能多地生成这些设置。The helper element and templated control should, as much as possible, establish these settings. 在设置属性或者建立绑定时,注意不要重写由用户设置的值。When setting properties or establishing bindings, care should be taken to not override values set by the user. 具体的最佳做法如下所示:Specific best practices are as follows:

    • 命名的帮助程序元素应当由父级标识,而且父级应当针对帮助程序元素建立任何必需的设置。Named helper elements should be identified by the parent and the parent should establish any required settings on the helper element.

    • 基于类型的帮助程序元素应当直接针对自身建立任何必需的设置。Type-based helper elements should establish any required settings directly on themselves. 这样做可能需要帮助程序元素查找它在使用时的信息上下文,包括其 TemplatedParent(它在使用时的模板的控件类型)。Doing this may require the helper element to query for information context in which it is being used, including its TemplatedParent (the control type of the template in which it is being used). 例如, ContentPresenter Content TemplatedParent Content 在派生类型中使用时,会自动将其的属性绑定到其属性 ContentControlFor example, ContentPresenter automatically binds the Content property of its TemplatedParent to its Content property when used in a ContentControl derived type.

    • 独立帮助程序元素不能按这种方式进行优化,这是因为按照定义,帮助程序元素和父级不能相互识别。Standalone helper elements cannot be optimized in this way because, by definition, neither the helper element nor the parent knows about the other.

  • 使用 Name 属性标记模板中的元素Use the Name property to flag elements within a template. 如果控件需要在样式中查找某个元素才能以编程方式访问它,则该控件应当使用 Name 属性和 FindName 范例来进行查找。A control that needs to find an element in its style in order to access it programmatically should do so using the Name property and the FindName paradigm. 控件不应在未找到所需元素时引发异常,而是应在不提示的情况下禁用需要该元素的功能。A control should not throw an exception when an element is not found, but silently and gracefully disable the functionality which required that element.

  • 使用最佳做法来表示样式中的控件状态和行为。Use best practices for expressing control state and behavior in a style. 下面按顺序列出了用来表示样式中的控件状态更改和行为的最佳做法。The following is an ordered list of best practices for expressing control state changes and behavior in a style. 应使用列表上的第一项来实现你的方案。You should use the first item on the list that enables your scenario.

    1. 属性绑定。Property binding. 示例:在和之间绑定 ComboBox.IsDropDownOpen ToggleButton.IsCheckedExample: binding between ComboBox.IsDropDownOpen and ToggleButton.IsChecked.

    2. 触发的属性更改或属性动画。Triggered property changes or property animations. 示例:的悬停状态 ButtonExample: the hover state of a Button.

    3. 命令。Command. 例如: LineUpCommand / LineDownCommand 在中 ScrollBarExample: LineUpCommand / LineDownCommand in ScrollBar.

    4. 独立帮助程序元素。Standalone helper elements. 例如: TabPanel 在中 TabControlExample: TabPanel in TabControl.

    5. 基于类型的帮助程序类型。Type-based helper types. 例如: ContentPresenter 在中 ButtonTickBar 中为 SliderExample: ContentPresenter in Button, TickBar in Slider.

    6. 命名的帮助程序元素。Named helper elements. 例如: TextBox 在中 ComboBoxExample: TextBox in ComboBox.

    7. 命名的帮助程序类型中的冒泡事件。Bubbled events from named helper types. 如果侦听样式元素中的冒泡事件,应当要求生成该事件的元素能够进行唯一标识。If you listen for bubbled events from a style element, you should require that the element generating the event can be uniquely identified. 例如: Thumb 在中 ToolBarExample: Thumb in ToolBar.

    8. 自定义 OnRender 行为。Custom OnRender behavior. 例如: ButtonChrome 在中 ButtonExample: ButtonChrome in Button.

  • 慎用样式触发器(与模板触发器相对)Use style triggers (as opposed to template triggers) sparingly. 影响模板中元素上的属性的触发器必须在模板中声明。Triggers that affect properties on elements in the template must be declared in the template. 影响控件上的属性的触发器(没有 TargetName)可以在样式中声明,除非你知道更改模板还可能会损坏触发器。Triggers that affect properties on the control (no TargetName) may be declared in the style unless you know that changing the template should also destroy the trigger.

  • 与现有的样式设置模式保持一致。Be consistent with existing styling patterns. 一个问题常常有多种解决办法。Many times there are multiple ways to solve a problem. 注意尽可能与现有的控件样式设置模式保持一致。Be aware of and, when possible, consistent with existing control styling patterns. 这对于从同一基类型派生的控件尤其重要 (例如,、、 ContentControl ItemsControl RangeBase 等) 。This is especially important for controls that derive from the same base type (for example, ContentControl, ItemsControl, RangeBase, and so on).

  • 在不重新模板化的情况下公开属性来启用常见自定义项方案Expose properties to enable common customization scenarios without retemplating. WPFWPF 不支持可插入/可自定义的部件,因此控件用户只能使用两种自定义方法:直接设置属性或者使用样式设置属性。does not support pluggable/customizable parts, so a control user is left with only two methods of customization: setting properties directly or setting properties using styles. 请记住,比较合适的做法是,设置数量有限的属性,使其面向极其常见的高优先级自定义项方案,否则的话,这些方案需要重新模板化。With that in mind, it is appropriate to surface a limited number of properties targeted at very common, high-priority customization scenarios which would otherwise require the retemplating. 下面是有关何时以及如何启用自定义项方案的最佳方法:Here are best practices for when and how to enable customization scenarios:

    • 极其常见的自定义项应当作为属性在控件上公开并由模板使用。Very common customizations should be exposed as properties on the control and consumed by the template.

    • 不太常见(尽管并非极少见)的自定义项应当作为附加属性公开并由模板使用。Less common (though not rare) customizations should be exposed as attached properties and consumed by the template.

    • 需要对已知但是极少见的自定义项重新模板化,这一点也是可接受的。It is acceptable for known but rare customizations to require retemplating.

主题注意事项Theme Considerations

  • 主题样式应尝试在所有主题中具有一致的属性语义,但不保证能够实现这一点Theme styles should attempt to have consistent property semantics across all themes, but make no guarantee. 作为控件文档的一部分,控件应当具有一个描述其属性语义(即控件属性的“含义”)的文档。As part of its documentation, your control should have a document describing the control's property semantics, that is, the "meaning" of a property for a control. 例如, ComboBox 控件应 Background 在内定义属性的含义 ComboBoxFor example, the ComboBox control should define the meaning of the Background property within ComboBox. 控件的默认样式应当尝试遵循在其文档中的所有主题中定义的语义。The default styles for your control should attempt to follow the semantics defined in that document across all themes. 另一方面,控件用户应当注意属性语义可能因主题而异。Control users, on the other hand, should be aware that property semantics can change from theme to theme. 在某些情况下,给定的属性在由特定主题所需的可视化约束下可能无法表示。In certain cases, a given property may not be expressible under the visual constraints required by a particular theme. (例如,对于许多控件来说,传统主题没有可以向其应用 Thickness 的边框。)(The Classic theme, for example, does not have a single border to which Thickness can be applied for many controls.)

  • 主题样式不需要在所有主题中具有一致的触发器语义Theme styles do not need to have consistent trigger semantics across all themes. 由控件样式通过触发器或动画公开的行为可能因主题而异。The behavior exposed by a control style through triggers or animations may vary from theme to theme. 控件用户应当注意到,控件不必使用同一个机制在所有主题中实现特定的行为。Control users should be aware that a control will not necessarily employ the same mechanism to achieve a particular behavior across all themes. 例如,一个主题可以使用动画来表示悬停行为,而另一个主题则可以使用触发器。One theme, for example, may use an animation to express hover behavior where another theme uses a trigger. 这可能会导致自定义控件上的行为保留出现不一致。This can result in inconsistencies in behavior preservation on customized controls. (例如,如果控件的悬停状态使用触发器来表示,则更改背景属性可能不会影响该状态。(Changing the background property, for example, might not affect the hover state of the control if that state is expressed using a trigger. 但是,如果悬停状态使用动画来实现,则更改背景属性可能会不可挽回地中断动画,从而中断状态过渡。)However, if the hover state is implemented using an animation, changing to background could irreparably break the animation and therefore the state transition.)

  • 主题样式不需要在所有主题中具有一致的“布局”语义Theme styles do not need to have consistent "layout" semantics across all themes. 例如,默认的样式不需要保证控件将在所有主题中占用同样的大小,也不需要保证控件将在所有主题中具有同样的内容边距/空白。For example, the default style does not need to guarantee that a control will occupy the same amount of size in all themes or guarantee that a control will have the same content margins / padding across all themes.

请参阅See also