目录
- 1. 定义
- 2. 背景
- 3. Binding源
- 3.1. 使用Data Context作为Binding的源
- 3.2. 使用LINQ检索结果作为Binding的源
- 4. Binding对数据的转换和校验
- 4.1. 需求
- 4.2. 实现步骤
- 4.3. 值转换和校验的好处
- 4.3.1. 数据转换的好处
- 4.4. 数据校验的好处
- 4.5. 原理
- 4.5.1. 值转换器原理
- 4.5.2. 数据校验原理
前面的博文我们讨论了Binding的Path知道了如何在一个对象身上寻找数据。本篇继续看如何为Binding指定Source。
1. 定义
Binding
,出于方便业界一直使用Binding
一词的音译,即“绑定”。我理解Binding
更注重表达它是一种像桥梁一样的关联关系。WPF中,正是在这段桥梁上我们有机会为往来流通的数据做很多事情。
Binding
在源与目标之间架起了沟通的桥梁,默认情况下数据既能够通过Binding
送达目标,也能够从目标返回源(收集用户对数据的修改)。
有时候数据只需要展示给用户、不允许用户修改,这时候可以把Binding模式更改为从源向目标的单向沟通。Binding
还支持从目标向源的单向沟通以及只在Binding
关系确立时读取一次数据,这需要我们根据实际情况去选择。
控制Binding
数据流向的属性是Mode
, 它的类型是BindingMode
枚举。
BindingMode
可取值为:
TwoWay
OneWay
OnTime
OneWayToSource
Default
这里的Default
值是指Binding
的模式会根据目标的实际情况来确定,比如若是可编辑的(如TextBox.Text
属性),Default就采用双向模式;
若是只读的(如TextBlock.Text
)则采用单向模式。
2. 背景
让我们回归程序的本质。程序的本质是数据加算法,用户给进一个输入,经过算法的处理程序会反馈一个输出。
这里,数据处于程序的核心地位。反过头来再看“UI驱动程序”,数据处于被动地位,总是在等待程序接收来自UI的消息/事件后被处理或者算法完成处理后被显示。
如何在GUI编程时把数据的地位由被动变主动、让数据回归程序的核心呢?这就是WPF中的Data Binding
的背景。
WPF具有这种能力的关键是它引入了Data Binding
概念以及与之配套的Dependency Property
系统和DataTemplate
。
在从传统的Windows Form
迁移到WPF之后,对于一个三层程序而言,数据存储层由数据库和文件系统来构建,数据传输和处理仍然使用.NET Framwork
的 ADO.NET
等基本类(与Windows Form
等开发一样),展示层则使用WPF类库来实现,而展示层与逻辑层的沟通就使用Data Bindin
g`来实现。
可见Data Binding
在WPF系统中起到的是数据高速公路的作用。有了这条高速公路,加工好的数据会自动送达用户界面加以显示,被用户修改过的数据也会自动传回逻辑层, 一旦数据被加工好又会被送达用户界面……程序的逻辑层就像一个强有力的引擎不停运转,用加工好的数据 驱动程序的用户界面以文字、图形、动画等形式把数据显示出来——这就是“数据驱动UI”。
3. Binding源
Binding的源是数据的来源,所以,只要一个对象包含数据并能通过属性把数据暴露出来,它就能当作Binding的源来使用。包含数据的对象比比皆是,但必须为Binding的Source指定合适的对象Binding才能正确工作,常见的办法有:
- 把普通CLR类型单个对象指定为Source:包括.NETFramework自带类型的对象和用户自定义类型的对象。如果类型实现了INotifyPropertyChanged接口,则可通过在属性的set语句里激发PropertyChanged事件来通知Binding数据已被更新。
- 把普通CLR集合类型对象指定为Source:包括数组、List、ObservableCollection等集合类型。实际工作中,我们经常需要把一个集合作为ItemsControl派生类的数据源来使用,一般是把控件的ItemsSource属性使用Binding关联到一个集合对象上。
- 把ADO.NET数据对象指定为Source:包括DataTable和DataView等对象。
- 使用XmlDataProvider把XML数据指定为Source:XML作为标准的数据存储和传输格
式几乎无处不在,我们可以用它表示单个数据对象或者集合;一些WPF控件是级联式的
(如TreeView和Menu),我们可以把树状结构的XML数据作为源指定给与之关联的Binding。
- 把依赖对象(DependencyObject)指定为Source:依赖对象不仅可以作为Binding的目标,同时也可以作为Binding的源。这样就有可能形成Binding链。依赖对象中的依赖属性可以作为Binding的Path。
- 把容器的DataContext指定为Source(WPFDataBinding的默认行为):有时候我们会遇到这样的情况——我们明确知道将从哪个属性获取数据,但具体把哪个对象作为Binding源还不能确定。这时候,我们只能先建立一个Binding、只给它设置Path而不设置Source,让这个Binding自己去寻找Source。这时候,Binding会自动把控件的DataContext当作自己的Source(它会沿着控件树一层一层向外找,直到找到带有Path指定属性的对象为止)。
- 通过ElementName指定Source:在C#代码里可以直接把对象作为Source赋值给Binding,但XAML无法访问对象,所以只能使用对象的Name属性来找到对象。
- 通过Binding的RelativeSource属性相对地指定Source:当控件需要关注自己的、自己容器的或者自己内部元素的某个值就需要使用这种办法。
- 把ObjectDataProvider对象指定为Source:当数据源的数据不是通过属性而是通过方法暴露给外界的时候,我们可以使用这两种对象来包装数据源再把它们指定为Source。
- 把使用LINQ检索得到的数据对象作为Binding的源。
3.1. 使用Data Context作为Binding的源
前面的例子都是把单个CLR类型对象指定为Binding的Source,方法有两种——把对象赋值给Binding.Source属性或把对象的Name赋值给Binding.ElementName。
DataContext属性被定义在FrameworkElement类里,这个类是WPF控件的基类,这意味着所有WPF控件(包括容器控件)都具备这个属性。
如前所述,WPF的UI布局是树形结构,这棵树的每个结点都是控件,由此我们推出另一个结论——在UI元素树的每个结点都有DataContext。
这一点非常重要,因为当一个Binding只知道自己的Path而不知道自己的Soruce时,它会沿着UI元素树一路向树的根部找过去,
每路过一个结点就要看看这个结点的DataContext是否具有Path所指定的属性。如果有,那就把这个对象作为自己的Source;如果没有,那就继续找下去;
如果到了树的根部还没有找到,那这个Binding就没有Source,因而也不会得到数据。
先创建一个名为Student的类,它具有Id、Name、Age三个属性:
public class Student
{
public int Id { get; set; }
public string Name { get; set; }
public int Age { get; set; }
}
<Window x:Class="WpfApp1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApp1"
Title="WPF 绑定进阶" Height="450" Width="800">
<StackPanel Background="LightBlue" Margin="10">
<!-- DataContext 设置为 Student 对象 -->
<StackPanel.DataContext>
<local:Student Id="6" Name="Tim" Age="29" />
</StackPanel.DataContext>
<!-- 绑定到 Student 的属性 -->
<TextBox Text="{Binding Path=Id, Mode=TwoWay}" Margin="5" />
<TextBox Text="{Binding Path=Name, Mode=TwoWay}" Margin="5" />
<TextBox Text="{Binding Path=Age, Mode=TwoWay}" Margin="5" />
</StackPanel>
</Window>
这个UI布局可以下面树状图来表示:
在实际工作中DataContext的用法是非常灵活的。比如:
(1)当UI上的多个控件都使用Binding关注同一个对象时,不妨使用DataContext。
(2)当作为Source的对象不能被直接访问的时候——比如B窗体内的控件想把A窗体内的控件当作自己的Binding源时,
但A窗体内的控件是private访问级别,这时候就可以把这个控件(或者控件的值)作为窗体A的DataContext(这个属性是public访问级别的)从而暴露数据。
形象地说,这时候外层容器的DataContext就相当于一个数据的“制高点”,只要把数据放上去,别的元素就都能看见。
另外,DataContext本身也是一个依赖属性,我们可以使用Binding把它关联到一个数据源上。
3.2. 使用LINQ检索结果作为Binding的源
自3.0版开始,.NET Framework开始支持LINQ(Language-IntegratedQuery,语言集成查询),
使用LINQ,我们可以方便地操作集合对象、DataTable对象和XML对象而不必动辄就把好几层foreach循环嵌套在一起却只是为了完成一个很简单的任务。
LINQ查询的结果是一个IEnumerable类型对象,而IEnumerable又派生自IEnumerable,所以它可以作为列表控件的ItemsSource来使用。
我们先来看查询集合对象。要从一个已经填充好的 List对象中检索出所有名字以字母T 开头的学生,代码如下:
<StackPanel Background="LightBlue" Margin="10">
<ListView x:Name="listViewStudents" Height="143" Margin="5">
<ListView.View>
<GridView>
<GridViewColumn Header="Id" Width="60" DisplayMemberBinding="{Binding Id}" />
<GridViewColumn Header="Name" Width="100" DisplayMemberBinding="{Binding Name}" />
<GridViewColumn Header="Age" Width="80" DisplayMemberBinding="{Binding Age}" />
</GridView>
</ListView.View>
</ListView>
<Button Content="Load" Height="25" Margin="5" Click="Button_Click" />
</StackPanel>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void Button_Click(object sender, RoutedEventArgs e)
{
List<Student> stuList = new List<Student>
{
new Student { Id = 0, Name = "Tim", Age = 29 },
new Student { Id = 1, Name = "Tom", Age = 28 },
new Student { Id = 2, Name = "Kyle", Age = 27 },
new Student { Id = 3, Name = "Tony", Age = 26 },
new Student { Id = 4, Name = "Vina", Age = 25 },
new Student { Id = 5, Name = "Mike", Age = 24 },
};
this.listViewStudents.ItemsSource = from stu in stuList where stu.Name.StartsWith("T") select stu;
}
}
4. Binding对数据的转换和校验
前面我们已经知道,Binding的作用就是架在Source与Target之间的桥梁,数据可以在这座桥梁的帮助下来流通。
就像现实世界中的桥梁会设置一些关卡进行安检一样,Binding这座桥上也可以设置关卡对数据的有效性进行检验,不仅如此,当Binding两端要求使用不同的数据类型时,我们还可以为数据设置转换器。
WPF中通过**值转换器(IValueConverter)和数据校验(IDataErrorInfo或INotifyDataErrorInfo)**来实现数据的转换和校验。
这种机制使得数据在显示和更新时更加灵活和安全。
4.1. 需求
假设我们有一个Person类,包含以下属性:
Age:用户的年龄(整数)。
IsAdult:一个布尔值,表示用户是否成年(年龄是否大于等于18)。
我们需要实现以下功能:
数据转换:将Age属性的值转换为布尔值IsAdult,并在UI中显示。
数据校验:确保用户输入的年龄是有效的(例如,年龄必须大于0)。
4.2. 实现步骤
(1) 创建Person类
Person类实现INotifyPropertyChanged接口,用于支持数据绑定的更新,并实现IDataErrorInfo接口用于数据校验。
文件:Person.cs
using System.ComponentModel;
namespace WpfDataBindingExample
{
public class Person : INotifyPropertyChanged, IDataErrorInfo
{
private int age;
public int Age
{
get => age;
set
{
if (age != value)
{
age = value;
OnPropertyChanged(nameof(Age));
OnPropertyChanged(nameof(IsAdult)); // 通知依赖属性更新
}
}
}
public bool IsAdult => Age >= 18;
// 实现 INotifyPropertyChanged 接口
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
// 实现 IDataErrorInfo 接口
public string Error => null; // 不需要整体校验
public string this[string columnName]
{
get
{
switch (columnName)
{
case nameof(Age):
if (Age <= 0)
return "年龄必须大于0";
break;
}
return null; // 无错误
}
}
}
}
(2) 创建值转换器
创建一个值转换器,将Age转换为布尔值IsAdult。
文件:AgeToAdultConverter.cs
using System;
using System.Globalization;
using System.Windows.Data;
namespace WpfDataBindingExample
{
public class AgeToAdultConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is int age)
{
return age >= 18;
}
return false;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException(); // 不需要反向转换
}
}
}
(3) XAML文件
在XAML中,使用Binding将Person对象的属性绑定到UI,并使用值转换器和数据校验。
文件:MainWindow.xaml
<Window x:Class="WpfDataBindingExample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfDataBindingExample"
Title="Data Binding with Conversion and Validation" Height="200" Width="400">
<Window.Resources>
<local:AgeToAdultConverter x:Key="AgeToAdultConverter" />
</Window.Resources>
<StackPanel Margin="10">
<TextBox x:Name="AgeTextBox"
Width="100"
Margin="5"
VerticalContentAlignment="Center"
Text="{Binding Age, Mode=TwoWay, ValidatesOnDataErrors=True}" />
<TextBlock Text="是否成年:" Margin="5" />
<TextBox IsReadOnly="True"
Background="LightGray"
Width="100"
Margin="5"
VerticalContentAlignment="Center"
Text="{Binding IsAdult, Converter={StaticResource AgeToAdultConverter}}" />
</StackPanel>
</Window>
(4) 代码后台
在代码后台中,初始化Person对象并设置为DataContext。
文件:MainWindow.xaml.cs
using System.Windows;
namespace WpfDataBindingExample
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new Person { Age = 20 }; // 初始化数据
}
}
}
4.3. 值转换和校验的好处
4.3.1. 数据转换的好处
- 灵活性:值转换器允许在绑定过程中对数据进行任意转换,例如格式化、逻辑判断等。
- 解耦:将数据转换逻辑从UI代码中分离出来,便于维护和复用。
4.4. 数据校验的好处
- 用户体验:在用户输入无效数据时,及时给出反馈,提升用户体验。
- 数据完整性:确保绑定到模型的数据始终符合业务规则,避免无效数据进入后端逻辑。
- 安全性:防止用户输入非法数据,减少潜在的安全风险。
4.5. 原理
4.5.1. 值转换器原理
IValueConverter接口定义了Convert和ConvertBack方法。
Convert方法用于将源数据转换为目标数据。
ConvertBack方法用于将目标数据转换回源数据(可选实现)。
在XAML中通过Binding的Converter属性指定值转换器。
4.5.2. 数据校验原理
IDataErrorInfo接口允许在数据绑定时对属性进行校验。
如果校验失败,Binding会自动将错误信息显示在UI上(例如,输入框显示红色边框)。
校验逻辑在this[string columnName]属性中实现,返回错误信息或null。
通过上述实现,我们可以在WPF中灵活地处理数据绑定的转换和校验,提升应用程序的用户体验和数据安全性。