【WPF.NET开发】WPF中的数据绑定

本文内容

  1. 什么是数据绑定
  2. 数据绑定基本概念
  3. 数据绑定的示例
  4. 创建绑定
  5. 数据转换
  6. 绑定到集合
  7. 数据模板化
  8. 数据验证
  9. 调试机制

Windows Presentation Foundation (WPF) 中的数据绑定为应用呈现数据并与数据交互提供了一种简单而一致的方法。 元素能够以 .NET 对象和 XML 的形式绑定到不同类型的数据源中的数据。 所有 ContentControl(例如 Button)以及所有 ItemsControl(例如 ListBox 和 ListView)都具有内置功能,使单个数据项或数据项集合可以灵活地进行样式设置。 可基于数据生成排序、筛选和分组视图。

WPF 中的数据绑定与传统模型相比具有几个优点,包括本质上支持数据绑定的大量属性、灵活的数据 UI 表示形式以及业务逻辑与 UI 的完全分离。

本文首先讨论 WPF 数据绑定的基本概念,然后介绍 Binding 类的用法和数据绑定的其他功能。 

1、什么是数据绑定

数据绑定是在应用 UI 与其显示的数据之间建立连接的过程。 如果绑定具有正确的设置,并且数据提供适当的通知,则在数据更改其值时,绑定到该数据的元素会自动反映更改。 数据绑定还意味着,如果元素中数据的外部表示形式发生更改,则基础数据可以自动进行更新以反映更改。 例如,如果用户编辑 TextBox 元素中的值,则基础数据值会自动更新以反映该更改。

数据绑定的典型用法是将服务器或本地配置数据放置到窗体或其他 UI 控件中。 此概念在 WPF 中得到扩展,包括将大量属性绑定到不同类型的数据源。 在 WPF 中,元素的依赖属性可以绑定到 .NET 对象(包括 ADO.NET 对象或与 Web 服务和 Web 属性关联的对象)和 XML 数据。

2、数据绑定基本概念

不论要绑定什么元素,也不论数据源是什么性质,每个绑定都始终遵循下图所示的模型。

basic-data-binding-diagram.png?view=netdesktop-8.0

如图所示,数据绑定实质上是绑定目标与绑定源之间的桥梁。 该图演示了以下基本的 WPF 数据绑定概念:

请务必记住,在建立绑定时,需要将绑定目标绑定到绑定源。 例如,如果要使用数据绑定在 ListBox 中显示一些基础 XML 数据,则需要将 ListBox 绑定到 XML 数据。

若要建立绑定,请使用 Binding 对象。 本文的其余部分讨论了与 Binding 对象相关的许多概念以及该对象的一些属性和用法。

2.1 数据上下文

当在 XAML 元素上声明数据绑定时,它们会通过查看其直接的 DataContext 属性来解析数据绑定。 数据上下文通常是绑定源值路径评估的绑定源对象。 可以在绑定中重写此行为,并设置特定的绑定源对象值。 如果未设置承载绑定的对象的 DataContext 属性,则将检查父元素的 DataContext 属性,依此类推,直到 XAML 对象树的根。 简而言之,除非在对象上显式设置,否则用于解析绑定的数据上下文将继承自父级。

绑定可以配置为使用特定的对象进行解析,而不是使用数据上下文进行绑定解析。 例如,在将对象的前景色绑定到另一个对象的背景色时,将使用直接指定源对象。 无需数据上下文,因为绑定在这两个对象之间解析。 相反,未绑定到特定源对象的绑定使用数据上下文解析。

当 DataContext 属性发生更改时,重新评估可能会受数据上下文影响的所有绑定。

2.2 数据流的方向

正如上图中的箭头所示,绑定的数据流可以从绑定目标流向绑定源(例如,当用户编辑 TextBox 的值时,源值会发生更改)和/或(在绑定源提供正确通知的情况下)从绑定源流向绑定目标(例如,TextBox 内容会随绑定源中的更改而进行更新)。

你可能希望应用允许用户更改数据,然后将该数据传播回源对象。 或者,可能不希望允许用户更新源数据。 可以通过设置 Binding.Mode 来控制数据流。

此图演示了不同类型的数据流:

databinding-dataflow.png?view=netdesktop-8.0

若要检测源更改(适用于 OneWay 和 TwoWay 绑定),则源必须实现合适的属性更改通知机制,例如 INotifyPropertyChanged。 请参阅如何:实现属性更改通知 (.NET Framework),获取 INotifyPropertyChanged 实现的示例。

Binding.Mode 属性提供有关绑定模式的详细信息,以及如何指定绑定方向的示例。

2.3 触发源更新的因素

TwoWay 或 OneWayToSource 绑定侦听目标属性中的更改,并将更改传播回源(称为更新源)。 例如,可以编辑文本框的文本以更改基础源值。

但是,在编辑文本时或完成文本编辑后控件失去焦点时,源值是否会更新? Binding.UpdateSourceTrigger 属性确定触发源更新的因素。 下图中右箭头的点说明了 Binding.UpdateSourceTrigger 属性的角色。

data-binding-updatesource-trigger.png?view=netdesktop-8.0

如果 UpdateSourceTrigger 值为 UpdateSourceTrigger.PropertyChanged,则目标属性更改后,TwoWay 或 OneWayToSource 绑定的右箭头指向的值会立即更新。 但是,如果 UpdateSourceTrigger 值为 LostFocus,则仅当目标属性失去焦点时才会使用新值更新该值。

与 Mode 属性类似,不同的依赖属性具有不同的默认 UpdateSourceTrigger 值。 大多数依赖属性的默认值为 PropertyChanged,这将导致源属性的值在目标属性值更改时立即更改。 即时更改适用于 CheckBox 和其他简单控件。 但对于文本字段,每次击键后都进行更新会降低性能,用户也没有机会在提交新值之前使用 Backspace 键修改键入错误。 例如,TextBox.Text 属性默认为 LostFocus 的 UpdateSourceTrigger 值,这会导致源值仅在控件元素失去焦点时(而不是在 TextBox.Text 属性更改时)更改。 有关如何查找依赖属性的默认值的信息,请参阅 UpdateSourceTrigger 属性页。

