WPF中的数据绑定提供了很强大的功能。与普通的WinForm程序相比,其绑定功能为我们提供了很多便利,例如Binding对象的自动通知/刷新,Converter,Validation Rules,Two Way Binding等功能,省去了很多维护的繁琐工作。另外对于WPF中提供的数据模板功能,让我们可以轻松定制可以被复用的控制呈现的模块—但这是以数据绑定为前提来做到轻松易用的效果的。数据提供者例如XmlDataProvider和ObjectDataProvider更是简化了将对象以特定方式绑定并呈现的过程。可以说,数据绑定是WPF中让我们真正能够开始体现其便利性的特征之一,而对以数据驱动的应用来讲,其重要性不言而喻。
数据绑定的关键是System.Windows.Data.Binding对象,它会把两个对象(UI对象与UI对象之间,UI对象与.NET数据对象之间)按照指定的方式粘合在一起,并在他们之间建立一条通信通道,绑定一旦建立,接下来的应用生命周期中它可以自己独立完成所有的同步工作。根据其应用场合的不同我们将在本文中从以下几个部分分别讨论:
· 对象间的绑定
· 绑定到集合
· 数据模板
· 向绑定添加规则和转换器
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="{Binding ElementName=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里提供了所有本文中的实例。在下一篇中我们会了解以下几个问题:
· Validation Rules
· Triggers