参考文档:Data binding overview - WPF .NET | Microsoft Learn

UI元素可以绑定到不同的数据源(.net对象和XML),可以实现数据绑定的控件:

  • ContentControl或其继承类(如Button),可以绑定到单项数据 
  • ItemsControl或其继承类(如ListBox)可以绑定到一个集合

什么是数据绑定

数据绑定是一个UI和它显示数据建立联系的过程。如果建立了正确的绑定,当数据发生变化并发出适当的通知时,UI元素也会自动跟着变化,当UI元素的数据表现发生变化与之绑定的数据也会自动跟着变化。WPF中只有元素的依赖属性可以绑定到.NET对象。

WPF中元素的依赖属性可以被绑定到.NET对象和XML数据上。

数据绑定的概念

不管什么元素也不管数据源的性质如何,每个绑定总遵循如下图所示的模型:

1. WPF DataBinding--概述_数据

数据绑定是绑定目标和绑定源的桥梁,上图展示了以下数据绑定概念:

  • 每个绑定有4个组件
    1. 绑定目标对象
    2. 目标属性
    3. 绑定源
    4. 绑定源值的路径
  • 绑定目标必须是依赖属性,大多数的UIElement属性都是依赖属性,除了只读属性外,大多数的依赖属性默认都支持数据绑定(只有DependencyObject对象可以定义依赖属性,并且所有的UIElement类型都继承自DependencyObject)。
  • 绑定源并不局限于.net对象,如ADO.net,Web服务器对象或者XMLa的节点数据都可以。

需要注意,建立绑定是将目标对象绑定到源对象。

Data context

在XAML中声明的数据绑定通过查找FrameworkElement.DataContext 属性进行解析。这个属性有继承性,如果元素本身没有指这个属性则向其父元素查找,直到XAML的根元素。

也可以为数据绑定指定解析对象。例如,当您将一个对象的前景色绑定到另一个对象的背景色时,可以直接指定源对象。不需要数据上下文,因为在这两个对象之间解析了绑定。

当DataContext属性改变时,所有可能受数据上下文影响的绑定都会被重新计算。

数据流方向

通过设置Binding.Mode控制数据流向, 下图展示了不同类型的数据流向:

1. WPF DataBinding--概述_数据_02

触发更新源

1. WPF DataBinding--概述_数据_03

创建绑定

<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>

因为使用了默认的转换器,所以ColorName(String)可以直接给Background(Brush)使用。

指定绑定源

上面的例子中,通过设置DockPanel.DataContext属性指定绑定源。Button元素继承了父元素(DockPanel)的DataContext.绑定源是绑定的四大必须组件之一,如果少了绑定源,绑定将不会做任何事情。

除DataContext属性外,还有其指定绑定源的方式:

<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>

指定值的路径

如果源是一个对象则使用Binding.Path属性, 如果源是XML则使用Binding.XPath. 如果你要绑定整个对象,则不用指定Path属性。

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

绑定和绑定表达式

BindingExpression维护源之目标之间的连接。BindingExpression不能共享,Binding对象定义了绑定的许多特性,可以在多个BindingExpression之间共享。

下面的例子中,将myText.TextProperty(依赖属性)绑定到

// 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对象将复选框的文本内容绑定到ColorName,在该场景中,将有两个BindingExpression实例共享myBinding对象。

数据转换

1. WPF DataBinding--概述_UI_04

如果没有指定则使用默认的转换器。

1. WPF DataBinding--概述_数据_05

也可自定义转换器

[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;
    }
}

绑定到集合

可以枚举实现了IEnumerable接口的集合。但是,要设置动态绑定以便集合中的插入或删除自动更新UI,集合必须实现INotifyCollectionChanged接口。此接口公开一个事件,该事件应在基础集合更改时引发。

WPF提供的ObservableCollection<T>类实现INotifyCollectionChanged接口。

集合视图

ItemsControl绑定到集合后,如果对数据它进行排序,过滤,分组等就需要使用集合视图,视图集合是实现ICollectionView接口的类。

什么是集合视图

集合视图是绑定源集合之上的一层,允许您根据排序、筛选和分组查询导航和显示源集合,而无需更改底层源集合本身。集合视图还维护一个指向集合中当前项的指针。