下表以 TextBox 为例,提供每个 UpdateSourceTrigger 值的示例方案。

展开表

UpdateSourceTrigger 值源值更新时间TextBox 的示例方案
LostFocusTextBox.Text 的默认值)TextBox 控件失去焦点时。与验证逻辑关联的 TextBox(请参阅下文的数据验证)。
PropertyChanged键入 TextBox 时。聊天室窗口中的 TextBox 控件。
Explicit应用调用 UpdateSource 时。可编辑窗体中的 TextBox 控件(仅当用户按“提交”按钮时才更新源值)。

3、数据绑定的示例

有关数据绑定的示例,请参阅数据绑定演示(显示拍卖项的列表)中的以下应用 UI。

demo.png?view=netdesktop-8.0

应用演示了数据绑定的以下功能:

除了绑定到集合以外,在希望绑定到整个对象,而不是仅绑定到对象的单个属性时,也可以使用此方案。 例如,如果源对象的类型为 String,则可能仅希望绑定到字符串本身。 另一种常见情况是希望将一个元素绑定到一个具有多个属性的对象。

你可能需要应用自定义逻辑,以便数据对于绑定的目标属性有意义。 如果不存在默认类型转换,则自定义逻辑可能采用自定义转换器的形式。 有关转换器的信息,请参阅数据转换

3.1 Binding 和 BindingExpression

在介绍数据绑定的其他功能和用法前,先介绍一下 BindingExpression 类会很有用。 如前面部分所述,Binding 类是用于绑定声明的高级类;该类提供许多供用户指定绑定特征的属性。 相关类 BindingExpression 是维持源与目标之间连接的基础对象。 一个绑定包含了可以在多个绑定表达式之间共享的所有信息。 BindingExpression 是无法共享的实例表达式,并包含 Binding 的所有实例信息。

举例来说,假设 myDataObject 是 MyData 类的实例,myBinding 是源 Binding 对象,而 MyData 是包含名为 ColorName 的字符串属性的定义类。 此示例将 TextBlock 的实例 myText 的文本内容绑定到 ColorName

// Make a new source
var myDataObject = new MyData();
var myBinding = new Binding("ColorName")
{
    Source = myDataObject
};

// Bind the data source to the TextBox control's Text dependency property
myText.SetBinding(TextBlock.TextProperty, myBinding);

可以使用同一 myBinding 对象来创建其他绑定。 例如,可使用 myBinding 对象将复选框的文本内容绑定到 ColorName。 在该方案中,将有两个 BindingExpression 实例共享 myBinding 对象。

通过对数据绑定对象调用 GetBindingExpression

当用户选择“添加产品”按钮时,会出现以下窗体。

demo-addproductlisting.png?view=netdesktop-8.0

用户可以编辑窗体中的字段,使用简略或详细预览窗格预览产品清单,然后选择 Submit 以添加新的产品清单。 任何现有的分组、筛选和排序设置都将应用于新条目。 在这种特殊情况下,上图中输入的项会作为 Computer 类别中的第二项显示。

“开始日期”TextBox 中提供的验证逻辑未在此图中显示。 如果用户输入一个无效日期(格式无效或日期已过),则会通过 ToolTip 和 TextBox 旁边显示的红色感叹号来通知用户。 数据验证一节讨论了如何创建验证逻辑。

在详细介绍数据绑定的上述不同功能之前,我们会先讨论对理解 WPF 数据绑定非常重要的基本概念。

4、创建绑定

前面部分中讨论的一些概念可以重申为:使用 Binding 对象建立绑定,且每个绑定通常具有四个组件:绑定目标、目标属性、绑定源以及指向要使用的源值的路径。 本节讨论如何设置绑定。

绑定源绑定到元素的活动 DataContext。 如果元素没有显式定义 DataContext,则会自动继承。

请考虑以下示例,其中的绑定源对象是一个名为 MyData 的类,该类在 SDKSample 命名空间中定义。 出于演示目的,MyData 具有名为 ColorName 的字符串属性,其值设置为“Red”。 因此,此示例生成一个具有红色背景的按钮。

<DockPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
           xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
           xmlns:c="clr-namespace:SDKSample">
    <DockPanel.Resources>
        <c:MyData x:Key="myDataSource"/>
    </DockPanel.Resources>
    <DockPanel.DataContext>
        <Binding Source="{StaticResource myDataSource}"/>
    </DockPanel.DataContext>
    <Button Background="{Binding Path=ColorName}"
            Width="150" Height="30">
        I am bound to be RED!
    </Button>
</DockPanel>

有关绑定声明语法的详细信息以及如何在代码中设置绑定的示例,请参阅绑定声明概述

如果将此示例应用于基本关系图,则生成的图如下所示。 此图描述 OneWay 绑定,因为 Background 属性默认支持 OneWay 绑定。

data-binding-button-background-example.png?view=netdesktop-8.0

你可能会想知道,此绑定为何在 ColorName 属性的类型为字符串而 Background 属性的类型为 Brush 的情况下也会起作用。 此绑定使用默认类型转换,这会在数据转换部分中进行讨论。

4.1 指定绑定源

请注意,在前面的示例中,通过设置 DockPanel.DataContext 属性指定绑定源。 然后,Button 从其父元素 DockPanel 继承 DataContext 值。 重申一下,绑定源对象是绑定的四个必需组件之一。 所以,如果未指定绑定源对象,则绑定将没有任何作用。

可通过多种方法指定绑定源对象。 将多个属性绑定到同一个源时,可以使用父元素上的 DataContext 属性。 不过,有时在个别绑定声明中指定绑定源可能更为合适。 对于前面的示例,不使用 DataContext 属性,而是通过在按钮的绑定声明中直接设置 Binding.Source 属性来指定绑定源,如以下示例所示。

<DockPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
           xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
           xmlns:c="clr-namespace:SDKSample">
    <DockPanel.Resources>
        <c:MyData x:Key="myDataSource"/>
    </DockPanel.Resources>
    <Button Background="{Binding Source={StaticResource myDataSource}, Path=ColorName}"
            Width="150" Height="30">
        I am bound to be RED!
    </Button>
</DockPanel>

除直接在元素中设置 DataContext 属性、从上级元素(例如第一个示例中的按钮)继承 DataContext 值以及通过在绑定上设置 Binding.Source 属性(例如最后一个示例中的按钮)来显式指定绑定源外,你还可以使用 Binding.ElementName 属性或 Binding.RelativeSource 属性指定绑定源。 当绑定到应用中的其他元素时(例如,使用滑块调整按钮的宽度时),ElementName 属性非常有用。 在 ControlTemplate 或 Style 中指定绑定时,可以使用 RelativeSource 属性。 

4.2 指定指向值的路径

如果绑定源是一个对象,则使用 Binding.Path 属性指定要用于绑定的值。 如果要绑定到 XML 数据,则使用 Binding.XPath 属性指定值。 在某些情况下,使用 Path 属性(即使数据为 XML)可能更为合适。 例如,如果要访问返回的 XmlNode(作为 XPath 查询的结果)的 Name 属性,则除 XPath 属性外,还应使用 Path 属性。

虽然我们已强调要使用的值的 Path 是绑定的四个必需组件之一,但在要绑定到整个对象的方案中,要使用的值会与绑定源对象相同。 在这些情况下,可以不指定 Path。 请看下面的示例。

<ListBox ItemsSource="{Binding}"
         IsSynchronizedWithCurrentItem="true"/>

以上示例使用空绑定语法:{Binding}。 在此示例中,ListBox 从父 DockPanel 元素继承 DataContext(此示例中未显示)。 未指定路径时,默认为绑定到整个对象。 换句话说,此示例中的路径已省略,因为要将 ItemsSource 属性绑定到整个对象。 (有关深入讨论,请参阅绑定到集合部分。)

  • 通常情况下,

  • 每个绑定具有四个组件:

    • 绑定目标对象。
    • 目标属性。
    • 绑定源。
    • 指向绑定源中要使用的值的路径。

    例如,如果将 TextBox 的内容绑定到 Employee.Name 属性,则可以类似如下所示设置绑定:

    展开表

    设置“值”
    目标TextBox
    目标属性Text
    源对象Employee
    源对象值路径Name
  • 目标属性必须为依赖属性。

    大多数 UIElement 属性都是依赖属性,而大多数依赖属性(只读属性除外)默认支持数据绑定。 只有从 DependencyObject 派生的类型才能定义依赖项属性。 所有 UIElement 类型从 DependencyObject 派生。

  • 绑定源不限于自定义 .NET 对象。

    尽管未在图中显示,但请注意,绑定源对象不限于自定义 .NET 对象。 WPF 数据绑定支持 .NET 对象、XML 甚至是 XAML 元素对象形式的数据。 例如,绑定源可以是 UIElement、任何列表对象、ADO.NET 或 Web 服务对象,或包含 XML 数据的 XmlNode。 有关详细信息,请参阅绑定源概述

  • 通过 OneWay 绑定,对源属性的更改会自动更新目标属性,但对目标属性的更改不会传播回源属性。 如果绑定的控件为隐式只读,则此类型的绑定适用。 例如,可能会绑定到股票行情自动收录器这样的源,也可能目标属性没有用于进行更改的控件接口(例如表的数据绑定背景色)。 如果无需监视目标属性的更改,则使用 OneWay 绑定模式可避免 TwoWay 绑定模式的系统开销。

  • 通过 TwoWay 绑定,更改源属性或目标属性时会自动更新另一方。 此类型的绑定适用于可编辑窗体或其他完全交互式 UI 方案。 大多数属性默认为 OneWay 绑定,但某些依赖属性(通常为用户可编辑控件的属性,例如 TextBox.Text 和 CheckBox.IsChecked)默认为 TwoWay 绑定。

    用于确定依赖项属性绑定在默认情况下是单向还是双向的编程方法是:使用 DependencyProperty.GetMetadata 获取属性元数据。 此方法的返回类型为 PropertyMetadata,它不包含任何有关绑定的元数据。 但是,如果可以将此类型强制转换为派生的 FrameworkPropertyMetadata,则可以检查 FrameworkPropertyMetadata.BindsTwoWayByDefault 属性的布尔值。 以下代码示例演示了如何获取 TextBox.Text 属性的元数据:

    public static void PrintMetadata()
    {
        // Get the metadata for the property
        PropertyMetadata metadata = TextBox.TextProperty.GetMetadata(typeof(TextBox));
    
        // Check if metadata type is FrameworkPropertyMetadata
        if (metadata is FrameworkPropertyMetadata frameworkMetadata)
        {
            System.Diagnostics.Debug.WriteLine($"TextBox.Text property metadata:");
            System.Diagnostics.Debug.WriteLine($"  BindsTwoWayByDefault: {frameworkMetadata.BindsTwoWayByDefault}");
            System.Diagnostics.Debug.WriteLine($"  IsDataBindingAllowed: {frameworkMetadata.IsDataBindingAllowed}");
            System.Diagnostics.Debug.WriteLine($"        AffectsArrange: {frameworkMetadata.AffectsArrange}");
            System.Diagnostics.Debug.WriteLine($"        AffectsMeasure: {frameworkMetadata.AffectsMeasure}");
            System.Diagnostics.Debug.WriteLine($"         AffectsRender: {frameworkMetadata.AffectsRender}");
            System.Diagnostics.Debug.WriteLine($"              Inherits: {frameworkMetadata.Inherits}");
        }
    
        /*  Displays:
         *  
         *  TextBox.Text property metadata:
         *    BindsTwoWayByDefault: True
         *    IsDataBindingAllowed: True
         *          AffectsArrange: False
         *          AffectsMeasure: False
         *           AffectsRender: False
         *                Inherits: False
        */
    }
    
  • OneWayToSource 绑定与 OneWay 绑定相反;当目标属性更改时,它会更新源属性。 一个示例方案是只需要从 UI 重新计算源值的情况。

  • OneTime 绑定未在图中显示,该绑定会使源属性初始化目标属性,但不传播后续更改。 如果数据上下文发生更改,或者数据上下文中的对象发生更改,则更改不会在目标属性中反映。 如果适合使用当前状态的快照或数据实际为静态数据,则此类型的绑定适合。 如果你想使用源属性中的某个值来初始化目标属性,且提前不知道数据上下文,则此类型的绑定也有用。 此模式实质上是 OneWay 绑定的一种简化形式,它在源值不更改的情况下提供更好的性能。

  • ListBox 的内容已绑定到 AuctionItem 对象的集合。 AuctionItem 对象具有 Description、StartPrice、StartDate、Category 和 SpecialFeatures 等属性。

  • ListBox 中显示的数据(AuctionItem 对象)已进行模板化,以便显示每个项的说明和当前价格。 通过使用 DataTemplate 来创建模板。 此外,每个项的外观取决于要显示的 AuctionItem 的 SpecialFeatures 值。 如果 AuctionItem 的 SpecialFeatures 值为 Color,则该项具有蓝色边框。 如果值为 Highlight,则该项具有橙色边框和一个星号。 数据模板化部分提供了数据模板化的相关信息。

  • 用户可以使用提供的 CheckBoxes 对数据进行分组、筛选或排序。 在上图中,选中了“按类别分组”和“按类别和日期排序”CheckBoxes。 你可能已注意到,数据按产品类别分组,类别名称按字母顺序排序。 这些项还按每个类别中的开始日期排序,但难以从图中注意到这一点。 排序使用集合视图实现。 绑定到集合部分讨论了集合视图。

  • 当用户选择某个项时,ContentControl 显示所选项的详细信息。 此体验称为主-从方案。 主-从方案部分提供有关此绑定类型的信息。

  • StartDate 属性的类型为 DateTime,该类型返回一个包括精确到毫秒的时间的日期。 在此应用中,使用了一个自定义转换器,以便显示较短的日期字符串。 数据转换部分提供有关转换器的信息。

