2019 年 5 月

第 34 卷,第 5 期

[XAML]

自定义 XAML 控件

作者 Jerry Nixon

作为企业开发人员,你了解使用 SQL Server 的方法。也了解 .NET Web 服务。而且,对你而言,(或许使用 Windows Presentation Foundation [WPF])设计美观的 XAML 界面再简单不过。像成千上万的其他职业开发人员一样,Microsoft 技术为你的简历贴上新的一页,并且你可以将本文等 MSDN 杂志文章剪辑并固定到你的看板上。本文是获取工具的筹码。

现在即可使用 XAML 控件,提升你的专业知识。XAML 框架为 UI 开发提供了丰富的控件库,但要完成所需的操作,需要借助更多工具。本文将介绍如何使用 XAML 自定义控件实现所需的结果。

自定义控件

在 XAML 中创建自定义控件有两种方法:用户控件和模板化控件。用户控件是一种简单且易于设计的方法,用于创建可重用布局。模板化控件提供更为灵活的布局,且面向开发人员提供可自定义的 API。与使用任何语言的情况一样,复杂的布局可以产生数千行 XAML,难以高效地导航。自定义控件是用于减少布局代码的有效策略。

选择正确的方法将影响在应用程序中使用和重用控件的成功程度。以下是帮助入门的一些注意事项。

简便性。容易并不总是简单,但简单总是很容易。用户控件既操作简单又易于使用。几乎不需要参阅文档,任何级别的开发人员都可交付控件。

设计体验。许多开发人员偏好使用 XAML 设计器。模板化控件布局可以在设计器中进行构建,但用户控件才包含设计时体验。

API 图面。通过构建直观的 API 图面,开发人员可以很容易地使用控件。用户控件支持自定义属性、事件和方法,但模板化控件最灵活。

灵活的视觉对象。通过提供出色的默认体验,开发人员可以轻松使用控件。但是,灵活的控件支持重新模板化的视觉对象。仅模板化控件支持重新模板化。

总之,要获得简便性和设计体验,用户控件是最佳选择,而模板化控件提供最佳的 API 图面和最为灵活的视觉对象。

如果决定开始使用用户控件,然后迁移到模板化控件,那么在此之前需要执行一些操作。但是,这不是固定不变的。本文开始使用用户控件,然后移至模板化控件。重要的是,要知道许多可重用的布局只需使用用户控件。打开业务线解决方案并找到用户控件和模板化控件,这也是可行的。

新用户情景

新用户必须同意新应用程序中的最终用户许可协议 (EULA)。众所周知,没有用户愿意同意 EULA。不过,法律必须确保用户勾选“我同意”,用户才能继续操作。因此,即使 EULA 令人感到困惑,你也要确保 XAML 界面干净整洁且直观可见。开始原型设计;添加 TextBlock、复选框、按钮,然后开始构建(如图 1 所示)。

原型 UI
图 1 原型 UI

在 XAML 设计器中执行原型设计,速度非常快。由于你花时间了解了工具,因此此操作非常容易。但是应用程序中的其他窗体呢?可能需要在其他位置使用此功能。封装是一种用于隐藏使用者提供的复杂逻辑的设计模式。不要自我重复 (DRY) 是另一种模式,专注于代码重用。  XAML 通过用户控件和自定义控件提供这两种模式。作为 XAML 开发人员,你知道自定义控件的功能比用户控件更强大,但它们并不那么简单。你决定开始使用用户控件。也许该控件可以完成作业。剧透警告:无法完成。

用户控件

用户控件非常简单。它们提供一致且可重用的接口和自定义封装代码隐藏。要创建用户控件,请从“添加新项”对话框中选择“用户控件”(如图 2 所示)。

“添加新项”对话框
图 2“添加新项”对话框

用户控件通常是其他控件的子控件。但是,其生命周期与窗口和页面的生命周期非常相似,用户控件可以是设置为 Window.Current.Content 属性的值。用户控件功能完善,支持视觉对象状态管理、内部资源以及 XAML 框架的所有其他主要功能。构建控件并不会损害可用功能。我的目标是在页面上重用控件,利用其对视觉对象状态管理、资源、样式和数据绑定的支持。其 XAML 实现非常简单且易于设计:

