03 数据绑定

发布时间 2023-12-01 10:20:07作者: 讨厌敲代码的老郭

03 数据绑定

数据绑定顾名思义就是将窗体某些控件的值绑定到某些数据上,如:根据用户的输入框内容自动修改某些控件的文本,常规代码写法如下

<TextBox Name="tb" HorizontalAlignment="Left" VerticalAlignment="Top" Width="200" Height="30" TextChanged="TextBox_TextChanged"></TextBox>
<Label Name="lbl" HorizontalAlignment="Left" VerticalAlignment="Top" Width="200" Height="50" Margin="0,50,0,0"></Label>

隐藏代码如下

private void TextBox_TextChanged(object sender, TextChangedEventArgs e)
{
    this.lbl.Content = ((TextBox)sender).Text;
}

使用数据绑定的语法如下

使用{}​表示要进行数据的绑定,ElementName​指定要绑定控件的名字,使用Path​指定绑定对应控件的哪个属性值

<Label Content="{Binding ElementName=tb,Path=Text}" HorizontalAlignment="Left" VerticalAlignment="Top" Width="200" Height="50" Margin="0,120,0,0"></Label>

数据绑定概述 - WPF .NET | Microsoft Learn

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

什么是数据绑定?

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

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

数据绑定基本概念

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

​​image

通常情况下,每个绑定具有四个组件:

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

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

设置 “值”
目标 TextBox
目标属性 Text
源对象 Employee
源对象值路径 Name

数据流的方向

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

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

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

image

  • 通过 OneWay 绑定,对源属性的更改会自动更新目标属性,但对目标属性的更改不会传播回源属性
  • 通过 TwoWay 绑定,更改源属性或目标属性时会自动更新另一方。
  • OneWayToSource 绑定与 OneWay 绑定相反;当目标属性更改时,它会更新源属性。

触发源更新的因素

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

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

image

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

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

数据的绑定

数据的绑定使用{Binding ElementName=控件名, Path=属性 }​,如下:

<Slider Name="slider" Background="Red" Foreground="Green" Maximum="100"/>
<TextBox Text="{Binding ElementName=slider, Path=Value}"/>

该例子中Slider​滑动会同步修改输入框的内容,TextBox​的修改将会在失去焦点后同步到滑块,我们也可以通过修改Mode​和UpdateSourceTrigger​属性来修改这些默认的行为

Mode​控制数据绑定的模式

<!--双向绑定-->
<Slider Name="slider1" Background="Red" Foreground="Green" Maximum="100"/>
<TextBox Text="{Binding ElementName=slider1, Path=Value, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
<!--数据源流向目标对象OneWay-->
<Slider Name="slider2" Background="Red" Foreground="Green" Maximum="100"/>
<TextBox Text="{Binding ElementName=slider2, Path=Value, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"/>
<!--目标对象流向数据源-->
<Slider Name="slider4" Background="Red" Foreground="Green" Maximum="100"/>
<TextBox Text="{Binding ElementName=slider4, Path=Value, Mode=OneWayToSource, UpdateSourceTrigger=PropertyChanged}"/>
<!--数据源流向目标对象一次OneTime,只有在程序初始化时根据数据源流向目标对象1次-->
<Slider Name="slider3" Background="Red" Foreground="Green" Maximum="100" Value="30"/>
<TextBox Text="{Binding ElementName=slider3, Path=Value, Mode=OneTime, UpdateSourceTrigger=PropertyChanged}"/>

UpdateSourceTrigger​控制何时触发修改操作,以下代码着重演示了Explicit​的使用,当点击

<!-- LostFocus: 控件失去焦点时执行 -->
<!-- PropertyChanged: 属性发生变化时执行 -->
<!-- Explicit: 当调用UpdateSource时触发 -->

<Slider Name="slider1" Background="Red" Foreground="Green" Maximum="100"/>
<TextBox Name="a" Text="{Binding ElementName=slider1, Path=Value, Mode=TwoWay, UpdateSourceTrigger=Explicit}"/>
<Button Content="更新" Click="Button_Click_1" />
private void Button_Click_1(object sender, RoutedEventArgs e)
{
  // 控件.GetBindingExpression(控件类型.xxxProperty).UpdateSource();  调用该方法执行数据的更新
  a.GetBindingExpression(TextBox.TextProperty).UpdateSource();
}

绑定类成员

除了会将控件的某些属性绑定到其他控件属性上之外,还经常会将控件的属性绑定到类的成员上,绑定类成员的方式如下

绑定当前类成员

如果需要将当前窗体控件绑定到当前类的某个属性上,需要以下步骤

  1. 给当前window添加name属性并命名
  2. 将类属性绑定到控件上即可
<Window x:Class="WpfApp1.MainWindow"
        Name="main">
  <StackPanel Name="panel" VerticalAlignment="Top">
    <TextBox Text="{Binding MyName, ElementName=main, UpdateSourceTrigger=PropertyChanged}"/>
  </StackPanel>
</Window>

另一种做法是使用相对资源设置DataContext​,代码如下

<Window.DataContext>
    <Binding RelativeSource="{RelativeSource FindAncestor,AncestorType=local:MainWindow}"/>
</Window.DataContext>
<TextBlock Text="{Binding Path=属性名}"/>

绑定其他类成员