除了绑定到集合以外,在希望绑定到整个对象,而不是仅绑定到对象的单个属性时,也可以使用此方案。 例如,如果源对象的类型为 String,则可能仅希望绑定到字符串本身。 另一种常见情况是希望将一个元素绑定到一个具有多个属性的对象。

你可能需要应用自定义逻辑,以便数据对于绑定的目标属性有意义。 如果不存在默认类型转换,则自定义逻辑可能采用自定义转换器的形式。 有关转换器的信息,请参阅数据转换

4.3 Binding 和 BindingExpression

在介绍数据绑定的其他功能和用法前,先介绍一下 BindingExpression 类会很有用。 如前面部分所述,Binding 类是用于绑定声明的高级类;该类提供许多供用户指定绑定特征的属性。 相关类 BindingExpression 是维持源与目标之间连接的基础对象。 一个绑定包含了可以在多个绑定表达式之间共享的所有信息。 BindingExpression 是无法共享的实例表达式,并包含 Binding 的所有实例信息。

举例来说,假设 myDataObject 是 MyData 类的实例,myBinding 是源 Binding 对象,而 MyData 是包含名为 ColorName 的字符串属性的定义类。 此示例将 TextBlock 的实例 myText 的文本内容绑定到 ColorName

// Make a new source
var myDataObject = new MyData();
var myBinding = new Binding("ColorName")
{
    Source = myDataObject
};

// Bind the data source to the TextBox control's Text dependency property
myText.SetBinding(TextBlock.TextProperty, myBinding);

可以使用同一 myBinding 对象来创建其他绑定。 例如,可使用 myBinding 对象将复选框的文本内容绑定到 ColorName。 在该方案中,将有两个 BindingExpression 实例共享 myBinding 对象。

通过对数据绑定对象调用 GetBindingExpression 来返回 BindingExpression 对象。 

5、数据转换

创建绑定部分,该按钮为红色,因为其 Background 属性绑定到值为“Red”的字符串属性。 此字符串值有效是因为 Brush 类型中存在类型转换器,可用于将字符串值转换为 Brush

将此信息添加到创建绑定部分的图中的情况如下所示。

data-binding-button-default-conversion.png?view=netdesktop-8.0

但是,如果绑定源对象拥有的不是字符串类型的属性,而是 Color 类型的 Color 属性,该怎么办? 在这种情况下,为了使绑定正常工作,首先需要将 Color 属性值转换为 Background 属性可接受的值。 需要通过实现 IValueConverter 接口来创建一个自定义转换器,如以下示例所示。

[ValueConversion(typeof(Color), typeof(SolidColorBrush))]
public class ColorBrushConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        Color color = (Color)value;
        return new SolidColorBrush(color);
    }

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        return null;
    }
}

现在,使用的是自定义转换器而不是默认转换,关系图如下所示。

data-binding-converter-color-example.png?view=netdesktop-8.0

重申一下,由于要绑定到的类型中提供了类型转换器,因此可以使用默认转换。 此行为取决于目标中可用的类型转换器。 如果无法确定,请创建自己的转换器。

6、绑定到集合

绑定源对象可以被视为其属性包含数据的单个对象,也可以被视为通常组合在一起的多态对象的数据集合(例如数据库查询的结果)。 目前为止,我们仅讨论了绑定到单个对象, 但绑定到数据集合也是常见方案。 例如,一种常见方案是使用 ItemsControl(例如 ListBoxListView 或 TreeView)来显示数据集合,如在什么是数据绑定部分所示的应用中。

幸运的是,基本关系图仍然适用。 如果将 ItemsControl 绑定到集合,则关系图如下所示。