<UserControl>
  <StackPanel Padding="20">
    <TextBlock>Lorem ipsum.</TextBlock>
    <CheckBox>I agree!</CheckBox>
    <Button>Submit</Button>
  </StackPanel>
</UserControl>

此 XAML 呈现早期的原型,并显示用户控件的简单程度。当然,目前不存在自定义行为,只有声明的控件的内置行为。

文本快速路径 EULA 非常冗长,因此我们要了解文本性能问题。TextBlock(仅限 TextBlock)已经过优化,可以使用快速路径、低内存和 CPU 呈现。它的构建速度很快,但我可以对其进行破坏:

<TextBlock Text="Optimized" />
<TextBlock>Not optimized</TextBlock>

使用 TextBlock 内联控件(如 <Run/> 和 <LineBreak />)可中断优化。CharacterSpacing、Line­StackingStrategy 和 TextTrimming 等属性可以执行相同的操作。感到迷惑吗?执行一个简单的测试:

Application.Current.DebugSettings
  .IsTextPerformanceVisualizationEnabled = true;

IsTextPerformanceVisualizationEnabled 是一个鲜为人知的调试设置,可以在调试时查看应用程序中已优化的文本。如果文本不是绿色,则需要对其进行调查。

使用每个版本的 Windows,很少有属性可以影响快速路径;不过,仍有一些不安全且意外的属性会影响性能。执行一些有意的调试,这些就不会造成太大影响。

不可变的规则 关于业务逻辑应驻留的选项,存在多种看法。常用规则将不太可变的规则置于更靠近控件的位置。这通常更容易、速度更快且可以优化可维护性。

顺便说一句,业务规则与数据验证不同。响应数据输入和检查文本长度以及数字范围只需进行验证。这些规则控制用户行为的类型。

例如,某银行的业务规则是不向信用评分低于特定值的客户提供贷款。管道工的规则是不允许在特定地区之外的地方与客户来往。规则即行为。在某些情况下,规则每天都会发生更改,例如信用评分会影响新的贷款。在其他情况下,规则永远不会改变,例如机械师在斯巴鲁汽车工作怎么也不会超过 2014 年。

现在,请考虑使用此验收条件:在选中复选框之前,用户无法单击按钮。这是一条规则,并且它尽可能接近不可变。我将在靠近控件的位置实现该规则:

<StackPanel Padding="20">
  <TextBlock>Lorem ipsum.</TextBlock>
  <CheckBox x:Name="AgreeCheckBox">I agree!</CheckBox>
  <Button IsEnabled="{Binding Path=IsChecked,
    ElementName=AgreeCheckBox}">Submit1</Button>
  <Button IsEnabled="{x:Bind Path=AgreeCheckBox.IsChecked.Value,
    Mode=OneWay}">Submit2</Button>
</StackPanel>

在此代码中,数据绑定完全满足我的要求。Submit1 按钮使用经典的 WPF(和 UWP)数据绑定。Submit2 按钮使用现代 UWP 数据绑定。

请注意,在图 3 中,启用了 Submit2。是否正确?嗯,在 Visual Studio 设计器中,经典数据绑定在设计时占有绘制优势。目前,编译数据绑定 (x:Bind) 仅在运行时发生。在经典数据绑定和编译数据绑定之间进行选择,这是将要做出的既难以抉择又非常简单的决定。一方面,编译绑定速度非常快。但是,另一方面,经典绑定非常简单。编译绑定的存在是为了解决 XAML 难以解决的性能问题:数据绑定。因为经典绑定需要运行时反射,所以它本身速度比较慢,难以缩放。

使用数据绑定实现业务规则
图 3 使用数据绑定实现业务规则

经典绑定中添加了许多新功能(如异步绑定),并且出现了几种可帮助开发人员的模式。然而,随着 UWP 成功接替 WPF,它遭遇了同样的拖放问题。以下是值得思索的问题:在异步模式下使用经典绑定的功能未从 WPF移植到 UWP。虽然了解你的需求,但它确实鼓励企业开发人员致力于编译绑定。编译绑定利用 XAML 代码生成器,自动创建代码隐藏,并将绑定语句与实际属性和数据类型耦合。

