WPF中的数据绑定提供了很强大的功能。与普通的WinForm程序相比,其绑定功能为我们提供了很多便利,例如Binding对象的自动通知/刷新,Converter,Validation Rules,Two Way Binding等功能,省去了很多维护的繁琐工作。另外对于WPF中提供的数据模板功能,让我们可以轻松定制可以被复用的控制呈现的模块—但这是以数据绑定为前提来做到轻松易用的效果的。数据提供者例如XmlDataProvider和ObjectDataProvider更是简化了将对象以特定方式绑定并呈现的过程。可以说,数据绑定是WPF中让我们真正能够开始体现其便利性的特征之一,而对以数据驱动的应用来讲,其重要性不言而喻。
数据绑定的关键是System.Windows.Data.Binding对象,它会把两个对象(UI对象与UI对象之间,UI对象与.NET数据对象之间)按照指定的方式粘合在一起,并在他们之间建立一条通信通道,绑定一旦建立,接下来的应用生命周期中它可以自己独立完成所有的同步工作。根据其应用场合的不同我们将在本文中从以下几个部分分别讨论:
- 对象间的绑定
- 绑定到集合
- 数据模板
- 向绑定添加规则和转换器
一、什么是数据绑定
WPF 中的数据绑定,必须要有绑定目标和要绑定的数据源。绑定目标可以是继承自 DependencyProperty的任何可访问的属性或控件,例如 TextBox 控件的 Text 属性。数据源可以是其他控件的属性,可以是对象实例、XAML 元素、ADO.NET Dataset、XML数据。微软针对XML绑定与对象绑定,提供了两个辅助类XmlDataProvider 和 ObjectDataProvider。
WPF的数据绑定跟ASP.NET与WinForm中的数据绑定有什么不同呢? 最大不同就是WPF使用{Binding …}这一语句。
Binding是用来实现界面控件的属性与后台数据之间的绑定,通过这种形式将前台界面与后台数据联系在一起达到界面与数据耦合的目的。
WPF绑定引擎从 Binding 对象获取有关以下内容的信息:
源对象和目标对象。
数据流的方向。你可以通过设置 Binding.Mode 属性来指定该方向。
值转换器(如果存在)。你可通过将 Converter 属性设置为用来实现 IValueConverter 的类的一个实例,指定值转换器。
WPF与ASP.NET与WinForm中的绑定方式比较,存在着如下几点差异:
(1)Binding可以通过XAML语句实现界面与数据的耦合。如果把Binding比作数据的桥梁,那么它的两端分别是Binding的源和目标。数据从哪里来哪里就是源,Binding是架在中间的桥梁,Binding目标是数据要往哪儿去。一般情况下,Binding源是逻辑层的对象,Binding目标是UI层的控件对象,这样,数据就会源源不断 通过Binding送达UI层,被UI层展现,也就完成了数据驱动UI的过程。如下图。
(2)Binding有一个重要的属性Mode,实现绑定中的数据流向。具体有如下几种。
成员名称 | 说明 |
Default | 使用绑定目标的默认 Mode 值。 每个依赖项属性的默认值都不同。 一般情况下,用户可编辑控件属性(例如文本框和复选框的属性)默认为双向绑定,而多数其他属性默认为单向绑定。 确定依赖项属性绑定在默认情况下是单向还是双向的编程方法是:使用 GetMetadata 获取属性的属性元数据,然后检查 BindsTwoWayByDefault 属性的布尔值。 |
OneTime | 当应用程序启动或数据上下文更改时,更新绑定目标。 此绑定类型适用于以下情况:使用当前状态的快照适合使用的或数据状态实际为静态的数据。 如果要从源属性初始化具有某个值的目标属性,并且事先不知道数据上下文,则也可以使用此绑定类型。 此绑定类型实质上是 OneWay 绑定的简化形式,在源值不更改的情况下可以提供更好的性能。 |
OneWay | 当绑定源(源)更改时,更新绑定目标(目标)属性。 此绑定类型适用于绑定的控件为隐式只读控件的情况。 例如,可以绑定到如股市代号之类的源。 或者,可能目标属性没有用于进行更改(例如表的数据绑定背景色)的控件接口。 如果无需监视目标属性的更改,则使用 OneWay 绑定模式可避免 TwoWay 绑定模式的系统开销。 |
OneWayToSource | 当目标属性更改时更新源属性。 |
TwoWay | 导致对源属性或目标属性的更改可自动更新对方。 此绑定类型适用于可编辑窗体或其他完全交互式 UI 方案。 |
(3)可通过配置触发器,决定用户在界面输入的数据在什么时候去修改数据源中的值。可以通过UpdateSourceTrigger属性实现,具体有如下几种值
成员名称 | 说明 |
Default | 绑定目标属性的默认 UpdateSourceTrigger 值。 大多数依赖项属性的默认值都为 PropertyChanged,而Text 属性的默认值为 LostFocus。 确定依赖项属性的默认 UpdateSourceTrigger 值的编程方法是使用 GetMetadata 来获取属性的属性元数据,然后检查 DefaultUpdateSourceTrigger 属性的值。 |
Explicit | 仅在调用 UpdateSource 方法时更新绑定源。 |
LostFocus | 当绑定目标元素失去焦点时,更新绑定源。 |
PropertyChanged | 当绑定目标属性更改时,立即更新绑定源。 |
|
|
具体用法如下:
<TextBox Name="itemNameTextBox" Text="{Binding Path=ItemName, UpdateSourceTrigger=Explicit}" />
- 1. UI对象间的绑定
UI对象间的绑定,也是最基本的形式,通常是将源对象Source的某个属性值绑定 (拷贝) 到目标对象Destination的某个属性上。源属性可以是任意类型,但目标属性必须是依赖属性(Dependency Property)。通常情况下我们对于UI对象间的绑定源属性和目标属性都是依赖属性 (有些属性不是) ,因为依赖属性有垂直的内嵌变更通知机制,WPF可以保持目标属性和源属性的同步。
看个简单的例子是如何在XAML中实现数据绑定的:
<Window x:Class="Allan.WpfBinding.Demo.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Basic Bindings" Height="400" Width="700" Style="{StaticResource windowStyle}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="40" />
<RowDefinition Height="*" />
<RowDefinition Height="40" />
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal" Margin="5" HorizontalAlignment="Right">
<Button x:Name="btnBasicBinding" Content="Basic" Style="{StaticResource buttonStyle}"></Button>
<Button x:Name="btnCollectionBinding" Content="Collection" Style="{StaticResource buttonStyle}"></Button>
<Button x:Name="btnDataTemplate" Content="Data Template" Style="{StaticResource buttonStyle}"></Button>
<Button x:Name="btnAdvanceBindings" Content="Advance" Style="{StaticResource buttonStyle}"></Button>
<Button x:Name="btnExit" Content="Exit" Style="{StaticResource buttonStyle}"></Button>
</StackPanel>
<StackPanel Grid.Row="1" HorizontalAlignment="Left">
<TextBox x:Name="txtName" Margin="5" Width="400" BorderThickness="0" Height="50" Text="Source Element"></TextBox>
<TextBlock x:Name="tbShowMessage" Margin="5" Width="400" Height="50" Text="{BindingElementName=txtName,Path=Text }" />
</StackPanel>
</Grid>
</Window>
- XAML绑定语法:
上边的代码我们将名为txtName的对象的Text属性作为源对象分别绑定给了两个TextBlock的Text属性。这里我们用了Binding关键字并指定了ElementName和Path,这两个就是指定源对象(Source)和源属性(Source Property). 通常我们在设定绑定时都用与StaticResource标记类似的语法{Binding… }并设置ElementName和Path属性:
Text=”{Binding ElementName=SourceObjectName, Path=SourceProperty}”
- 用Coding(C#)添加Binding
而对于C#里和绑定相关的代码,则看起来会罗嗦很多。但它们都同样的使用了Binding对象,然后指定PropertyPath的一个实例为源属性,然后可以有两个方法来加载绑定规则:
- 1. 调用FrameworkElement 或FrameworkContentElement对象的SetBinding方法
- 2. 调用BindingOperations.SetBinding静态方法
以下代码实现了和上边XAML文件类似的功能:
Binding binding = new Binding();
//设置源对象
binding.Source = txtName;
//设置源属性
binding.Path = new PropertyPath("Text");
//添加到目标属性
this.tbShowMessage.SetBinding(TextBlock.TextProperty, binding);
//or
//BindingOperations.SetBinding(tbShowMessage, TextBlock.TextProperty, binding);
- 用Coding(C#)移除Binding
当你在应用程序中某个地方添加了绑定,而在某个时候又不想这个绑定在接下来继续有效时,你可以有两种方式来断开这个绑定:
- 1. 用BindingOperations.ClearBinding静态方法。
例如BindingOperations.ClearBinding(currentTextBlock, TextBlock.TextProperty); BindingOperations同时还提供了ClearAllBindings方法,只需要传入要清除绑定的目标对象的名称,它就会将所有这个对象的绑定移除。
- 2. 简单的将目标属性设置为一个新的值。
这个简单的方法同样有效,可以断开与前边设置的binding的连接。简单的设置为任何值即可:如:currentTextBlock.Text = “it’s a new value.”;
- Binding对象的属性
Property | Description |
| 转换器 |
| 绑定的源对象 |
| 绑定无法返回有效值时的默认显示。 |
| 绑定方式 |
| 属性 |
| 常用于自身绑定或者数据模板中来指定绑定的源对象。 |
| 源对象 |
| 格式化表达式 |
| Sets the events on which binding will occur. |
| 验证规则 |
总结:对于对象间的绑定,绑定源为ElementName,Path为绑定源属性。ElementName必须为以下可选项之一:
| DataContext是WPF最后才试图查找的源。一旦RelativeSource和Source对象都没有被设置,则会在逻辑树种向上搜寻。 |
| 用来标识和当前控件关联的对象,通常用于自我引用或数据模板。 |
| 数据提供者/对象 |
- 2. 绑定到集合
- 利用ItemsSource来绑定数据源
常用标记:{Binding Path =””} ItemSource DisplayMemberPath
通常来说这是我们在做以数据驱动为主的应用时最经常用到的绑定方式。WPF支持任何类型的.NET对象作为数据源绑定到WPF对象。对于所有的ItemsControl对象都有一个ItemsSource依赖属性,这是专门为数据绑定而准备的。ItemsSource的类型是IEnumerable,所以对于我们几乎所有的集合类型我们都可以轻易的改变成ItemsSource的源对象。通过以下语句我们可以将一个名为photos的集合赋予ListBox对象,并以显示Name属性的值:
<ListBox x:Name=”pictureBox” DisplayMemberPath=”Name” ItemsSource=”(Binding {DynamicResource photos}”
我们知道,依赖属性内建的垂直通知功能让UI对象间的绑定可以自己负责同步处理,但是对于.NET集合/对象来讲,它不具备这样的能力。为了让目标属性与源集合的更改保持同步,源集合必须实现一个叫INotifyCollectionChanged的接口,但通常我们只需要将集合类继承于ObservableCollection类即可。因为ObservableCollection实现了INotifyPropertyChanged和INotifyCollectionChanged接口。示例代码中我们这么去定义Photos集合类:
public class Photos : ObservableCollection<Photo>
- 利用DataContext来作为共享数据源
常用标记:{Binding Path=””} DataContext
顾名思义,DataContext就是数据上下文对象,它是为了避免多个对象共享一个数据源时重复的对所有对象显式地用binding标记每个Source/RelativeSource/ElementName,而把同一个数据源在上下文对象的某个范围内共享,这样当一个绑定没有显式的源对象时,WPF会便利逻辑数找到一个非空的DataContext为止。
例如我们可以通过以下代码给ListBox和Title设置绑定:
<StackPanel Orentation=”Vertical” Margin=”5” DataContext=”{DynamicResource photos}”>
<Label x:Name=”TitleLabel” Content=”{Binding Path=Count}” DockPanel.Dock=”Bottom” />
<ListBox x:Name=”pictureBox” DisplayMemeberPath=”Name” ItemSource=”{Binding}” />
</StackPanel>
对于这些简单的绑定我们可以很灵活的组合他们的应用来达到我们的要求,这也是我们通常使用的方法。例如:
<Window.Resources>
<local:Employee
x:Key="MyEmployee" EmployeeNumber="123" FirstName="John"
LastName="Doe" Department="Product Development" Title="QA Manager" />
</Window.Resources>
<Grid DataContext="{StaticResource MyEmployee}">
<TextBox Text="{Binding Path=EmployeeNumber}"></TextBox>
<TextBox Text="{Binding Path=FirstName}"></TextBox>
<TextBox Text="{Binding Path=LastName}" />
<TextBox Text="{Binding Path=Title}"></TextBox>
<TextBox Text="{Binding Path=Department}" />
</Grid>
总结:对于集合的绑定,通常会需要用到以下几个标记:
| 指定源对象中被显示的属性。ToString()方法会被默认调用。 |
| 指定要显示的数据源 |
| 指定以什么样的格式来显示数据(类似于符合控件,可以在数据模板中利用多种控件来控制展现方式) |
| 数据源对象中的属性—控制显示 |
| 共享数据源 |
- 3. 数据模板 – Data Template
当源属性和目标属性为兼容的数据类型,且源所显示的东西正是你需要显示的东西时,数据绑定确实很简单,你只需要向Section 1中讲的来匹配对象关系即可。而通常情况下我们对数据绑定都要做一些定制,特别对于.NET对象的绑定,你需要将数据源按照不同的方式分割显示。Data Template就负责来完成这样的功能:按照预想的数据展现模式将数据源的不同部分显示,而其作为可以被复用的独立结构,一旦定义可以被添加到一个对象内部,将会创建一个全新的可视树。
数据模板通常会被应用到以下几类控件来填充其类型为DataTemplate的属性:
- 内容控件(Content Control):ContentTemplate属性,控制Content的显示
- 项控件(Items Control) : ItemTemplate属性,应用于每个显示的项
- 头控件(Header Content Control) : HeaderTemplate属性,控制Header的展现。
每个数据模板的定义都是类似的方式,你可以像设计普通的窗体一样来设计其展现的方式,而且他们共享数据模板父空间所赋予的绑定源。例如下边的代码我们用一个图片来替代ListBox中的每一项:
<ListBox x:Name="pictureBox" ItemsSource="{Binding}"ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<ListBox.ItemTemplate>
<DataTemplate>
<Image Source="{Binding Path=FullPath}" Margin="3,8" Height="35">
<Image.LayoutTransform>
<StaticResource ResourceKey="st"/>
</Image.LayoutTransform>
<Image.ToolTip>
<StackPanel>
<TextBlock Text="{Binding Path=Name}"/>
<TextBlock Text="{Binding Path=DateTime}"/>
</StackPanel>
</Image.ToolTip>
</Image>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
最终的ListBox中每一项的展现将按照我们在数据模板中设定的样式以图片来显示:
通常数据模板是不需要被内联声明的,它可以被定义成一个资源存放在Application.Resources这样的全局资源辞典中,或者单独的Resource Dictionary中在多个元素间共享。
- 4. 向绑定添加规则和转换器
- 使用值转换器Value Converter
无论你的绑定XAML写得多么漂亮,所有的绑定值毫无疑问你都可以得到,但是它不总是可以满足你不经过任何程序变化显示出来就能满足要求的。例如对于本文示例代码的照片总数的显示,我们还想显示得更为智能一些:对于一些符合某种要求的数据我们将其背景显示为黄色,而对于有多于一条记录时我们显示15 Items,仅有一条时显示1 Item。这时Value Converter就派上用场了。
要定义一个Value Converter需要声明一个类让其继承于System.Windows.Data.IValueConverter接口,并实现其中的两个方法Convert和ConvertBack方法。
public class RawCountToDescriptionConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter,CultureInfo culture)
{
// Let Parse throw an exception if the input is bad
int num = int.Parse(value.ToString());
return num + (num == 1 ? " item" : " items");
}
public object ConvertBack(object value, Type targetType, object parameter,CultureInfo culture)
{
throw new NotSupportedException();
}
}
在XAML中声明资源,然后将其通过静态资源引用的方式赋予Binding对象的Converter属性。
<Window.Resources>
<local:CountToBackgroundConverter x:Key="myConverter"/>
<local:RawCountToDescriptionConverter x:Key="myConverter2"/>
</Window.Resources>
<TextBlock x:Name="filePath" DockPanel.Dock="Top" Style="{StaticResource titleStyle}"
Text="{Binding Count, Converter={StaticResource myConverter2}}"></TextBlock>
同样,我们可以对输入进行转换。如果数据的输入是被验证规则(如果有的话)标记为有效的,那么值转换器将会被调用,来对输入进行转换后反应出来。 (参考附件代码中的BindingConverter窗体)
- 向绑定添加规则
每个Binding对象都有一个ValidationRules属性,可以被设置为一个或多个派生自ValidationRule的对象,每个规则都会检查特定的条件并更具结果来标记数据的有效性。就像我们在ASP.NET中应用RequiredValidator, CustomValidator一样,你只需要定义自己的规则,WPF会在每次调用数据时(通常是TextBox等输入控件失去焦点)会调用验证检查。这些是在值转换器之前发生的,如果数据无效,它会标记此次更新无效,并将数据标记为无效—这是通过设置目标元素的Validation.HasError属性为true并触发Validation.Error事件(ValidationResult会被返回,并且其IsValid属性为false)。我们可以通过一个触发器来设定当数据无效时对用户的提示。例如下边的代码我们就通过定义一个JpgValidationRule,当数据无效时通过tooltip来提示用户输入无效。
public class JpgValidationRule : ValidationRule
{
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
string filename = value.ToString();
// Reject nonexistent files:
if (!File.Exists(filename))
{
return new ValidationResult(false, "Value is not a valid file.");
}
// Reject files that don’t end in .jpg:
if (!filename.EndsWith(".jpg", StringComparison.InvariantCultureIgnoreCase))
{
return new ValidationResult(false, "Value is not a .jpg file.");
}
else
{
return new ValidationResult(true, null);
}
}
}
上边的代码定义了我们验证的规则。接下来在XAML中来应用这个规则。我们将这个规则用来检测输入框中的数据是否合法:
<TextBox Style="{StaticResource validateTextBoxStyle}">
<TextBox.Text>
<Binding UpdateSourceTrigger="PropertyChanged" Path="Department">
<Binding.ValidationRules>
<local:JpgValidationRule/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
当数据不合法时我们以什么样的方式来告诉用户呢?这里有两个方法可以做,一个是定义你自己的ErrorTemplate,另外一个是根据Trigger来设置一些可见信息。通常我们都可以来自己定义一些Error Provider和可以复用的ErrorTemplate,这个话题我们会在下一篇文章中讲。这里我们只让背景做改变并用tooltip来提示用户—显示的是ValidationRule返回的出错信息。因为都是控制显示的,所以定义成共用的Style:
<Style x:Key="validateTextBoxStyle" TargetType="{x:Type TextBox}">
<Setter Property="Width" Value="300" />
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="True">
<Setter Property="Background" Value="Red"/>
<Setter Property="ToolTip" Value="{Binding RelativeSource={RelativeSource Self}, Path=(Validation.Errors)[0].ErrorContent}"/>
</Trigger>
</Style.Triggers>
</Style>
总的来说,对于验证,我们常用一下几个属性来定义错误验证规则和错误展现方式:
- Errors – 错误信息集合
- HasError – 是否有错误出现.
- ErrorTemplate – 错误提示的展现方式.
- Binding.ValidationRules 绑定验证规则
Coming Next:
本文我们了解了有关Binding以及和绑定有关的附加验证规则,转换器等。附加验证规则我们将在下一篇中了解更多自定义Error Provider,Error Template等。附加的Demo里提供了所有本文中的实例。