data-binding-itemscontrol.png?view=netdesktop-8.0

如图所示,若要将 ItemsControl 绑定到集合对象,则需要使用 ItemsControl.ItemsSource 属性。 你可以将 ItemsSource 视为 ItemsControl 的内容。 绑定为 OneWay,因为 ItemsSource 属性默认支持 OneWay 绑定。

6.1 如何实现集合

你可以枚举实现 IEnumerable 接口的任何集合。 但是,若要设置动态绑定,以便集合中的插入或删除操作可以自动更新 UI,则集合必须实现 INotifyCollectionChanged 接口。 此接口公开一个事件,只要基础集合发生更改,就应该引发该事件。

WPF 提供 ObservableCollection<T> 类,该类是公开 INotifyCollectionChanged 接口的数据集合的内置实现。 若要完全支持将数据值从源对象传输到目标,支持可绑定属性的集合中的每个对象还必须实现 INotifyPropertyChanged 接口。 

在实现自己的集合前,请考虑使用 ObservableCollection<T> 或现有集合类之一,例如 List<T>Collection<T> 和 BindingList<T> 等。 如果有高级方案并且希望实现自己的集合,请考虑使用 IList,它提供可以按索引逐个访问的对象的非泛型集合,因而可提供最佳性能。

6.2 集合视图

在 ItemsControl 绑定到数据集合后,你可能希望对数据进行排序、筛选或分组。 为此,应使用集合视图,这些视图是实现 ICollectionView 接口的类。

什么是集合视图?

集合视图这一层基于绑定源集合,它允许基于排序、筛选和分组查询来导航并显示源集合,而无需更改基础源集合本身。 集合视图还维护一个指向集合中当前项的指针。 如果源集合实现 INotifyCollectionChanged 接口,则 CollectionChanged 事件引发的更改会传播到视图。

由于视图不会更改基础源集合,因此每个源集合都可以有多个关联的视图。 例如,可以有 Task 对象的集合。 使用视图,可以通过不同方式显示相同数据。 例如,可能希望在页面左侧显示按优先级排序的任务,而在页面右侧显示按区域分组的任务。

视图创建方法

创建并使用视图的一种方式是直接实例化视图对象,然后将它用作绑定源。 以什么是数据绑定部分中所示的数据绑定演示应用为例。 该应用的实现方式是将 ListBox 绑定到基于数据集合的视图,而不是直接绑定到数据集合。 下面的示例摘自数据绑定演示应用。 CollectionViewSource 类是从 CollectionView 继承的类的 XAML 代理。 在此特定示例中,视图的 Source 绑定到当前应用对象的 AuctionItem 集合(类型为 ObservableCollection<T>)。

<Window.Resources>
    <CollectionViewSource 
      Source="{Binding Source={x:Static Application.Current}, Path=AuctionItems}"   
      x:Key="listingDataView" />
</Window.Resources>

资源 listingDataView 随后用作应用中元素(例如 ListBox)的绑定源。

<ListBox Name="Master" Grid.Row="2" Grid.ColumnSpan="3" Margin="8" 
         ItemsSource="{Binding Source={StaticResource listingDataView}}" />

若要为同一集合创建另一个视图,则可以创建另一个 CollectionViewSource 实例,并为其提供不同的 x:Key 名称。

下表显示作为默认集合视图创建或由 CollectionViewSource 根据源集合类型创建的视图数据类型。

展开表

源集合类型集合视图类型说明
IEnumerable基于 CollectionView 的内部类型无法对项进行分组。
IListListCollectionView最快。
IBindingListBindingListCollectionView

使用默认视图

创建并使用集合视图的一种方式是指定集合视图作为绑定源。 WPF 还会为用作绑定源的每个集合创建一个默认集合视图。 如果直接绑定到集合,WPF 会绑定到该集合的默认视图。 此默认视图由同一集合的所有绑定共享,因此一个绑定控件或代码对默认视图所做的更改(例如排序或对当前项指针的更改,下文将对此进行讨论)会在同一集合的所有其他绑定中反映。

若要获取默认视图,请使用 GetDefaultView 方法。 

包含 ADO.NET DataTables 的集合视图

为了提高性能,ADO.NET DataTable 或 DataView 对象的集合视图将排序和筛选委托给 DataView,这导致排序和筛选在数据源的所有集合视图之间共享。 若要使每个集合视图都能独立进行排序和筛选,请使用每个集合视图自己的 DataView 对象进行初始化。

排序

如前所述,视图可以将排序顺序应用于集合。 如同在基础集合中一样,数据可能具有或不具有相关的固有顺序。 借助集合视图,可以根据自己提供的比较条件来强制确定顺序,或更改默认顺序。 由于这是基于客户端的数据视图,因此一种常见情况是用户可能希望根据列对应的值,对多列表格数据进行排序。 通过使用视图,可以应用这种用户实施的排序,而无需对基础集合进行任何更改,甚至不必再次查询集合内容。 

以下示例演示了什么是数据绑定部分中的应用 UI 的“按类别和日期排序”CheckBox 的排序逻辑。

private void AddSortCheckBox_Checked(object sender, RoutedEventArgs e)
{
    // Sort the items first by Category and then by StartDate
    listingDataView.SortDescriptions.Add(new SortDescription("Category", ListSortDirection.Ascending));
    listingDataView.SortDescriptions.Add(new SortDescription("StartDate", ListSortDirection.Ascending));
}

筛选

视图还可以将筛选器应用于集合,以便视图仅显示完整集合的特定子集。 可以根据条件在数据中进行筛选。 例如,正如什么是数据绑定部分中的应用所做的那样,“仅显示成交商品”CheckBox 包含了筛选出成交价等于或大于 25 美元的项的逻辑。 如果选择了 CheckBox,则会执行以下代码将 ShowOnlyBargainsFilter 设置为 Filter 事件处理程序。