由于这种耦合,不匹配的类型可能会产生错误,因为可能会尝试绑定到匿名对象或动态 JSON 对象。许多开发人员都无法避开这些边缘情况,但这些情况已得到解决:

  • 编译绑定解决了数据绑定的性能问题,同时引入了某些约束。
  • 后向兼容性保留经典绑定支持,同时为 UWP 开发人员提供了更好的选择。
  • 对编译绑定而不是经典绑定进行数据绑定的创新和改进。
  • 函数绑定等功能仅适用于编译绑定,其中明确关注 Microsoft 的绑定策略。

然而,经典绑定的简便性和设计时支持使参数处于活动状态,迫使 Microsoft 开发人员工具团队继续改进编译绑定及其开发人员体验。请注意,在本文中,选择经典绑定还是选择编译绑定将产生几乎无法估量的影响。一些示例将演示经典绑定,而其他示例演示编译绑定。由你来决定。当然,该决定在大型应用中意义重大。

自定义事件 无法在 XAML 中声明自定义事件,因此可以在代码隐藏中对其进行处理。例如,可以将提交按钮的单击事件转发到用户控件上的自定义单击事件:

public event RoutedEventHandler Click;
public MyUserControl1()
{
  InitializeComponent();
  SubmitButton.Click += (s, e)
    => Click?.Invoke(this, e);
}

代码在此处引发自定义事件,从按钮转发 Routed­EventArgs。消费方开发人员可以声明性地处理这些事件,就像 XAML 中的每个其他事件一样:

<controls:MyUserControl1 Click="MyUserControl1_Click" />

这样做的好处在于,消费方开发人员无需学习新的范例;自定义控件和开箱即用的第一方控件可以实现相同的功能。

自定义属性 要让消费方开发人员提供其自己的 EULA,可以在 TextBlock 上设置 x:FieldModifier 属性。此操作从默认的私有值修改 XAML 编译行为:

<TextBlock x:Name="EulaTextBlock" x:FieldModifier="public" />

不过,简单并不意味着好。此方法几乎不提供抽象,并且需要开发人员了解内部结构。它还需要使用代码隐藏。所以,在这种情况下,应避免使用属性方法:

public string Text
{
  get => (string)GetValue(TextProperty);
  set => SetValue(TextProperty, value);
}
public static readonly DependencyProperty TextProperty =
  DependencyProperty.Register(nameof(Text), typeof(string),
    typeof(MyUserControl1), new PropertyMetadata(string.Empty));

同样简单且无需注意的是,依赖属性数据绑定到 TextBlock 的 Text 属性。此操作允许消费方开发人员读取、写入或绑定到自定义 Text 属性:

<StackPanel Padding="20">
  <TextBlock Text="{x:Bind Text, Mode=OneWay}" />
  <CheckBox>I agree!</CheckBox>
  <Button>Submit</Button>
</StackPanel>

要支持数据绑定,必需使用依赖属性。可靠的控件支持基本用例(如数据绑定)。另外,依赖属性只向代码库添加一行:

<TextBox Text="{x:Bind Text, Mode=TwoWay,
  UpdateSourceTrigger=PropertyChanged}" />

无需使用 INotifyPropertyChanged,即可支持用户控件中自定义属性的双向数据绑定。这是因为依赖属性会引发绑定框架监视的内部更改事件。这是它自己的 INotifyPropertyChanged 类。

上述代码提醒我们 UpdateSourceTrigger 确定注册更改的时间。可能的值为 Explicit、LostFocus 和 PropertyChanged。后者在进行更改时发生。

障碍 消费方开发人员可能要设置用户控件的 content 属性。这是一种直观的方法,但不受用户控件支持。该属性已设置为声明的 XAML:

<Controls:MyUserControl>
  Lorem Ipsum
</Controls:MyUserControl>

此语法将覆盖 content 属性:TextBlock、复选框和按钮。如果考虑重新模板化基本 XAML 用例,那么用户控件就无法提供完整且可靠的体验。用户控件非常简单,但提供很少的控件或扩展性。直观的语法和重新模板化支持是通用体验的一部分。请考虑使用模板化控件。

模板化控件

XAML 在内存使用情况、性能、辅助功能和视觉对象一致性方面取得了巨大改进。开发人员偏好使用 XAML,因为它非常灵活。模板化控件就是一个很好的例子。

模板化控件可以定义全新的内容,但通常是几个现有控件的组合。此处的 TextBlock、复选框和按钮示例是一个经典场景。