因为视图不会改变源数据,所以一个源数据集合可以创建多个视图。

如何创建一个集合视图

创建和使用视图的一种方法是直接实例化视图对象,然后将其用作绑定源。

实例化视图

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

使用视图

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

使用默认视图

WPF为每个集创建了默认视图,绑定中如果直接使用源集合等于使用默认视图集。

获取默认视图的方法:

myCollectionView = (CollectionView)
    CollectionViewSource.GetDefaultView(rootElem.DataContext);

当前项指针

集合视图有当前项的概念, 下面的代码假设DataContext是一个集合:

<!-- 整个集合 -->
<Button Content="{Binding }" />
<!-- 集合当前项 -->
<Button Content="{Binding Path=/}" />
<!-- 当前项的Description属性 -->
<Button Content="{Binding Path=/Description}" />

遍历具有层次结构的集合。下面的示例绑定到名为Offices的集合的当前项,该集合是源集合当前项的属性。

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

主从绑定场景

<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"/>

ListBox 和 ContentControl 都绑定为同一个数据源{Binding Source={StaticResource listingDataView}}因为单个数据(ContentControl)绑定到数据视图时,它自动绑定到视图的当前项。CollectionViewSource对象自动同步当前的选择。如ListBox对象不是绑定到CollectionViewSource需把IsSynchronizedWithCurrentItem设置为true.

一个简单的例子:

class Student
{
    public int  Age { get; set; }
    public string Name { get; set; } = "";
    public string Class { get; set; } = "defaultClass";
}
class StudentViewModel:ObservableObject
{
    private readonly Student _student = new Student();
    public int Age { get => _student.Age; set => SetProperty(_student.Age, value, _student,(s, v)=>s.Age = v); }
    public string Name { get=>_student.Name; set => SetProperty(_student.Name, value, _student, (s,v)=>s.Name = v); }
    public string Class { get=>_student.Class; set => SetProperty(_student.Class, value, _student, (s,v)=>s.Class = v); }
}
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
    public MainWindow()
    {
        ObservableCollection<StudentViewModel> students = new ObservableCollection<StudentViewModel>();
        students.Add(new StudentViewModel { Age = 20, Name = "张三", Class = "1班" });
        students.Add(new StudentViewModel { Age = 10, Name = "张四", Class = "2班" });
        students.Add(new StudentViewModel { Age = 30, Name = "张五", Class = "2班" });
        students.Add(new StudentViewModel { Age = 50, Name = "张六", Class = "3班" });
        DataContext = students;
        InitializeComponent();
    }
}
<Grid>
       <Grid.ColumnDefinitions>
           <ColumnDefinition />
           <ColumnDefinition />
       </Grid.ColumnDefinitions>
       <ListBox
           DisplayMemberPath="Name"
           IsSynchronizedWithCurrentItem="True"
           ItemsSource="{Binding}" />
       <ContentControl Grid.Column="1" Content="{Binding}">
           <ContentControl.ContentTemplate>
               <DataTemplate>
                   <StackPanel Orientation="Vertical">
                       <StackPanel Orientation="Horizontal">
                           <TextBlock>姓名:</TextBlock>
                           <TextBox Width="120" Text="{Binding Name}" />
                       </StackPanel>
                       <StackPanel Orientation="Horizontal">
                           <TextBlock>班级:</TextBlock>
                           <TextBox Width="120" Text="{Binding Class}" />
                       </StackPanel>
                       <StackPanel Orientation="Horizontal">
                           <TextBlock>年龄:</TextBlock>
                           <TextBox Width="120" Text="{Binding Age}" />
                       </StackPanel>
                   </StackPanel>
               </DataTemplate>
           </ContentControl.ContentTemplate>
       </ContentControl>
   </Grid>

排序

private void CheckBox_Checked(object sender, RoutedEventArgs e)
 {
     var view = (CollectionView)CollectionViewSource.GetDefaultView(DataContext);
     view.SortDescriptions.Add(new System.ComponentModel.SortDescription("Age", System.ComponentModel.ListSortDirection.Ascending));
 }

 private void CheckBox_Unchecked(object sender, RoutedEventArgs e)
 {
     var view = (CollectionView)CollectionViewSource.GetDefaultView(DataContext);
     view.SortDescriptions.Clear();
 }