private void AddFilteringCheckBox_Checked(object sender, RoutedEventArgs e)
{
    if (((CheckBox)sender).IsChecked == true)
        listingDataView.Filter += ListingDataView_Filter;
    else
        listingDataView.Filter -= ListingDataView_Filter;
}

ShowOnlyBargainsFilter 事件处理程序具有以下实现。

private void ListingDataView_Filter(object sender, FilterEventArgs e)
{
    // Start with everything excluded
    e.Accepted = false;

    // Only inlcude items with a price less than 25
    if (e.Item is AuctionItem product && product.CurrentPrice < 25)
        e.Accepted = true;
}

如果直接使用其中一个 CollectionView 类而不是 CollectionViewSource,则可以使用 Filter 属性指定回叫。 

分组

除了用来查看 IEnumerable 集合的内部类之外,所有集合视图都支持分组功能,用户可以利用此功能将集合视图中的集合划分成逻辑组。 这些组可以是显式的,由用户提供组列表;也可以是隐式的,这些组依据数据动态生成。

以下示例演示了“按类别分组”CheckBox 的逻辑。

// This groups the items in the view by the property "Category"
var groupDescription = new PropertyGroupDescription();
groupDescription.PropertyName = "Category";
listingDataView.GroupDescriptions.Add(groupDescription);

当前项指针

视图还支持当前项的概念。 可以在集合视图中的对象之间导航。 在导航时,你是在移动项指针,该指针可用于检索存在于集合中特定位置的对象。 

由于 WPF 只通过使用视图(你指定的视图或集合的默认视图)绑定到集合,因此集合的所有绑定都有一个当前项指针。 绑定到视图时,Path 值中的斜杠(“/”)字符用于指定视图的当前项。 在下面的示例中,数据上下文是一个集合视图。 第一行绑定到集合。 第二行绑定到集合中的当前项。 第三行绑定到集合中当前项的 Description 属性。

<Button Content="{Binding }" />
<Button Content="{Binding Path=/}" />
<Button Content="{Binding Path=/Description}" />

还可以连着使用斜杠和属性语法以遍历集合的分层。 以下示例绑定到一个名为 Offices 的集合的当前项,此集合是源集合的当前项的属性。

<Button Content="{Binding /Offices/}" />

当前项指针可能会受对集合应用的任何排序或筛选操作的影响。 排序操作将当前项指针保留在所选的最后一项上,但集合视图现已围绕此指针重构。 (或许所选项以前曾位于列表的开头,但现在所选项可能位于中间的某个位置。)如果所选内容在筛选之后保留在视图中,则筛选操作会保留所选项。 否则,当前项指针会设置为经过筛选的集合视图的第一项。

主-从绑定方案

当前项的概念不仅适用于集合中各项的导航,也适用于主-从绑定方案。 再考虑一下什么是数据绑定部分中的应用 UI。 在该应用中,ListBox 中的选择确定 ContentControl 中显示的内容。 换句话说,选择 ListBox 项目时,ContentControl 显示所选项的详细信息。

只需将两个或更多控件绑定到同一视图即可实现主-从方案。 数据绑定演示
中的以下示例演示了在
什么是数据绑定部分中的应用 UI 上看到的 ListBox 和 ContentControl 的标记。

<ListBox Name="Master" Grid.Row="2" Grid.ColumnSpan="3" Margin="8" 
         ItemsSource="{Binding Source={StaticResource listingDataView}}" />
<ContentControl Name="Detail" Grid.Row="3" Grid.ColumnSpan="3"
                Content="{Binding Source={StaticResource listingDataView}}"
                ContentTemplate="{StaticResource detailsProductListingTemplate}" 
                Margin="9,0,0,0"/>

请注意,这两个控件都绑定到同一个源,即 listingDataView 静态资源。 此绑定有效是因为将单一实例对象(在本例中为 ContentControl)绑定到集合视图时,它会自动绑定到该视图的 CurrentItem。 CollectionViewSource 对象会自动同步货币和选择。 如果列表控件未像本示例中那样绑定到 CollectionViewSource 对象,则需要将其 IsSynchronizedWithCurrentItem 属性设置为 true 才能起作用。

你可能已经注意到上述示例使用了一个模板。 实际上,如果不使用模板(ContentControl 显式使用的模板以及 ListBox 隐式使用的模板),数据不会按照我们希望的方式显示。 现在,我们开始介绍下一节中的数据模板化。

7、数据模板化

如果不使用数据模板,数据绑定示例部分中的应用 UI 将如下所示:

demo-no-template.png?view=netdesktop-8.0

如前面部分中的示例所示,ListBox 控件和 ContentControl 都绑定到 AuctionItem 的整个集合对象(更具体地说,是绑定到集合对象视图)。 如果未提供如何显示数据集合的特定说明,则 ListBox 会以字符串形式显示基础集合中的每个对象,ContentControl 会以字符串形式显示绑定到的对象。

为了解决该问题,应用定义了 DataTemplates。 如前面部分中的示例所示,ContentControl 显式使用 detailsProductListingTemplate 数据模板。 显示集合中的 AuctionItem 对象时,ListBox 控件隐式使用以下数据模板。

