说明:本系列基本上是《WPF揭秘》的读书笔记。在结构安排与文章内容上参照《WPF揭秘》的编排,对内容进行了总结并加入一些个人理解。
首先来说一下什么是数据绑定,举一个常见的例子,我们需要在一个ListView中显示来自数据库中的某些数据,当然我们会首先将这些数据变为内存中的对象。然后将ListView的对象与内存中这些数据对象结合在一起完成数据的展现。所以.NET中数据绑定就是将不同类型.NET对象绑定在一起的过程。
数据绑定的核心是System.Windows.Data.Binding对象,其将要绑定的两个.NET对象关联在一起,建立一条通信通到并在应用程序生命周期内负责同步工作。
使用代码完成数据绑定
下面代码演示了在代码中使用Binding对象完成绑定工作:
Binding binding = new Binding(); //设置源对象 binding.Source = treeView; //设置源属性 binding.Path = new PropertyPath("SelectItem.Header"); //设置目标属性--绑定(方法一) textBlock.SetBinding(TextBlock.TextProperty, binding); //绑定方式(方法二) BindingOperations.SetBinding(textBlock, TextBlock.TextProperty, binding);
这样当TreeView对象的SelectedItem.Header改变时,textBlock的Text属性将自动更新(当数据源的某一Item中不存在Header时,绑定会失败)。以上代码的原理及工作方式通过代码及注释可以很容易看出。稍微需要多提的一点是,第二种绑定方式即通过BindingOperations类的SetBinding方法设置有一个优势,其可以在不是由FrameworkElement或FrameworkContentElement继承而来的对象上设置绑定。(第一种设置方式中,SetBinding为FrameworkElement与FrameworkContentElement提供的实例方法!)
当一个绑定不再需要时,可以随时使用BindingOperations.ClearBinding方法断开绑定关系。如断开前文给出的绑定代码如下:
1 BindingOperations.ClearBinding(textBlock, TextBlock.TextProperty);
如果一个目标对象存在多个绑定,可以调用BindingOperations.ClearAllBindings()方法来一次性清除所有绑定。如下代码:
1 BindingOperations.ClearAllBindings(textBlock);
还有一种清除绑定的方法就是直接给目标属性赋一个新值。这种方式不如前两种调用BindingOperations相应方法的原因是,前二者可以使目标属性保留以较低优先级由属性值继承等方式获得值的能力。
在内部给目标属性添加的绑定本质上是赋给这个目标属性一种表达式样式的值,而解除绑定本质上就是清空这个表达式值。BindingOperations的ClearBinding()方法内部就是调用目标对象的ClearValue()方法。
在XAML中完成绑定
WPF提供了一个标记扩展,用于在XAML中以声明方式实现SetBinding完成的数据绑定。这个标记扩展类就是前文用到的Binding。(Binding类名称中没有Extension后缀,属于非标准的标记扩展)
下面的代码展示了与前文代码绑定相同效果的XAML的实现:
1 <TextBlock Name="textBlock" Text="{Binding ElementName=treeView, Path=SelectedItem.Header}" />
这是Binding这个标记扩展标准使用方法,调用Binding的默认构造函数,分别设置ElementName与Path两个属性。另外Binding还有接受Path作为参数的构造方法,作为扩展方法可以这样使用:
1 <TextBlock Name="textBlock" Text="{Binding SelectedItem.Header, ElementName=treeView}" />
这样只需要单独设置ElementName这个属性。这里还需注意一般情况下用于标记扩展时使用Binding的ElementName属性,而像前文代码中设置源对象使用Source属性。在代码中直接设置ElementName属性也是可以的,但是要在XAML的Binding标记扩展中使用Source属性,需要以标准的资源定义格式来定义Source所需的值,如下:
1 <TextBlock Name="textBlock" Text="{Binding SelectedItem.Header, Source={StaticResource treeView}}" />
另一种在XAML中通过Binding标记扩展设置绑定的方法,是通过RelativeSource属性,这个属性为RelativSource类型也是作为一个标记扩展来使用,表示通过与目标元素的关系来得到源元素。这个标记扩展具体使用方式有如下几种:
-
用目标元素作为源元素本身,如把元素的一个属性绑定到另一个属性上,而不用指定源元素名称。
1 {Binding RelativeSource={RelativeSource Self}}
-
将目标元素的一个属性作为源元素本身,下面的例子中Slider的ToolTip属性绑定到自身的Value属性。
1 <Slider ToolTip="{Binding RelativeSource={RelativeSource Self}, Path=Value}" />
-
用目标元素的TemplatedParent(见模版一节)作为源元素
1 {Binding RelativeSource={RelativeSource TemplatedParent}}
-
使用指定类型的最近的父类型作为源元素
1 {Binding RelativeSource={RelativeSource FindAncestor,AncestorType={x:Type desiredType}}}
-
使用指定类型的最近的第n层父类型作为源元素
1 <TextBlock Name="textBlock" Text="{Binding RelativeSource={RelativeSource FindAncestor,AncestorLevel=n,AncestorType={x:Type desiredType}}}" />
-
使用之前数据绑定集合中的数据项作为源数据
1 {Binding RelativeSource={RelativeSource PreviousData}}
与普通.NET属性绑定
在前面的例子中(无论是使用过程代码绑定还是在XAML中实现绑定),源属性与目标属性均为依赖属性。而依赖属性内建的垂直的变更通知机制是WPF保持目标属性与源属性同步的关键。
WPF也支持把任何.NET对象的任何属性作为绑定的数据源。下面的例子展示了使用photoes对象(集合类型)的Count属性作为数据源:
1 <Label x:Name="lbItems" Content="{Binding Source={StaticResource photos}, Path=Count}" />
这段代码作用的前提是我们把photoes集合定义为资源。
接着我们要说使用普通.NET属性作为绑定数据源的一个大问题,即默认情况下这种绑定没有自动变更机制的支持。也就是说当原属性变化时,如无其他处理,目标属性不会改变!建立自动同步机制可以使用如下方法之一:
-
实现System.ComponentModel.INotifyPropertyChanged接口,该接口包含Property.Changed事件。
-
实现XXXChanged事件(XXX是发生变化的源属性的名称)
应该首选使用第一种方法,WPF对这种方法做了优化,而方法二仅是为了向后兼容。
这样上文的问题就可以通过让photos对象的类型实现INotifyPropertyChanged接口,对于像photoes这样的集合类型一个更方便的方法是将其集合类替换为WPF内建的,已经实现了INotifyPropertyChanged接口的ObservableCollection
对于代码仅需做如下变动:
1 public class Photos : Collection<Photo>
变为(见粗体部分):
1 public class Photos : ObservableCollection<Photo>
提示:使用普通.NET属性作为绑定数据源的内部工作方式
WPF使用反射获得源属性的值,但当源对象实现了ICustomTypeDescriptor,WPF会优先使用此接口,后者对于提高性能有较大帮助。
注意:目标属性只能为依赖属性
不同于源属性,目标属性拥有不同的内部处理方式,这要求它必须是依赖属性。另外源属性也要求必须是属性而非成员。
绑定到对象
之前的例子中,所有绑定的源都是某个对象的某个属性,我们通过Binding标记扩展的Path属性来定位源属性。我们也可以把一个目标绑定到整个源对象,这时不显示设置Path属性即可。
我们通过一个例子来看一下绑定到对象的作用。场景很简单,我们需要把一个名为zoomPopup的控件放在一个名为zoomButton的按钮中间。传统方式下我们使用这样的C#代码来完成:
1 Button zoomButton = new Button(); 2 Popup zoomPopup = new Popup(); 3 zoomPopup.Placement = PlacementMode.Center; 4 zoomPopup.PlacementTarget = zoomButton;
可以看到PlacementTarget属性需要一个UIElement对象。现在有了绑定到对象这种技术我们可以轻松通过XAML实现控件和元素这种高级的数据绑定:
1 <Button x:Name="zoomButton">Button> 2 <Popup PlacementTarget="{Binding ElementName=zoomButton}" Placement="Center">Popup>
可以看到这个功能提供的很方便的地方在于使你可以在XAML中设置接收值为一个对象的属性,而该对象无法通过类型转换器或标记扩展得到。
需要注意的一点是,如果绑定源不是一个UIElement元素,将会在源对象上调用ToString()方法,并使用这个字符串作为源。
另外对于源对象为集合对象,将其绑定到ListBox这种可以接受集合对象的控件时绑定到对象功能也是必须的,这个将在下一节绑定到集合中单独介绍。
注意:绑定到UIElement时可能产生的问题
首先,我们看一段问题XAML,这段代码会引发InvalidOperationException异常,错误信息是"指定的元素已经是另一个元素的逻辑子元素"
1 <Label x:Name="One" Content="{Binding ElementName=Two}"/> 2 <Label x:Name="Two" Content="Second"/>异常的原因是,这种绑定会把相同的元素放入一棵可视树的多个位置。把第一行改为如下样子就不会出现问题:
1 <TextBlock x:Name="One" Text="{Binding ElementName=Two}"/>但最佳做法是使用Binding的Path属性给这个绑定指定源属性,此处即Label.Content。
绑定到集合
前文中我们提到了绑定到集合这个概念,如ListBox这样的ItemsControl都提供了一个名为ItemSource的依赖属性,作为接受IEnumerable类型的属性,这个属性几乎是为数据绑定提供的。我们可以使用如下这种方式设置绑定:
1 <ListBox x:Name="picLstBox" ItemsSource="{Binding Source={StaticResource photos}}">ListBox>
为了让目标属性在源集合发生元素添加或删除时保持同步,源集合需要实现一个名为INotifyCollectionChanged的接口。我们可以让绑定源选择继承ObservableCollection
上面给出的绑定中,默认会使用源集合中每一项的ToString()方法的结果作为ListBox中每一个目标项显示的内容。我们可以使用ListBox的DisplayMemberPath属性来指定需要把源集合中每一项的哪个属性作为目标对象的显示内容。如下面改进的例子在ListBox中显示了源对象中每一项的Name属性:
1 <ListBox x:Name="picLstBox" DisplayMemberPath="Name" ItemsSource="{Binding Source={StaticResource photos}}">ListBox>
更高级一些如果我们想要在ListBox中显示照片而不是现有的属性,除了给Photo类添加Image字段外,下文会介绍更合适的方式 – 数据模板或值转换器。
注意:不能同时设置Items属性与ItemsSource属性
对Items属性的设置只是UI级别的操作,只有当ItemsSource属性为null时才可以设置Items属性,相反只有Items集合为空时才能设置ItemsSource,否则都会抛出异常。而通过Items属性可以获得使用这两种方式设置给ItemControl的项。
在控件部分我们曾介绍过如ListBox这样的Selector控件有一个叫做选中项的概念,当把一个Selector控件与一个实现了IEnumerable的对象绑定时,WPF会对选中项进行跟踪,每一个绑定到这个集合对象的控件都会得到同样的当前项信息。基于这个功能,只需要使用XAML代码就可以创建主/从关系的功能界面或同步多个选择器控件。要使用这个功能,所有希望同步的Selector控件都需要将IsSynchronizedWithCurrentItem属性设置为true,如果有绑定到相同源集合的控件的该属性设置为false,则其他控件当前项的变化不会影响该控件,同样该控件当前项的变化也不会影响其他项。需要特别指出的是多选中项不支持同步,多选中项中只有第一个被选中的项会被同步。另外ListBox的滚动也不会被自动同步。
使用数据上下文DataContext
对于在同一个界面中,不同目标控件绑定到同一个源对象不同的源属性是很常见的。为了避免在每个绑定中都显示使用Binding设置相同的Source、RelativeSource或ElementName,WPF提供了设置一个隐式的数据源 – DataContext的功能。
要设置一个数据上下文,可以设置所有目标控件共有的父元素的DataContext属性(所有FrameworkElements和FrameworkContentElements对象都有一个Object类型的DataContext属性),将此属性设置为源对象。如果一个目标控件中绑定没有显示指定源对象,WPF会向上遍历逻辑树直到找到一个非空的DataContext属性。
下面的XAML展示了定义与使用DataContext的方法:
1 <StackPanel x:Name="parent" DataContext="{StaticResource photos}"> 2 <Label x:Name="numLabel" Content="{Binding Path=Count}"/> 3 <ListBox x:Name="picLstBox" DisplayMemberPath="Name" ItemsSource="{Binding}">ListBox> 4 StackPanel>
上面代码中值得一提的是{Binding}这种写法,这个绑定表示源对象是整个DataContext对象。
另外DataContext可以很方便的使用代码设置:
1 parent.DataContext = photos;
这样也省去了把源对象定义为资源的过程。
使用DataContext最有意义的一个场景是你在代码中任意位置使用资源,这样资源定义中就不需要显示指定数据源,而假设存在DataContext,并由数据上下文中得到绑定源。这样资源的定义就相对独立一些,并可以用于不同的DataContext中。
数据绑定与呈现
数据绑定的一个重要目的是目标属性将源属性的内容呈现给用户。如果源属性的内容与目标属性类型相同,且源内容正式目标属性需要展示的,这时数据绑定就非常简单。但往往目标属性需要将源内容进行一些处理。如源属性是一个Url,而目标属性中需要显示给用户对应的图片。这时就需要对数据绑定进行一些定制。
WPF提供了两种机制,数据模板与值转换器。
数据模板
数据模板提供了一种使用自定义方式显示指定内容的机制,许多WPF控件都有DataTemplete类型的属性供给控件设置模板用。例如,内容控件(ContentControl)有一个ContentTemplete属性,可以控制Content对象的呈现,而ItemControl有一个ItemTemplete属性,用来给每一项应用一个模板,HeaderedContentControl的HeaderTemplete属性提供了使用模板控制Header呈现的功能。
将一个DataTemplete实例赋给这些用于设置数据模板的DateTemplete类型的属性会创建一个全新的可视树。如ItemsPanelTemplete等DataTemplete均是派生自FrameworkTemplete。这个类型有一个名为VisualTree的属性,可以设置给这个属性任何一棵FrameworkElement元素树,通常使用内容属性的方式来设置这个属性,在XAML中这都很容易实现。
下面是一个较具有实践性的例子:
在这个例子中我们把绑定到ListBox的Item的URI字符串以图片的形式显示出来。
首先为了使ListBox的Item中显示图片,我们需要给ListBox的Item设置一个数据模板。XAML如下:
1 <ListBox x:Name="picLstBox" ItemsSource="{Binding Source={StaticResource photos}}"> 2 <ListBox.ItemTemplate> 3 <DataTemplate> 4 <Image Source="ph.jpg" Height="36" /> 5 DataTemplate> 6 ListBox.ItemTemplate> 7 ListBox>
这步中为了简单,我们还没有为数据模板中的Image对象添加数据绑定。下一步我们就开始完成这个工作:
1"{Binding Path=FullPath}" Height="36" />
这个数据绑定隐式使用了一个数据上下文即ItemSource属性中的设置的对象,这样FullPath就被作为源对象photos中的属性来对待,从而正确的实现了对Image的Source属性的设置。
上面的例子中,DataTemplete使用了内联声明的方式。实际应用中更常见的做法是把DataTemplete做为一个资源以便在多个元素之间共享。
另外有一个特殊的为层次数据设计的DataTemplete的子类 – HierarchicalDataTemplate,其可以直接把层次对象绑定到原生支持层次类型数据的控件(如TreeView和Menu)上。
值转换器
值转换器可以把源值转换为完全不同的目标值,最常见的情况是进行不同数据类型间的转换,也可以在值转换器中插入自定义逻辑增强显示效果。例如通过值转换器可以基于一些非Brush的源值改变元素的背景色或前景色。又如使用值转换器,可以简单的实现根据数值显示单词单复数这种增强效果。下面将分别详细介绍这两种功能:
-
实现数据类型的转换
我们的功能场景是根据photos集合的Count值(源属性)来改变Label的Background属性。
按我们已经掌握的数据绑定的用法,首先会想到这种写法:
1 <Label Background="{Binding Path=Count,Source={StaticResource photoes}}" />
由于类型不同(Background的Brush类型与Count属性的int类型),这句XAML显然不会工作。
注意:像上面XAML中这样的数据绑定错误,没有以异常的方式被抛出,而是被记录在调试跟踪记录中,这一功能通过System.Diagnostics.TraceSource来实现,我们可以在调试器,如Visual Studio的输出窗口中看到这些信息。
接下来我们通过转换器来解决这个问题,我们创建一个名为IntToBrushConverter的转换器类,转换器需要实现System.Windows.Data命名空间下的IValueConverter接口。IValueConverter接口包含两个简单的方法 – Convert与ConvertBack。Convert将通过参数接收的源对象转化为目标对象,ConvertBack则相反。
下面的代码为IntToBrushConverter类的实现:
1 public class IntToBackgroundConverter : IValueConverter 2 { 3 public object Convert(object value, Type targetType, object parameter, CultureInfo culture) 4 { 5 if (targetType != typeof(Brush)) 6 throw new InvalidOperationException("The target must be a Brush!"); 7 8 int num = int.Parse(value.ToString()); 9 10 return (num == 0 ? parameter : Brushes.Transparent); 11 } 12 13 public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) 14 { 15 throw new NotSupportedException(); 16 } 17 }
可以看到,这段代码实现的效果是,当photos数量为空时,给Label一个有色的背景以突出。在每次源值改变时Convert方法都会被调用。此处ConvertBack没有用所以没有被实现。
IntToBrushConverter类实现的IValueConverter接口的Convert方法接收4个参数,其中两个分别为parameter与culture。这两个值分别通过Binding标记扩展中ConverterParameter与ConverterCulture这个属性来设置,如果标记扩展中没有显式指定这两个值,则默认传给parameter和culture参数的分别为null和"en-US"。
下面的XAML演示了ConverterParameter的使用:
1 <Label Background="{Binding Path=Count, Converter={StaticResource myConverter}, ConverterParameter=Yellow, Source={StaticResource photos}}" ... />
这样object类型的parameter参数就会接受到一个颜色为Yellow的Brush类型的对象。这里ConverterParameter自动进行了类型转换,这种自动类型转换对所有标记扩展都有效。如设置给ConverterCulture属性的值"zh-CN"会被自动转换为CultrueInfo类型对象。
接前文,在通过ConverterParameter属性设置了parameter参数以后。前文代码中:
1 return (num == 0 ? Brushes.Yellow : Brushes.Transparent);
就可以被下面这种更灵活的方式替换:
1 return (num == 0 ? parameter : Brushes.Transparent);
这样颜色就可以在XAML中灵活的设置。
提示:
WPF内置了一些值转换器来处理一些常见的数据绑定场景,BooleanToVisibilityConverter是其中之一。它实现一个bool或bool?与具有Visible,Hidden或Collapsed三种状态的Visibility枚举之间的转化。
具体说,对于Convert方法,会将true转换为Visible,false或null转换为Collapsed,对于ConvertBack方法,Visible被转换为true,Hidden和Collapsed被映射为false。
下面的例子使用BooleanToVisibilityConverter实现了通过CheckBox控制StatusBar的显示,整个例子完全通过XAML来实现。
1 <Window.Resources> 2 <BooleanToVisibilityConverter x:Key="BoolToVis" /> 3 Window.Resources> 4 <Canvas> 5 <CheckBox x:Name="checkBox">显示状态栏CheckBox> 6 <StatusBar Visibility="{Binding ElementName=checkBox,Path=IsChecked,Converter={StaticResource BoolToVis}}">StatusBar> 7 Canvas>仅当CheckBox的IsChecked属性为true时,StatusBar可见。
-
自定义数据显示
在源与目标数据类型一致的情况下,值转换器也可以通过其中自定义的转换逻辑增强目标的显示效果来发挥作用。如我们前文提到的增强单词单复数显示的例子,我们将通过一个CountToDescriptionConverter来实现这个功能,这个转换器如下:
1 public class CountToDescriptionConverter : IValueConverter 2 { 3 public object Convert(object value, Type targetType, object parameter, CultureInfo culture) 4 { 5 int num = int.Parse(value.ToString()); 6 return num + (num == 1 ? " item" : " items"); 7 } 8 9 public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) 10 { 11 throw new NotSupportedException(); 12 } 13 }
提示:
通过在Convert或ConvertBack方法中返回Binding.Nothing使转换器取消这次数据绑定(注意,不是null,null可能为一个有效的目标值),这样目标属性不会受影响,另外注意Binding依然存在,下次源值改变时,值转换器会被再次调用。
提示:
在绑定源是一个集合时,如果使用值转换器转换其中每一项,推荐的做法是给ItemControl的ItemTemplete属性设置期望的数据模板,并在数据模板的Binding中使用值转换器。另一种方法是实现一个接受集合的转换器,并返回转换后的集合,这样可以直接把源集合绑定到ItemControl上,但这种方式效率较低不推荐。
定制集合的视图
当绑定到一个集合(实现了IEnumerable的类),会有一个默认的视图(实现了ICollectionView接口的对象)被置于源对象与目标对象之间,通过下面的代码可以得到对源对象(集合)到默认视图的引用:
1 ICollectionView view = CollectionViewSource.GetDefaultView(this.FindResource("photos"));
由于ICollectionView的对象是与源集合相关,而与目标集合无关,所以如果同一个源集合绑定到多个目标(如ListBox),当ICollectionView被排序时,所有绑定目标都会呈现同样的排序效果。该视图保存了当前项的信息,同时这个视图也支持排序,分组,过滤和导航。先问将逐一深入介绍:
排序
ICollectionView的SortDescriptions属性用来控制视图项的排序。该属性接受SortDescription类对象使用如下方式来创建:
1 SortDescription sort = new SortDescription("Name", ListSortDirection.Ascending);
通过代码可以看到排序的两个依据分别是集合项的某个属性与排序方式(升序还是降序)。
SortDescriptions是一个集合属性,我们可以在其中加入多个SortDescription对象以基于多个属性进行排序。下面的代码将我们刚创建的sort对象添加到SortDescriptions集合:
1 view.SortDescriptions.Add(sort);
这个过程可以使用下面这种更简洁的写法:
1 view.SortDescriptions.Add(new SortDescription("DateTime", ListSortDirection.Descending));
这行代码我们添加了一个新的排序条件 – 依DateTime属性降序排列。另外注意,SortDescriptions集合中第一个SortDescription有着最高的优先级,后面的SortDescription对象优先级递减。
SortDescriptions集合有一个Clear()方法,调用此方法会返回默认排序视图;默认排序仅仅是源集合的原始顺序,这时可以再次添加新的排序条件。
提示:自定义排序
使用自定义排序可以获得比SortDescriptions属性更高的控制能力。如果源集合实现了IList接口(大多数集合都会实现),则CollectionViewSource的GetDefaultView方法返回的ICollectionView对象其实就是ListCollectionView类的一个实例。把这个ICollectionView转换为ListCollectionView类的对象,这样可以给这个ListCollectionView类的CustomSort属性一个实现了IComparer接口的自定义对象。这样IComparer.Compare方法会被调用,Compare方法中就是你自定义的排序逻辑。
分组
ICollectionView有一个GroupDescriptions属性,与SortDescriptions属性类似,GroupDescriptions属性中可以添加任意数量的PropertyGroupDescription对象,这样可以把源集合的项放入组与子组中。
下面的例子根据DateTime属性对photos集合中的项进行分组:
1 ICollectionView view = CollectionViewSource.GetDefaultView(this.FindResource("photos")); 2 view.GroupDescriptions.Clear(); 3 view.GroupDescriptions.Add(new PropertyGroupDescription("DateTime"));
由于照片的时间往往都不同,简单根据DateTime属性分组往往达不到目的。这次我们可以再次请出转换器,实现一个把时间转换为日期的DateTimeToDateConverter,这样使分组依据日期,可以提高分组的效果。转换器的代码如下:
1 public class DateTimeToDateConverter : IValueConverter 2 { 3 public object Convert(object value, Type targetType, object parameter, CultureInfo culture) 4 { 5 return ((DateTime)value).ToString("yyyy-MM-dd"); 6 } 7 8 public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) 9 { 10 throw new NotSupportedException(); 11 } 12 }
我们甚至可以在转换器中添加更复杂的自定义逻辑,从而实现按周,按月分组。某些情况下,我们可能需要使用ConverterCulture向Convert方法的cultrue参数传入适当的值以保证处理DateTime对象时应用了正确的区域语言信息。定义了转换器后,我们可以以如下方式使用这个转换器来进行自定义的分组:
1 view.GroupDescriptions.Add(new PropertyGroupDescription("DateTime", new DateTimeToDateConverter()));
提示:如果需要基于几个属性的值进行自定义分组,可以使用类似下面的代码:
1 view.GroupDescriptions.Add( 2 new PropertyGroupDescription(null, new ObjectToGroupByObjConverter()));我们传入的第一个参数为null,这表示分组将基于源对象而不是单一属性值。同时这个值转换器也就需要可以接受整个对象并返回可以作为分组条件的单一值。
与排序不同,分组的效果默认无法直接在ItemControl控件上看到。我们需要为ItemsControl的GroupStyle属性指定一个GroupStyle类型的实例。这个对象有一个HeaderTemplete属性,用于定义分组头的样式,我们可以给这个属性指定一个数据模板。
提示:WPF内建了一个简单的GroupStyle样式,可以让我快速的实现分组效果,通过GroupStyle.Default属性可以得到这个样式实例。下面的XAML展示了其使用方法:
1 <ListBox x:Name="picBox" ItemsSource="{Binding Source={StaticResource photos}}"> 2 <ListBox.GroupStyle> 3 <x:Static Member="GroupStyle.Default" /> 4 ListBox.GroupStyle> 5 ListBox>
下面的XAML中我们为GroupStyle的HeaderTemplete定义了一个数据模板:
1 <ListBox.GroupStyle> 2 <GroupStyle> 3 <GroupStyle.HeaderTemplate> 4 <DataTemplate> 5 <Border BorderBrush="Black" BorderThickness="1"> 6 <TextBlock Text="{Binding Path=Name}" FontWeight="Bold"/> 7 Border> 8 DataTemplate> 9 GroupStyle.HeaderTemplate> 10 GroupStyle> 11 ListBox.GroupStyle>
上面的代码中,需要注意的是数据模板中的数据绑定,这个数据绑定使用一个特殊的CollectionViewGroup对象数据上下文,这个对象在后台被实例化,其有一个Name属性表示每个分组的名称。前文的XAML正是在数据模板中使用数据绑定在分组头中显示该名称。
排序与分组的关系
我们可以把一系列排序放置在分组前面,这样得到的效果是主SortDescription会被应用到每个组,而剩余的SortDescription会被应用到组中每一项。所以应该确认用于分组排序的属性(或自定义逻辑)与用于分组的属性(或自定义逻辑)是一致的。
过滤
类似于排序和分组,ICollectionView中的Filter属性用于根据任意条件对要显示的数据进行过滤。这个属性是Predicate
默认情况下,Filter为null,源集合中所有项都会显示在视图中。如果该委托类型的属性被设置,则会在源集合每一项上调用委托,而委托正是用来决定该项是显示(返回true时)还是隐藏(返回false时)。下面的代码展示了Filter属性的设置:
1 view.Filter = (o) => (DateTime.Now - (o as Photo).DateTime).Days <= 7;
这样展示出来的就是过滤后的结果。如果想取消过滤,只需把Filter置为null即可。
在项目中导航
ICollectionView通过CurrentItem返回当前项,对应的CurrentPosition属性表示由0起的当前项的索引。ICollectionView中提供了很多方法以编程方式改变当前项的设置,如下面的代码:
1 //逆向移动 2 view.MoveCurrentToPrevious(); 3 4 //转到最后一项 5 if (view.IsCurrentBeforeFirst) 6 view.MoveCurrentToLast(); 7 8 //正向移动 9 view.MoveCurrentToNext(); 10 11 //回到第一项 12 if (view.IsCurrentAfterLast) 13 view.MoveCurrentToFirst();
另外注意,当ICollectionView中还没有项被选中时CurrentItem为null,同时CurrentPosition返回-1。
当绑定源中CurrentItem等变化后,所有绑定到此源的控件的当前项都会随之更新,然而这个更新是有条件的,见下面"注意"。
注意:不同于排序,分组和过滤操作中对数据源的处理会直接显示在数据展示控件(如Selector中),默认设置下导航效果不会直接由数据源反映到目标控件。这个设置是由Selector控件的IsSynchronizedWithCurrentItem属性决定的,默认值为false,当被设置为true时,导航效果会自动同步到控件中。
提示:Binding标记扩展中属性路径的表示
我们通过解释示例来说明这个问题
{Binding Path=/} 当数据源是一个集合,"/"表示绑定到当前项 {Binding Path=/DateTime} 绑定到当前项的DateTime属性 {Binding Path=Photoes/} 绑定到非当前数据源(在后文对绑定到非默认视图部分介绍中可以看到非当前数据源的情况)的Photoes属性(数据源不一定是集合,而Photoes属性需要是一个集合) {Binding Path=Photoes/DateTime} 绑定到上一个例子源的DateTime属性。灵活的使用这些绑定路径,可以在不写任何程序代码的情况下实现master/detail形式的界面。当然要注意上文提到的控件的IsSynchronizedWithCurrentItem属性要设置为true。
非默认视图
之前介绍的排序,分组,过滤与导航都是在源集合的默认视图上进行的。某些情况下,你可能需要在一个相同的源集合上有不同的视图,从而以不同的展示方式绑定到多个控件上显示。
下面代码展示了用CollectionViewSource来创建一个非默认的视图,并给这个视图设置数据源:
1 CollectionViewSource viewSource = new CollectionViewSource(); 2 viewSource.Source = photos;
此时viewSource.View是一个非默认的ICollectionView实现。
更常见的方式是使用XAML来实现上述C#的功能:
1 <Window.Resources> 2 <local:Photos x:Key="photos"/> 3 <CollectionViewSource x:Key="viewSource" Source="{StaticResource photos}"/> 4 Window.Resources>
接下来把这个非默认的视图绑定到目标属性上只需要在{Binding}的Source中使用
我们可以在这个非默认的视图上实现前文介绍的排序,分组等等,前提是我们需要通过CollectionViewSource对象的CollectionViewSource.View实例属性得到ICollectionView类型的对象(不应使用CollectionViewSource.GetDefaultView方法)。
可以扩展前文的XAML,把SortDescription等设置放在声明CollectionViewSource资源的地方:
1 <CollectionViewSource x:Key="viewSource" Filter="viewSource_Filter" Source="{StaticResource photos}"> 2 <CollectionViewSource.SortDescriptions> 3 <compModel:SortDescription PropertyName="DateTime" Direction="Descending"/> 4 CollectionViewSource.SortDescriptions> 5 <CollectionViewSource.GroupDescriptions> 6 <PropertyGroupDescription PropertyName="DateTime"/> 7 CollectionViewSource.GroupDescriptions> 8 CollectionViewSource>
由于这其中用到.NET命名空间中的SortDescription类,需要在XAML根节点中添加.NET的命名空间到xml命名空间的映射。
1 xmlns:compModel="clr-namespace:System.ComponentModel;assembly=WindowsBase"
另外,viewSource_Filter方法实现类似前文委托指向的匿名函数。
1 private void viewSource_Filter(object sender, FilterEventArgs e) 2 { 3 e.Accepted = (DateTime.Now - (e.Item as Photo).DateTime).Days <= 7; 4 }
通过设置事件参数e的Accepted布尔属性,来判断此项(可以通过e.Item属性访问当前项)是现实还是隐藏。
提示:显式使用CollectionViewSource创建自定义视图(而不是使用默认视图)的好处就是可以用上面展示的XAML的方式来添加排序,过滤等,从而可以不使用C#代码。
注意:针对自定义视图(非默认视图),当选择器(Selector)控件绑定到它们时,IsSynchronizedWithCurrentItem属性默认值会被WPF自动设置为true。除非手动给此属性指定了一个值或者选择器控件的SelectMode属性不是Single。这种情况下的默认设置可能会很符合你的需求。
数据提供程序
WPF原生支持两种数据提供程序,XmlDataProvider与ObjectDataProvider。它们都支持带通知的数据绑定,而且可以在XAML中声明使用。首先我们看一下XmlDataProvider在XAML中定义的例子。
<Window.Resources> <XmlDataProvider x:Key="dataProvider" XPath="GameStats"> <x:XData> <GameStats xmlns=""> <GameStat Type="Beginner"> <HighScore>1203HighScore> GameStat> <GameStat Type="Intermediate"> <HighScore>1089HighScore> GameStat> <GameStat Type="Advanced"> <HighScore>541HighScore> GameStat> GameStats> x:XData> XmlDataProvider> Window.Resources>
这段示例中,我们把外部的xml片段(非粗体部分)放置在
注意:在数据源中定义的XPath并不是最终决定绑定源的决定条件,XPath还可以在{Binding}标记扩展中进一步定义。
另一种更普遍情况是XML位于独立文件中。我们通过给XmlDataProvider的Source属性设置一个Uri来引入这个文件。这个Uri可以是本地文件,Internet上的文件或嵌入的资源。下面是一个示例:
1 <XmlDataProvider x:Key="dataProvider" XPath="GameStats" Source="GameStats.xml" />
下面的例子中,我们将ListBox绑定到前文
1 <ListBox ItemsSource="{Binding Source={StaticResource dataProvider}, XPath=GameStat/HighScore}" />
另外,XPath可以与之前Path属性混合使用,如:
1 <ListBox ItemsSource="{Binding Source={StaticResource dataProvider}, XPath=GameStat/HighScore}, Path=OuterXml" />
另外,ItemsControl的DisplayMemberPath属性即支持Path语法,也支持XPath语法。
将xml绑定到一个层次结构
如果要将xml绑定到类似TreeView或Menu这样的层次数据控件。需要使用一个或多个HierarchicalDataTemplete。对于xml每一级的父节点对应一个HierarchicalDataTemplete,而子节点对应普通的DataTemplete。可以给HierarchicalDataTemplete中的ItemSource定义{Binding},当然这其中支持XPath。这些代码我们需要放在
1 <Grid.Resources>
2 <HierarchicalDataTemplate DataType="GameStats" 3 ItemsSource="{Binding XPath=*}"> 4 <TextBlock FontStyle="Italic" Text="All Game Stats"/> 5 HierarchicalDataTemplate> 6 <HierarchicalDataTemplate DataType="GameStat" ItemsSource="{Binding XPath=*}"> 7 <TextBlock FontWeight="Bold" FontSize="20" Text="{Binding XPath=@Type}"/> 8 HierarchicalDataTemplate> 9 <DataTemplate DataType="HighScore"> 10 <TextBlock Foreground="Blue" Text="{Binding XPath=.}"/> 11 DataTemplate> 12 <XmlDataProvider x:Key="dataProvider" XPath="GameStats"> 13 <x:XData> 14 <GameStats xmlns=""> 15 <GameStat Type="Beginner"> 16 <HighScore>1203HighScore> 17 GameStat> 18 <GameStat Type="Intermediate"> 19 <HighScore>1089HighScore> 20 GameStat> 21 <GameStat Type="Advanced"> 22 <HighScore>541HighScore> 23 GameStat> 24 GameStats> 25 x:XData> 26 XmlDataProvider> 27 Grid.Resources>
对于普通.NET对象,HierarchicalDataTemplete对应到DataType属性指定的类型的对象。
最后我们看一下TreeView绑定到以上这个数据源的XAML:
1 <TreeView ItemsSource="{Binding Source={StaticResource dataProvider},XPath=.}" />
上面的例子运行中的TreeView效果如下图:
提示:在编写XPath表达式时,如果要访问某一命名空间下的元素,我们要在表达式中明确添加这个前缀。如对于这样一段xml源数据:
<xml xmlns:blog="http://lsxqw2004.cnblogs.com/">
<blog:post name=""/>
<post url=""/>
xml>
很容易看出两个Post在不同的命名空间下,如果我们要访问blog:post的Name属性,则XPath形如:"blog:post/@name"。注意,要在XAML实现这种对某个命名空间下元素的访问。我们需要创建一个XmlNamespaceMappingCollection的对象,并将其指定给XmlDataProvider的XmlNamespaceManager属性。参考如下代码:
XmlNamespaceMappingCollection的定义:
<XmlNamespaceMappingCollection x:Key="namespaceMapping">
<XmlNamespaceMapping Uri="http://lsxqw2004.cnblogs.com/" Prefix="blog" />
XmlNamespaceMappingCollection>
在XmlDataProvider中设置:
<XmlDataProvider x:Key="dataProvider" XPath="blog:post" Source=""
XmlNamespaceManager="{StaticResource namespaceMapping}">
ObjectDataProvider
虽然在WPF中我们可以直接绑定到一个.NET对象,但通过配置ObjectDataProvider作为绑定.NET对象的代理,可以获得更多的控制:
-
以声明方式使用带参数的构造函数实例化源对象
-
绑定到源对象的一个方法
-
提供实现异步数据绑定的其他方法
-
以声明方式使用带参数的构造函数实例化源对象
由浅入深,首先我们用ObjectDataProvider封装一个对象:
1 <Window.Resources> 2 <local:Photos x:Key="photos" /> 3 <ObjectDataProvider x:Key="dataProvider" ObjectInstance="{StaticResource photos}"/> 4 Window.Resources>
这种方式下,通过ObjectDataProvider绑定和直接绑定到.NET对象没有任何区别,Binding包括其中的Path属性的设置都完全一样,ObjectDataProvider会被自动unwrap以得到其中的.NET对象。
ObjectDataProvider的优势表现在其可以封装一个类型,并自动创建此类型的实例。(可能需要在此类型 构造函数中实现一些逻辑)。
1 <ObjectDataProvider x:Key="dataProvider" ObjectType="{x:Type local:Photos}" />
如果构造函数有带参数的重载,我们也可以在ObjectDataProvider中这样设置以使用自定义的重载。
1 <ObjectDataProvider x:Key="dataProvider" ObjectType="{x:Type local:Photos}"> 2 <ObjectDataProvider.ConstructorParameters> 3 <sys:Int32>36sys:Int32> 4 ObjectDataProvider.ConstructorParameters> 5 ObjectDataProvider>
这些特性对于想要以声明方式定义一个数据源很有用。
-
绑定到一个方法
对于大多数自定义类型来说,应该把数据源作为属性提供,而对于某些可能来自第三方的无法更改的类,我们确实需要把一个方法作为数据源,而ObjectDataProvider可以帮我们做到这些:
1 <ObjectDataProvider x:Key="dataProvider" ObjectType="{x:Type local:Photos}" MethodName="GetFolderName" />
如果这个方法需要参数,可以通过MethodParameters属性,类似给构造函数传参的ConstructorParameters,可以这样:
1 <ObjectDataProvider x:Key="dataProvider" ObjectType="{x:Type local:Photos}" MethodName="GetFolderName"> 2 <ObjectDataProvider.MethodParameters> 3 <sys:Int32>35sys:Int32> 4 ObjectDataProvider.MethodParameters> 5 ObjectDataProvider>
这样通过绑定ObjectDataProvider就可以将目标绑定到方法返回值上:
1 <TextBlock Text="{Binding Source={StaticResource dataProvider}}"/>
提示:由于数据提供程序自动拆封的特性,绑定到ObjectDataProvider或XmlDataProvider这样的数据提供程序后,会自动将源对象由数据提供程序以指定方式变为封装的源对象。可以将Binding的BindDirectlyToSource属性设置为true来使绑定源确实为ObjectDataProvider对象。
-
实现异步数据绑定的其他方法
在大部分情况下,数据绑定应该以异步方式运行,以防止用户界面被冻结,WPF提供了两种独立的异步绑定实现方式:比较普遍的一种是设置Binding的IsAsync属性,另一种是XmlDataProvider与OjbectDataProvider所独有的,这两者提供了IsAsynchronous属性。IsAsync的默认值总是false,OjbectDataProvider的IsAsynchronous属性的默认值也是false,而XmlDataProvider的IsAsynchronous属性的默认值是true。当IsAysnchronous属性设置为true时,数据提供程序会在背景线程上创建源对象,这样对于源对象构建较慢的情况设置IsAsynchronous为true是最好的方法,如XmlDataProvider的默认设置。
高级内容
自定义数据流
之前我们提到的所有绑定例子,数据流都是单向的,即单向绑定。我们可以通过Binding的Mode属性来设置绑定方式。绑定方式定义于BindingMode枚举中:
-
OneWay – 源改变时,目标被更新
-
TwoWay – 源或目标改变都会导致另一方被更新
-
OneWayToSource – 与OneWay相反,目标改变时,源会被更新
-
OneTime – 目标会保留绑定初始化时源的值,但源的变化不会反映到目标上
对于目标属性,其中多数依赖属性默认是OneWay绑定,另一部分默认为TwoWay – 像TextBox.Text。特别需要注意的是,默认TwoWay绑定的控件应该是绑定到一个可读可写的属性上,而如果要将其绑定到一个只读属性上,需要显式设置绑定方式为OneWay或OneTime。前文介绍的转换器需要的Convert与ConvertBack方法也正是应了正向与反向绑定的需要。
提示:OneWayToSource的使用场景
我们在多个绑定目标中共享一个源,我们需要通过更改其中一个或多个绑定目标来将变化反馈给源,再由源将变化通知到其他绑定目标。 由于Binding中要求目标属性为依赖属性,但如果目标属性不是依赖属性,而源属性恰好是依赖属性,我们可以采取将是依赖属性源属性反向绑定到非依赖属性的目标属性,并将Binding方式设置为OneWayToSource。
绑定源更新触发方法
Binding的UpdateSourceTrigger属性可以控制OneWayToSource绑定方式下源被更新的策略。此属性使用UpdateSourceTrigger枚举值表示:
-
PropertyChanged – 只要目标属性改变,源就更新
-
LostFocus – 目标属性改变并且目标元素失去焦点时源才会被更新
-
Explicit – 显示调用BindingExpression.UpdateSource方法时源才会被更新。(通过调用BindingOperations.GetBindingExpression静态方法或调用任意一个FrameworkElement/FrameworkContentElement对象的GetBindingExpression实例方法,可以得到BindingExpression的实例。)
不同的控件的UpdateSourceTrigger的设置也不同,如TextBox.Text默认设置是LostFocus。
提示:依赖属性的默认设置
如上文所述,TextBox.Text等依赖属性对于Binding的Mode或UpdateSourceTrigger等有不同的默认值。这些值存储于元数据中,调用TextBox.TextProperty.GetMetadata()可以看到返回值中BindsTwoWayByDefault或DefaultUpdateSourceTrigger等属性。
提示:我们可以处理FrameworkElements/FrameworkContentElements的SourceUpdated和TargetUpdated事件来在源或目标发生更新时进行额外的处理,如记录日志或给出动画通知。但特别注意需要将Binding的NotifyOnSourceUpdated属性或NotifyOnTargetUpdated属性为true时,才会触发上述事件。
绑定的验证规则
这是一个很常见的需求,在一个TwoWay绑定或OneWayToSource绑定中,当用户输入了无效数据,系统应该拒绝数据并给出用户一个友好的提示。一般我们通过自己编写验证规则来实现这个需求。
Binding有一个ValidationRules属性,其接受一个或多个派生自ValidationRule的对象,每个ValidationRule中都会检查数据,并将无效数据给以标记。当ValidationRule返回false时表示数据无效,反之true表示有效。我们给出一个自定义ValidationRule的例子,这个例子检查输入的文件是否为jpg格式,且文件是否存在。
1 public class JpgValidationRule : ValidationRule 2 { 3 public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo) 4 { 5 string filename = value.ToString(); 6 7 //文件是否存在? 8 if (!File.Exists(filename)) 9 return new ValidationResult(false, "文件不存在"); 10 11 //文件格式不对 12 if (!filename.EndsWith(".jpg", StringComparison.InvariantCultureIgnoreCase)) 13 return new ValidationResult(false, "格式不对"); 14 15 //通过条件检查 16 return new ValidationResult(true, null); 17 } 18 }
代码所示,自定义验证规则派生自ValidationRule对象,其中重写了父类的Validate方法,其中实现了验证规则。下面的XAML展示了在绑定中使用这个自定义验证:
1 <TextBox> 2 <TextBox.Text> 3 <Binding Source=""> 4 <Binding.ValidationRules> 5 <local:JpgValidationRule /> 6 Binding.ValidationRules> 7 Binding> 8 TextBox.Text> 9 TextBox>
由于TextBox默认的UpdateSourceTrigger设置为LostFocus,所以当其失去焦点时,会调用我们自定义逻辑对数据验证。这个验证过程会发生在转换器调用前。
当数据未通过验证时,目标元素会被应用一个新模板 – 目标元素被套上一个红色细边框。通过目标元素的Validation.ErrorTemplate附加属性来自定义这个模板。
当数据被标记为无效时,目标元素的Validate.HasError附加属性也会被设置为true,并且当Binding的NotifyOnValidationError属性被设置为true时,Validation.Error附加事件会被触发,可以处理这个事件实现一些友好的通知。
除了可以通过返回的ValadationRule中的字符串得到错误信息外,还可以通过Validation.Errors附加属性得到错误信息,注意,当随后的绑定成功完成时,这个属性会被清空。
内置的ValidationRule
WPF内建一个名为ExceptionValidationRule,我们可以直接以这种方式使用此验证规则:
1 <TextBox> 2 <TextBox.Text> 3 <Binding Source=""> 4 <Binding.ValidationRules> 5 <ExceptionValidationRule/> 6 Binding.ValidationRules> 7 Binding> 8 TextBox.Text> 9 TextBox>
当更新源属性时,有任何异常抛出ExceptionValidationRule都会标记数据为无效项,并有机会在日志中记录异常,并给用户以友好通知。
提示:处理异常的其他方式
更新源时另一种处理异常的方式是给Binding的UpdateSourceExceptionFilter属性添加一个委托。当更新源时抛出异常,委托会被调用,在其中我们可以编写C#代码来处理异常。
另外有趣的一点是,如果在委托中返回ValidationResult对象,则会像使用了自定义ValidationRules一样发生一系列相关的后续处理,如在Validation.Errors添加错误,Validation.HasError被设置为true,并有条件的(见前文)触发Valadtion.Error等。
组合使用多个数据源
WPF中提供下面几个类用于将多个数据源组合使用
-
CompositeCollection
-
MultiBinding
-
PriorityBinding
-
CompositeCollection
这个类在绑定到一个由多个源组成的集合项时很有用。下面的XAML中定义的CompositeCollection中包含了photos集合及两个单独的photo项:
1 <CompositeCollection> 2 <CollectionContainer Collection="{Binding Source={StaticResource photos}}" /> 3 <local:Photo /> 4 <local:Photo /> 5 CompositeCollection>
photos集合包装在一个CollectionContainer对象中,从而其中的项可以被作为CompositeCollection的一部分。
-
MultiBinding
MultiBinding可以将多个绑定汇集起来,并输出一个单独的目标值。MultiBinding需要使用转换器来完成这个过程。下面的示例XAML结合三个绑定的值作为ProgressBar的值。
1 <ProgressBar x:Name="sample"> 2 <ProgressBar.Value> 3 <MultiBinding Converter="{StaticResource converter}"> 4 <Binding Source="{StaticResource worker1}" /> 5 <Binding Source="{StaticResource worker2}" /> 6 <Binding Source="{StaticResource worker3}" /> 7 MultiBinding> 8 ProgressBar.Value> 9 ProgressBar>
这其中三个绑定与转换器都定义于资源中。转换器的代码如下:
1 public class ProgressConverter : IMultiValueConverter 2 { 3 public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) 4 { 5 int totalProgress = 0; 6 7 foreach (Worker worker in values) 8 totalProgress += worker.Progress; 9 return totalProgress / values.Length; 10 } 11 12 public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) 13 { 14 throw new NotSupportedException(); 15 } 16 }
注意:与普通转换器不同,MultiBinding所使用的转换器需实现IMultiValueConverter接口。
-
PriorityBinding(WPF)
PriorityBinding的设置看起来与MultiBinding很相似(都封装了多个Binding对象)。但其目标值不是聚集Binding的结果,而是依Binding优先级来决定,首先我们看一段XAML,这是一个PriorityBinding声明的模板:
1 <PriorityBinding> 2 <Binding Source="HighPri" Path="SlowBinding" IsAsync="True" /> 3 <Binding Source="MediumPri" Path="MediumBinding" IsAsync="True" /> 4 <Binding Source="LowPri" Path="FastBinding" /> 5 PriorityBinding>
所有这些Binding,优先级按高到低排列,会以此被处理,一个较低优先级的Binding结果会被较高优先级Binding的结果覆盖。所以较低优先级的Binding应该是最快完成的,而越慢的Binding优先级越高。特别注意,除了优先级最低的Binding(最后一个),其余的Binding的IsAsync="True",否则这些Binding会锁住界面,从而失去使用PriorityBinding的意义。
本文完
参考:
《WPF揭秘》