顺便说一句,不要将模板化控件与自定义控件混淆。模板化控件是一种自定义布局。自定义控件只是一个继承现有控件的类,不带任何自定义样式。有时,如果只需使用现有控件上的额外方法或属性,则自定义控件是理想之选;它们的视觉对象和逻辑已经存在,只需对其进行扩展。

控件模板 控件的布局由 ControlTemplate 定义。此特殊资源在运行时应用。每个框和按钮都驻留在 ControlTemplate 中。可以通过 Template 属性轻松访问控件的模板。该 Template 属性不是只读的。开发人员可以将其设置为自定义 ControlTemplate,转换控件的视觉对象和行为,以满足其特定需求。这就是重新模板化的强大功能:

<ControlTemplate>
  <StackPanel Padding="20">
    <ContentControl Content="{TemplateBinding Content}" />
    <CheckBox>I agree!</CheckBox>
    <Button>Submit1</Button>
  </StackPanel>
</ControlTemplate>

ControlTemplate XAML 看起来与任何其他布局声明无异。在上述代码中,请注意特殊的 TemplateBinding 标记扩展。此特殊绑定针对单向模板操作进行了调整。自 Windows 10 1809 版起,UWP ControlTemplate 定义支持 x:Bind 语法。这允许在模板中执行、编译、双向和函数绑定。TemplateBinding 非常适用于大多数情况。

Generic.xaml 要创建模板化控件,请在“添加新项”对话框中选择“模板化控件”。此操作引入了三个文件:XAML 文件、其代码隐藏以及包含 ControlTemplate 的 themes/generic.xaml。Themes/generic.xaml 文件与 WPF 相同。它非常特殊。框架会自动将其合并到应用的资源中。此处定义的资源位于应用程序级别范围内:

<Style TargetType="controls:MyControl">
  <Setter Property="Template">
    <Setter.Value>
      <ControlTemplate TargetType="controls:MyControl" />
    </Setter.Value>
  </Setter>
</Style>

使用隐式样式(不带键的样式)应用 ControlTemplate。显式样式有一个可用于将样式应用于控件的键;隐式样式基于 TargetType 进行应用。因此,必须设置 DefaultStyleKey:

public sealed class MyControl : Control
{
  public MyControl() => DefaultStyleKey = typeof(MyControl);
}

此代码设置 DefaultStyleKey,确定隐式应用于控件的样式。将其设置为 ControlTemplate 中 TargetType 的相应值:

<ControlTemplate TargetType="controls:MyControl">
  <StackPanel Padding="20">
    <TextBlock Text="{TemplateBinding Text}" />
    <CheckBox Name="AgreeCheckbox">I agree!</CheckBox>
    <Button IsEnabled="{Binding IsChecked,
      ElementName=AgreeCheckbox}">Submit</Button>
  </StackPanel>
</ControlTemplate>

TemplateBinding 将 TextBlock 的 Text 属性绑定到从用户控件复制到模板化控件的自定义依赖属性。TemplateBinding 是一种非常有效的方式,通常是最佳选择。

图 4 显示了我在设计器中的工作成果。ControlTemplate 中声明的自定义布局应用于自定义控件,并在设计时执行并呈现绑定:

 

<controls:MyControl Text="My outside value." />

使用 Internal 属性透明地预览
图 4 使用 Internal 属性透明地预览

使用自定义控件的语法非常简单。通过允许开发人员使用内联文本,可以更好的实现。这是设置元素内容的最直观的语法。XAML 提供了一个类属性,可以帮助执行操作,如图 5 所示。

图 5 使用 Class 属性设置 Content 属性

[ContentProperty(Name = "Text")]
public sealed class MyControl : Control
{
  public MyControl() => DefaultStyleKey = typeof(MyControl);
  public string Text
  {
    get => (string)GetValue(TextProperty);
    set => SetValue(TextProperty, value);
  }
  public static readonly DependencyProperty TextProperty =
    DependencyProperty.Register(nameof(Text), typeof(string),
      typeof(MyControl), new PropertyMetadata(default(string)));
}

请注意,ContentProperty 属性来自 Windows.UI.Xaml.Markup 命名空间。它指示应该写入 XAML 中声明的内联属性直接内容。那么,现在可按如下声明内容:

<controls:MyControl>
  My inline value!