过滤

private void AddFiltering(object sender, RoutedEventArgs args)
 {
    var view = (CollectionView)CollectionViewSource.GetDefaultView(DataContext);
     view.Filter += ShowOnlyBargainsFilter;
 }

 private void RemoveFiltering(object sender, RoutedEventArgs args)
 {
      var view = (CollectionView)CollectionViewSource.GetDefaultView(DataContext);
     view.Filter -= ShowOnlyBargainsFilter;
 }

分组

private void AddGrouping(object sender, RoutedEventArgs args)
 {
     // This groups the items in the view by the property "Class"
     var groupDescription = new PropertyGroupDescription {PropertyName = "Class"};
     var view = (CollectionView)CollectionViewSource.GetDefaultView(DataContext);
     view.GroupDescriptions.Add(groupDescription);
 }
private void RemoveGrouping(object sender, RoutedEventArgs args)
{
    var view = (CollectionView)CollectionViewSource.GetDefaultView(DataContext);
    view.GroupDescriptions.Clear();
}

ListBox分组模板

<ListBox.GroupStyle>
    <GroupStyle
        HeaderTemplate="{StaticResource GroupingHeaderTemplate}" />
</ListBox.GroupStyle>

...
<DataTemplate x:Key="GroupingHeaderTemplate">
    <TextBlock Text="{Binding Path=Name}" Style="{StaticResource GroupHeaderStyle}"/>
</DataTemplate>

...
 <Style x:Key="GroupHeaderStyle" TargetType="TextBlock">
     <Setter Property="FontWeight" Value="Bold" />
     <Setter Property="FontSize" Value="12" />
     <Setter Property="Foreground" Value="Navy" />
     <Style.Triggers>
         <!--High contrast support-->
         <DataTrigger Binding="{Binding Path=(SystemParameters.HighContrast)}" Value="true" >
             <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.WindowTextBrushKey}}" />
         </DataTrigger>
     </Style.Triggers>
 </Style>

数据模板

如果没有模板

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

1. WPF DataBinding--概述_数据_06


<ListBox IsSynchronizedWithCurrentItem="True" ItemsSource="{Binding}">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="{Binding Name}" />
                <TextBlock>
                    >>
                </TextBlock>
                <TextBlock Text="{Binding Age}" />
                <TextBlock>
                    >>
                </TextBlock>
                <TextBlock Text="{Binding Class}" />
            </StackPanel>
        </DataTemplate>
    </ListBox.ItemTemplate>
    <ListBox.GroupStyle>
        <GroupStyle>
            <GroupStyle.HeaderTemplate>
                <DataTemplate>
                    <TextBlock FontSize="24" Text="{Binding Name}" />
                </DataTemplate>
            </GroupStyle.HeaderTemplate>
        </GroupStyle>
    </ListBox.GroupStyle>
</ListBox>

GroupStyle.HeaderTemplate 绑定的数据类型是CollectionViewGroup

数据校验

对用户输入的数据进行校验,以确保输入的信息是正确的。

将验证规则与绑定关联

 <StackPanel Orientation="Horizontal">
     <TextBlock>年龄:</TextBlock>
     <TextBox Width="120">
         <TextBox.Text>
             <Binding Path="Age">
                 <Binding.ValidationRules>
                     <ExceptionValidationRule />
                 </Binding.ValidationRules>
             </Binding>
         </TextBox.Text>
     </TextBox>
 </StackPanel>

当age输入的不是数字时,边框显红色。

自定义校验规则

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;
    }
}

提供视觉反馈

<TextBox Width="120">
    <Validation.ErrorTemplate>
        <ControlTemplate>
            <DockPanel>
                <TextBlock VerticalAlignment="Center" Foreground="Red">!</TextBlock>
                <AdornedElementPlaceholder />
            </DockPanel>
        </ControlTemplate>
    </Validation.ErrorTemplate>
    <TextBox.Text>
        <Binding Path="Age">
            <Binding.ValidationRules>
                <ExceptionValidationRule />
            </Binding.ValidationRules>
        </Binding>
    </TextBox.Text>

</TextBox>

Validation.ErrorTemplate是附加属性, 当校验出错时使用此模板显示控件

把错误信息显示到ToolTip上

<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>