参考文档:Data binding overview - WPF .NET | Microsoft Learn
UI元素可以绑定到不同的数据源(.net对象和XML),可以实现数据绑定的控件:
- ContentControl或其继承类(如Button),可以绑定到单项数据
- ItemsControl或其继承类(如ListBox)可以绑定到一个集合
什么是数据绑定
数据绑定是一个UI和它显示数据建立联系的过程。如果建立了正确的绑定,当数据发生变化并发出适当的通知时,UI元素也会自动跟着变化,当UI元素的数据表现发生变化与之绑定的数据也会自动跟着变化。WPF中只有元素的依赖属性可以绑定到.NET对象。
WPF中元素的依赖属性可以被绑定到.NET对象和XML数据上。
数据绑定的概念
不管什么元素也不管数据源的性质如何,每个绑定总遵循如下图所示的模型:
数据绑定是绑定目标和绑定源的桥梁,上图展示了以下数据绑定概念:
- 每个绑定有4个组件
1. 绑定目标对象
2. 目标属性
3. 绑定源
4. 绑定源值的路径 - 绑定目标必须是依赖属性,大多数的UIElement属性都是依赖属性,除了只读属性外,大多数的依赖属性默认都支持数据绑定(只有DependencyObject对象可以定义依赖属性,并且所有的UIElement类型都继承自DependencyObject)。
- 绑定源并不局限于.net对象,如ADO.net,Web服务器对象或者XMLa的节点数据都可以。
需要注意,建立绑定是将目标对象绑定到源对象。
Data context
在XAML中声明的数据绑定通过查找FrameworkElement.DataContext
属性进行解析。这个属性有继承性,如果元素本身没有指这个属性则向其父元素查找,直到XAML的根元素。
也可以为数据绑定指定解析对象。例如,当您将一个对象的前景色绑定到另一个对象的背景色时,可以直接指定源对象。不需要数据上下文,因为在这两个对象之间解析了绑定。
当DataContext属性改变时,所有可能受数据上下文影响的绑定都会被重新计算。
数据流方向
通过设置Binding.Mode控制数据流向, 下图展示了不同类型的数据流向:
触发更新源
创建绑定
<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对象。
数据转换
如果没有指定则使用默认的转换器。
也可自定义转换器
[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}" />
<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>