</controls:MyControl>

太棒了!使用模板化控件,可以灵活地以任何最直观的方式设计控件交互和语法。图 6 显示将 ContentProperty 引入控件的结果****。

设计预览
图 6 设计预览

ContentControl 之前必须创建自定义属性并将该属性映射到控件的内容,XAML 提供了一个已为该操作构建的控件。该控件称为 ContentControl,其属性称为 Content。ContentControl 还提供 ContentTemplate 和 ContentTransition 属性来处理可视化效果和转换。按钮、复选框和框架以及许多标准 XAML 控件都继承 ContentControl。我的也可以;我只使用 Content 而不使用 Text:

public sealed class MyControl2 : ContentControl
{
  // Empty
}

在此代码中,请注意 terse 语法,用于使用 Content 属性创建自定义控件。ContentControl 在声明时自动呈现为 ContentPresenter。这是一个快速且简单的解决方案。但是有一点需要注意:ContentControl 不支持 XAML 中的文本字符串。因为它违反了我的目标,使控件支持文本字符串,我将坚持使用控件,其次考虑使用 ContentControl。

访问内部控件 x:Name 指令声明在代码隐藏中自动生成的字段名称。为复选框提供一个 MyCheckBox 的 x:Name,并且生成器将在名为 MyCheckBox 的类中创建字段。

相反,x:Key(仅适用于资源)不会创建字段。首次使用资源之前,它会将资源添加到未解析类型的字典中。对于资源,x:Key 提供了性能改进。

因为 x:Name 创建了一个支持字段,所以不能在 ControlTemplate 中使用它;模板与任何支持类互不相干。相反,可以使用 Name 属性。

Name 是 FrameworkElement(控件的上级元素)上的依赖属性。可在无法使用 x:Name 时使用该属性。由于 FindName,Name 和 x:Name 在给定范围内互相排斥。

FindName 是一个 XAML 方法,用于按名称查找对象;它适用于 Name 或 x:Name。它在代码隐藏中是可靠的,但在必须使用 GetTemplateChild 的模板化控件中不可靠:

protected override void OnApplyTemplate()
{
  if (GetTemplateChild("AgreeCheckbox") is CheckBox c)
  {
    c.Content = "I really agree!";
  }
}

GetTemplateChild 是 OnApply­Template 替代中使用的辅助方法,用于查找 ControlTemplate 创建的控件。该方法用于查找对内部控件的引用。

更改模板 重新模板化控件非常简单,但我已构建了类以期望使用具有某些名称的控件。我必须确保新模板可维护此依赖项。我们来构建新的 ControlTemplate:

<ControlTemplate
  TargetType="controls:MyControl"
  x:Key="MyNewStyle">

对 ControlTemplate 的小改动都是正常的。不必从头开始。在 Visual Studio 文档大纲窗格中,右键单击任何控件并提取其当前模板的副本(请参阅图 7)。

提取控件模板
图 7 提取控件模板

如果保留其依赖项,新的 ControlTemplate 可以完全更改控件的视觉对象和行为。在控件上声明显式样式会告知框架忽略默认的隐式样式:

<controls:MyControl Style="{StaticResource MyControlNewStyle}">

但是这次对 ControlTemplate 的重写附带一条警告。控件设计器和开发人员必须小心谨慎,才能为辅助功能和本地化功能提供支持。很容易就误删除这些内容了。

TemplatePartAttribute 如果自定义控件可以传达其预期的命名元素,那将会很方便。仅在边缘情况下可能需要使用一些命名元素。在 WPF 中,有 TemplatePartAttribute:

[TemplatePart (
  Name = "EulaTextBlock",
  Type = typeof(TextBlock))]
public sealed class MyControl : Control { }

此语法显示控件如何将内部依赖项传递给外部开发人员。在此示例中,我期望在 ControlTemplate 中使用名称为 EulaTextBlock 的 TextBlock。还可以在自定义控件中指定想要的视觉对象状态:

[TemplateVisualState(
  GroupName = "Visual",
  Name = "Mouseover")]
public sealed class MyControl : Control { }

Blend 与 TemplateVisualState 一起使用 TemplatePart,可以指导开发人员在创建自定义模板时不受预期影响。可以针对这些属性验证 ControlTemplate。自 10240 起,WinRT 已附带这些属性。UWP 可以使用这些属性,但 Blend for Visual Studio 不可以使用这些属性。这些仍然是一种不错的、具有前瞻性的做法,但文档仍然是最佳方式。