<DataTemplate DataType="{x:Type src:AuctionItem}">
    <Border BorderThickness="1" BorderBrush="Gray"
            Padding="7" Name="border" Margin="3" Width="500">
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition/>
                <RowDefinition/>
                <RowDefinition/>
                <RowDefinition/>
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="20"/>
                <ColumnDefinition Width="86"/>
                <ColumnDefinition Width="*"/>
            </Grid.ColumnDefinitions>

            <Polygon Grid.Row="0" Grid.Column="0" Grid.RowSpan="4"
                     Fill="Yellow" Stroke="Black" StrokeThickness="1"
                     StrokeLineJoin="Round" Width="20" Height="20"
                     Stretch="Fill"
                     Points="9,2 11,7 17,7 12,10 14,15 9,12 4,15 6,10 1,7 7,7"
                     Visibility="Hidden" Name="star"/>

            <TextBlock Grid.Row="0" Grid.Column="1" Margin="0,0,8,0"
                       Name="descriptionTitle"
                       Style="{StaticResource smallTitleStyle}">Description:</TextBlock>
            
            <TextBlock Name="DescriptionDTDataType" Grid.Row="0" Grid.Column="2"
                       Text="{Binding Path=Description}"
                       Style="{StaticResource textStyleTextBlock}"/>

            <TextBlock Grid.Row="1" Grid.Column="1" Margin="0,0,8,0"
                       Name="currentPriceTitle"
                       Style="{StaticResource smallTitleStyle}">Current Price:</TextBlock>
            
            <StackPanel Grid.Row="1" Grid.Column="2" Orientation="Horizontal">
                <TextBlock Text="$" Style="{StaticResource textStyleTextBlock}"/>
                <TextBlock Name="CurrentPriceDTDataType"
                           Text="{Binding Path=CurrentPrice}" 
                           Style="{StaticResource textStyleTextBlock}"/>
            </StackPanel>
        </Grid>
    </Border>
    <DataTemplate.Triggers>
        <DataTrigger Binding="{Binding Path=SpecialFeatures}">
            <DataTrigger.Value>
                <src:SpecialFeatures>Color</src:SpecialFeatures>
            </DataTrigger.Value>
            <DataTrigger.Setters>
                <Setter Property="BorderBrush" Value="DodgerBlue" TargetName="border" />
                <Setter Property="Foreground" Value="Navy" TargetName="descriptionTitle" />
                <Setter Property="Foreground" Value="Navy" TargetName="currentPriceTitle" />
                <Setter Property="BorderThickness" Value="3" TargetName="border" />
                <Setter Property="Padding" Value="5" TargetName="border" />
            </DataTrigger.Setters>
        </DataTrigger>
        <DataTrigger Binding="{Binding Path=SpecialFeatures}">
            <DataTrigger.Value>
                <src:SpecialFeatures>Highlight</src:SpecialFeatures>
            </DataTrigger.Value>
            <Setter Property="BorderBrush" Value="Orange" TargetName="border" />
            <Setter Property="Foreground" Value="Navy" TargetName="descriptionTitle" />
            <Setter Property="Foreground" Value="Navy" TargetName="currentPriceTitle" />
            <Setter Property="Visibility" Value="Visible" TargetName="star" />
            <Setter Property="BorderThickness" Value="3" TargetName="border" />
            <Setter Property="Padding" Value="5" TargetName="border" />
        </DataTrigger>
    </DataTemplate.Triggers>
</DataTemplate>

使用这两个 DataTemplate 时,生成的 UI 即为
什么是数据绑定部分中所示的 UI。 如屏幕截图所示,除了可以在控件中放置数据以外,使用 DataTemplate 还可以为数据定义引人注目的视觉对象。 例如,上述 DataTemplate 中使用了 DataTrigger,因而 SpecialFeatures 值为 HighLight 的 AuctionItem 会显示为带有橙色边框和一个星号。

8、数据验证

接受用户输入的大多数应用都需要具有验证逻辑,以确保用户输入了预期信息。 可基于类型、范围、格式或特定于应用的其他要求执行验证检查。 本部分讨论数据验证在 WPF 中的工作原理。

8.1 将验证规则与绑定关联

WPF 数据绑定模型允许将 ValidationRules 与 Binding 对象关联。 例如,以下示例将 TextBox 绑定到名为 StartPrice 的属性,并将 ExceptionValidationRule 对象添加到 Binding.ValidationRules 属性。

<TextBox Name="StartPriceEntryForm" Grid.Row="2"
         Style="{StaticResource textStyleTextBox}" Margin="8,5,0,5" Grid.ColumnSpan="2">
    <TextBox.Text>
        <Binding Path="StartPrice" UpdateSourceTrigger="PropertyChanged">
            <Binding.ValidationRules>
                <ExceptionValidationRule />
            </Binding.ValidationRules>
        </Binding>
    </TextBox.Text>
</TextBox>

ValidationRule 对象检查属性的值是否有效。 WPF 有两种类型的内置 ValidationRule 对象:

还可以通过从 ValidationRule 类派生并实现 Validate 方法来创建自己的验证规则。 以下示例演示了
什么是数据绑定部分中添加产品清单“起始日期”TextBox 所用的规则。

public class FutureDateRule : ValidationRule
{
    public override ValidationResult Validate(object value, CultureInfo cultureInfo)
    {
        // Test if date is valid
        if (DateTime.TryParse(value.ToString(), out DateTime date))
        {
            // Date is not in the future, fail
            if (DateTime.Now > date)
                return new ValidationResult(false, "Please enter a date in the future.");
        }
        else
        {
            // Date is not a valid date, fail
            return new ValidationResult(false, "Value is not a valid date.");
        }

        // Date is valid and in the future, pass
        return ValidationResult.ValidResult;
    }
}

StartDateEntryFormTextBox 使用此 FutureDateRule,如以下示例所示。

<TextBox Name="StartDateEntryForm" Grid.Row="3"
         Validation.ErrorTemplate="{StaticResource validationTemplate}" 
         Style="{StaticResource textStyleTextBox}" Margin="8,5,0,5" Grid.ColumnSpan="2">
    <TextBox.Text>
        <Binding Path="StartDate" UpdateSourceTrigger="PropertyChanged" 
                 Converter="{StaticResource dateConverter}" >
            <Binding.ValidationRules>
                <src:FutureDateRule />
            </Binding.ValidationRules>
        </Binding>
    </TextBox.Text>
</TextBox>

因为 UpdateSourceTrigger 值为 PropertyChanged,所以绑定引擎会在每次击键时更新源值,这意味着它还会在每次击键时检查 ValidationRules 集合中的每条规则。 我们会在“验证过程”一节中对此深入讨论。

8.2 提供视觉反馈