绑定其他类成员的方式要麻烦一点

  1. 导入对应类的命名空间
  2. 引入资源(引入你要绑定哪个类)
  3. 声明绑定数据上下文对象
  4. 绑定数据源的属性
<Window x:Class="WpfApp1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
        mc:Ignorable="d"
        Title="MainWindow" Height="764" Width="780"
        xmlns:local="clr-namespace:WpfApp1">
    <!-- 1、引入命名空间,命名为local -->
    <StackPanel Name="panel" VerticalAlignment="Top">
        <!-- 2、引入资源(类),并命名为abc-->
        <StackPanel.Resources>
            <!-- <命名空间名:类名 x:Key="该类别名"/> -->
            <local:MyClass x:Key="abc"/>
        </StackPanel.Resources>
        <!-- 3、设置当前容器的数据上下文为abc-->
        <StackPanel.DataContext>
            <Binding Source="{StaticResource abc}"/>
        </StackPanel.DataContext>
        <!-- 4、数据绑定 -->
        <TextBox Text="{Binding Path=TestName, UpdateSourceTrigger=PropertyChanged}"/>
    </StackPanel>
</Window>

绑定多个类中的属性

如果我们有两个TextBox​,其中一个绑定Class1的属性,另一个绑定Class2的属性,如果还这么写,程序就会报错,因为一个DataContext​中只能存在一个Binding

再考虑一个场景,如果两个类中都拥有相同的属性,那他该去找哪个呢?

<StackPanel Name="panel" VerticalAlignment="Top">
    <!--引入两个类的资源-->
    <StackPanel.Resources>
        <local:MyClass1 x:Key="one"/>
        <local:MyClass2 x:Key="two"/>
    </StackPanel.Resources>
    <!--设置某个类为数据上下文-->
    <StackPanel.DataContext>
        <Binding Source="{StaticResource one}"/>
    </StackPanel.DataContext>
    <!--绑定的属性默认去当前数据上下文查找-->
    <TextBox Text="{Binding MyName}" />
    <!--也可以指定让他去哪个资源中查找-->
    <TextBox Text="{Binding MyName, Source={StaticResource one}}" />
    <TextBox Text="{Binding MyName, Source={StaticResource two}}" />
</StackPanel>

另一种写法是直接修改对应控件的DataContext​属性

<TextBox Text="{Binding MyName}" DataContext="{Binding Source={StaticResource two}}" />

或者在隐藏代码中这么写

MyClass1 class1 = new MyClass1() { MyName ="哈啊哈哈" };
private void Window_Loaded(object sender, RoutedEventArgs e)
{
    this.控件名.DataContext = class1;
}

<TextBlock x:Name="a" Text="{Binding MyName}"/>

绑定集合视图

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

什么是集合视图

集合视图这一层基于绑定源集合,它允许基于排序、筛选和分组查询来导航并显示源集合,而无需更改基础源集合本身。

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

视图的创建

创建并使用视图的一种方式是直接实例化视图对象,然后将它用作绑定源。而不是绑定真正的数据源

<XXX.Resources>
    <!-- 和创建普通的资源类似,将控件名修改为CollectionViewSource即可 -->
    <CollectionViewSource 
        Source="{Binding Path=Ps, RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=local:MainWindow}}"
        x:Key="list"
    />
</XXX.Resources>

<DataGrid ItemsSource="{Binding Source={StaticResource list}}">
    <DataGrid.Columns>
        <DataGridTextColumn Header="姓名" Binding="{Binding Path=Name}"/>
        <DataGridTextColumn Header="年龄" Binding="{Binding Path=Age}"/>
    </DataGrid.Columns>
</DataGrid> 

集合视图的操作

创建并使用集合视图的一种方式是指定集合视图作为绑定源。 WPF 还会为用作绑定源的每个集合创建一个默认集合视图。 如果直接绑定到集合,WPF 会绑定到该集合的默认视图。

获取集合视图

CollectionViewSource view = (CollectionViewSource)this.xxx.FindResource("list");

视图的排序

// 两个参数分别为排序的列以及排序的方式(正序|倒序)
view.SortDescriptions.Add(new System.ComponentModel.SortDescription("Age", System.ComponentModel.ListSortDirection.Ascending));

视图的过滤

// 添加一个过滤条件
view.Filter += (s, e) =>
{
    // 通过设置Accepted属性是否为true表示改数据是否通过筛选
    e.Accepted = false;
    if (e.Item is People p && p.Age >= 18)
    {
        e.Accepted = true;
    }
};

视图的分组

view.GroupDescriptions.Add(new PropertyGroupDescription() { PropertyName = "要分组的列名称" });

关于DataContext

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

DataContext的绑定方式

<object.DataContext>
  <!-- 绑定某个静态资源 -->
  <Binding Source="{StaticResource abc}"/>
</object.DataContext>
<object.DataContext>
  <!-- 绑定当前窗体类 -->
  <Binding RelativeSource="{RelativeSource FindAncestor,AncestorType=local:MainWindow}"/>
</object.DataContext>
<object.DataContext>
  <!-- 绑定某个命名空间下的某个类 -->
  <namespace:ClassName />
</object.DataContext>

也通常在隐藏代码中进行数据的绑定,这样绑定的上下文在xaml中没有类型提示,还需要在xaml中进行类型的标注

xxx.DataContext = new Class();
<object d:DataContext="{d:DesignInstance Type=namespace:ClassName}" />