辅助功能 第一方 XAML 控件经过精心设计和测试,美观、兼容且易于访问。辅助功能要求现在处于“高级别”,并且发布要求适用于每个控件。

重新模板化第一方控件时,会使开发团队精心添加的辅助功能受到威胁。这些很难做到正确,而且很容易出错。在选择重新模板化控件时,应该精通框架的辅助功能及实现这些功能的技术。否则,你会损失相当大一部分好处。

将辅助功能添加为发布要求,不仅可以帮助那些永久性残障人士,也可以帮助那些暂时丧失能力的人。此外,还可在重新模板化第一方控件时降低风险。

你做到了!

从用户控件升级到模板化控件后,几乎不用引入新的代码。即可添加许多功能。让我们思考一下总体上已完成的操作。

封装。该控件是几个控件的集合,捆绑了消费方开发人员可以轻松地在应用程序中重复使用的自定义视觉对象和行为。

业务逻辑。该控件包含满足用户情景的验收条件的业务规则。将不可变的规则置于靠近控件的位置,同时支持丰富的设计时体验。

自定义事件。该控件公开特定于控件的自定义事件(如“单击”),可帮助开发人员与控件进行互操作,而无需了解布局的内部结构。

自定义属性。该控件用于公开属性,消费方开发人员可使用这些属性来影响布局的内容。这样做是为了完全支持 XAML 数据绑定。

API 语法。该控件支持一种直观的方法,允许开发人员使用文本字符串并以直接的方式声明其内容。我利用了 ContentProperty 属性来执行此操作。

模板。该控件附带默认的展示直观界面的 ControlTemplate。但是,支持 XAML 重新模板化,消费方开发人员可以按需自定义视觉对象。

还有更多可以实现的精彩功能

控件需要使用更多,但不是过多的功能 - 只需要注意布局(比如需要滚动大型文本)和一些属性(如复选框的内容)。多么令人惊讶!

控件可以支持 Visual State 管理(XAML 的本机功能),可以根据大小调整事件或框架事件(如鼠标悬停)来更改属性。成熟的控件具有视觉对象状态。

控件可以支持本地化;UWP 中的本机功能使用 x:Uid 指令将控件与按可用区域设置筛选的 RESW 字符串关联。成熟的控件支持本地化。

控件可以支持外部样式定义,以帮助更新其视觉对象,而无需使用新模板;这可能涉及共享视觉对象和利用主题和 BasedOn 样式。成熟的控件共享和重复使用样式。

总结

在 XAML 中对 UI 进行原型设计速度非常快。用户控件可轻松创建简单、可重用的布局。模板化控件需要执行更多操作,才能创建简单且可重用的布局,其中附带更丰富的功能。正确的做法取决于开发人员,是否有相关知识和丰富的经验。试验。对工具越精通,工作效率就越高。

Windows 窗体于 2002 年接替 Visual Basic 6,就像 WPF 于 2006 年接替 Windows Forms 一样。WPF 引入其 XAML(UI 的声明性语言)。Microsoft 开发人员从未用过 XAML 之类的任何功能。如今,Xamarin 和 UWP 将 XAML 引入 iOS、Android、HoloLens、Surface Hub、Xbox、IoT 和新式桌面。实际上,XAML 现在是一种构建 Windows OS 本身的技术。

世界各地的开发人员都偏好使用 XAML,因为它非常高效和灵活。Microsoft 工程师也有同感;我们将使用 XAML 构建自己的应用甚至构建 Windows 10。未来一片光明,工具功能强大,并且技术比以往更加易于使用。


Jerry Nixon 是 Microsoft 商业软件工程的高级软件工程师和首席架构师。他在软件开发和设计方面已拥有 20 年的经验。Nixon 是一名演讲者、组织者、教师和作者,也是 DevRadio 的主持人。他的大部分时间都花在给他的三个女儿讲述“星际迷航”的背景故事和情节剧情。

衷心感谢以下 Microsoft 技术专家对本文的审阅:Daniel Jacobson、Dmitry Lyalin、Daren May、Ricardo Minguez Pablos


在 MSDN 杂志论坛讨论这篇文章