如果用户输入的值无效,你可能希望在应用 UI 上提供一些有关错误的反馈。 提供此类反馈的一种方法是将 Validation.ErrorTemplate 附加属性设置为自定义 ControlTemplate。 如前面部分所示,StartDateEntryFormTextBox 使用名为 validationTemplate 的 ErrorTemplate。 以下示例显示了 validationTemplate 的定义。

<ControlTemplate x:Key="validationTemplate">
    <DockPanel>
        <TextBlock Foreground="Red" FontSize="20">!</TextBlock>
        <AdornedElementPlaceholder/>
    </DockPanel>
</ControlTemplate>

AdornedElementPlaceholder 元素指定应放置待装饰控件的位置。

此外,还可以使用 ToolTip 来显示错误消息。 StartDateEntryForm 和 StartPriceEntryFormTextBox 都使用样式 textStyleTextBox,该样式创建显示错误消息的 ToolTip。 以下示例显示了 textStyleTextBox 的定义。 如果绑定元素属性上的一个或多个绑定出错,则附加属性 Validation.HasError 为 true

<Style x:Key="textStyleTextBox" TargetType="TextBox">
    <Setter Property="Foreground" Value="#333333" />
    <Setter Property="MaxLength" Value="40" />
    <Setter Property="Width" Value="392" />
    <Style.Triggers>
        <Trigger Property="Validation.HasError" Value="true">
            <Setter Property="ToolTip" 
                    Value="{Binding (Validation.Errors).CurrentItem.ErrorContent, RelativeSource={RelativeSource Self}}" />
        </Trigger>
    </Style.Triggers>
</Style>

使用自定义 ErrorTemplate 和 ToolTip 时,StartDateEntryFormTextBox 在发生验证错误时如下所示。

demo-validation-date.png?view=netdesktop-8.0

如果 Binding 具有关联的验证规则,但未在绑定控件上指定 ErrorTemplate,则发生验证错误时,将使用默认的 ErrorTemplate 通知用户。 默认的 ErrorTemplate 是一个控件模板,它在装饰层中定义红色边框。 使用默认的 ErrorTemplate 和 ToolTip 时,StartPriceEntryFormTextBox 的 UI 在发生验证错误时如下所示。

demo-validation-price.png?view=netdesktop-8.0

8.3 验证过程

通常,在目标的值传输到绑定源属性时会进行验证。 此传输在 TwoWay 和 OneWayToSource 绑定上发生。 重申一下,导致源更新的因素取决于 UpdateSourceTrigger 属性的值,如
触发源更新的因素部分所述。

以下各项描述了验证过程。 只要验证过程中发生验证错误或其他类型的错误,该过程就会中断:

  1. 绑定引擎检查是否为该 Binding 定义了任何将 ValidationStep 设置为 RawProposedValue 的自定义 ValidationRule 对象,在这种情况下,绑定引擎将对每个 ValidationRule 调用 Validate 方法,直到其中一个出错或直到全部通过。

  2. 绑定引擎随后会调用转换器(如果存在)。

  3. 如果转换器成功,则绑定引擎会检查是否为该 Binding 定义了任何将 ValidationStep 设置为 ConvertedProposedValue 的自定义 ValidationRule 对象,在这种情况下,绑定引擎将对每个 ValidationRule(将 ValidationStep 设置为 ConvertedProposedValue)调用 Validate 方法,直到其中一个出错或直到全部通过。

  4. 绑定引擎设置源属性。

  5. 绑定引擎检查是否为该 Binding 定义了任何将 ValidationStep 设置为 UpdatedValue 的自定义 ValidationRule 对象,在这种情况下,绑定引擎将对每个 ValidationRule(将 ValidationStep 设置为 UpdatedValue)调用 Validate 方法,直到其中一个出错或直到全部通过。 如果 DataErrorValidationRule 与绑定关联并且其 ValidationStep 设置为默认的 UpdatedValue,则此时将检查 DataErrorValidationRule。 此时检查将 ValidatesOnDataErrors 设置为 true 的所有绑定。

  6. 绑定引擎检查是否为该 Binding 定义了任何将 ValidationStep 设置为 CommittedValue 的自定义 ValidationRule 对象,在这种情况下,绑定引擎将对每个 ValidationRule(将 ValidationStep 设置为 CommittedValue)调用 Validate 方法,直到其中一个出错或直到全部通过。

如果 ValidationRule 在整个过程中的任何时间都没有通过,则绑定引擎会创建 ValidationError 对象并将其添加到绑定元素的 Validation.Errors 集合中。 绑定引擎在任何给定步骤运行 ValidationRule 对象之前,它会删除在执行该步骤期间添加到绑定元素的 Validation.Errors 附加属性的所有 ValidationError。 例如,如果将 ValidationStep 设置为 UpdatedValue 的 ValidationRule 失败,则下次执行验证过程时,绑定引擎会在调用将 ValidationStep 设置为 UpdatedValue 的任何 ValidationRule 之前删除 ValidationError

如果 Validation.Errors 不为空,则元素的 Validation.HasError 附加属性设置为 true。 此外,如果 Binding 的 NotifyOnValidationError 属性设置为 true,则绑定引擎将在元素上引发 Validation.Error 附加事件。

另请注意,任何方向(目标到源或源到目标)的有效值传输操作都会清除 Validation.Errors 附加属性。

如果绑定具有关联的 ExceptionValidationRule,或将 ValidatesOnExceptions 属性设置为 true,并且在绑定引擎设置源时引发异常,则绑定引擎将检查是否存在 UpdateSourceExceptionFilter。 可以使用 UpdateSourceExceptionFilter 回叫来提供用于处理异常的自定义处理程序。 如果未在 Binding 上指定 UpdateSourceExceptionFilter,则绑定引擎会创建具有异常的 ValidationError 并将其添加到绑定元素的 Validation.Errors 集合中。

9、调试机制

可以在与绑定相关的对象上设置附加属性 PresentationTraceSources.TraceLevel,以接收有关特定绑定状态的信息。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

吉特思米(gitusme)

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值