6.3 Binding 的源与路径
Binding 对源的要求并不苛刻——只要它是一个对象,并且通过属性(Property)公开自己的数据,它就能作为 Binding 的源。
在日常的工作中,有时会把控件自己或自己的容器或子集元素当作源、用一个控件作为另一个控件的数据源、把集合作为 ItemsControl 的数据源、使用 XML 作为 TreeView 或 Menu 的数据源、把多个控件关联到“数据制高点”上,甚至干脆不给 Binding 制定数据源、让它自己去找。下面,我们就分述这些情况。
6.3.1 把控件作为 Binding 源与 Binding 标记扩展
前面提过,大多数情况下 Binding 的源是逻辑层的对象,但有时候为了让 UI 元素产生一些联动效果也会使用 Binding 在控件之间建立关联。下面的代码把一个 TextBox 的 Text 属性关联在了 Slider 的 Value 属性上。
<Window x:Class="DataBinding.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Control as Source" Height="110" Width="300">
<StackPanel>
<TextBox x:Name="textBox1" Text="{Binding Path=Value ElementName=slider1}" BorderBrush="Black" Margin="5" />
<Slider x:Name="slider1" Maximum="100" Minimum="0" Margin="5" />
</StackPanel>
</Window>
正如大家所见,除了可以在 c# 代码中建立 Binding 外在 XAML 代码里也可以方便地设置 Binding,这就给了设计师很大的自由度来决定 UI 元素之间的关联情况。
值得注意的是,在 c# 代码中可以访问 XAML 代码中声明的变量但 XAML 代码中却无法访问 c# 代码中声明的变量。
回过头来看这句XAML代码,它使用了Binding标记扩展语法:
<TextBox x:Name="textBox1" Text="{Binding Path=Value ElementName=slider1}" BorderBrush="Black" Margin="5" />
与之等价的 c# 代码是:
this.textBox1.SetBinding(TextBox.TextProperty, new Binding("Value"){ElementName="slider1"});
因为Binding类的构造器本身可以接收Path作为参数,所以也常写为:
<TextBox x:Name="textBox 1" Text="{Binding Value, ElementName=slider 1;" BorderBrush="Black" Margin="5" />
Binding的标记扩展语法,初看起来平淡无奇甚至有些别扭,但细品起来就会发现它的精巧之处。说它“别扭”是因为我们已经习惯了Text="Hello World"这种“键-值”式的赋值方式,而且认为值与属性的数据类型一定要一致——大脑很快会质询Text="{Binding Value, ElementName= slider1}"的字面意思——Text的类型是string,为什么要赋一个Binding类型的值呢?其实,我们并不是为Text属性“赋了一个Binding类型的值”,为了消除这个误会,你可以把这句代码读作“为Text属性设置Binding为……”。再想深一步,在编程时我们不是经常把函数视为一个值吗?只是这个值需要在函数执行结束后才能得到。同理,我们也可以把{Binding}视为一个值,只是这个值并非像"Hello World"字符串一样直接和固定。也就是说,我们可以把Binding视为一种间接的、不固定的赋值方式——Binding标记扩展很恰当地表示了这个含义。
6.3.2 控制 Binding 的方向及数据更新
Binding 在源与目标之间架起了沟通的桥梁,默认情况下数据既能够通过 Binding 送达目标,也能够从目标返回源(收集用户对数据的修改)。有时候数据只需要展示给用户、不允许用户修改,这时候可以把Binding模式更改为从源向目标的单向沟通。Binding 还支持从目标向源的单向沟通以及只能在 Binding 关系确立时读取一次数据,这需要我们根据实际情况去选择。
控制 Binding 数据流向的属性是 Mode,它的类型是 BindingMode 枚举。BindingMode 可取值:
- TwoWay
- OneWay
- OnTime
- OneWayToSource
- Default
这里的 Default 值是指 Binding 的模式会根据目标的实际情况来确定,比如若是可编辑的,Default 就采用双向模式;若是只读的则采用单向模式。
Binding 还有一个重要的属性——UpdateSourceTrigger,它的类型是 UpdateSourceTrigger 枚举,可取值:
- PropertyChanged
- LostFocus
- Explicit
- Default
顺便提一句,Binding 还具有 NotifyOnSourceUpdated 和 NotifyOnTargetUpdated 两个 bool 类型的属性。如果设为 true,则当源或目标被更新后 Binding 会激发相应的 SourceUpdated 事件和 TargetUpdated 事件。实际工作中,我们可以通过监听这两个事件来找出有哪些数据或控件被更新了。
6.3.3 Binding 的路径(Path)
尽管在 XAML 代码中或者 Binding 类的构造器参数列表中我们以一个字符串来表示 Path,但 Path 的实际类型是 PropertyPath。下面让我们看看如何创建 Path 来应对各种情况。
最简单的情况就是直接把 Binding 关联在 Binding 源的属性上,前面的例子就是这样。语法如下:
<TextBox x:Name="TextBox" Text="{Binding Path=Value, ElementName=Slider}"/>
等效的 C# 代码是:
Binding binding = new Binding() { Path = new PropertyPath("Value"), Source = this.Slider };
this.TextBox.SetBinding(TextBox.TextProperty, binding);
或者使用 Binding 的构造器简写为:
Binding binding = new Binding("Value") { Source = this.Slider };
this.TextBox.SetBinding(TextBox.TextProperty, binding);
Binding 还支持多级路径。比如,如果我们想让一个 TextBox 显示另外一个 TextBox 的文本长度,我们可以写:
<StackPanel>
<TextBox x:Name="TextBox" BorderBrush="Black" Margin="5"/>
<TextBox x:Name="TextBox2" Text="{Binding Path=Text.Length, ElementName=TextBox, Mode=OneWay }"
BorderBrush="Black" Margin="5"/>
</StackPanel>
等效的 C# 代码是:
this.TextBox2.SetBinding(TextBox.TextProperty, new Binding("Text.Length") { Source = this.TextBox, Mode = BindingMode.OneWay});
我们知道,集合类型的索引器(Indexer)又称为带参属性。既然是属性,索引器也能作为Path来使用。比如我想让一个TextBox显示另一个TextBox文本的第四个字符,我们可以这样写:
<StackPanel>
<TextBox x:Name="TextBox" BorderBrush="Black" Margin="5"/>
<TextBox x:Name="TextBox2" Text="{Binding Path=Text[3], ElementName=TextBox, Mode=OneWay }"
BorderBrush="Black" Margin="5"/>
</StackPanel>
等效的C#代码是:
this.TextBox2.SetBinding(TextBox.TextProperty,
new Binding("Text[3]") { Source = this.TextBox, Mode = BindingMode.OneWay });
当使用一个集合或者DataView作为Binding源时,如果我们想把它的默认元素当作Path使用,则需要使用这样的语法:
List<string> stringList = new List<string>() { "Tim", "Tom", "Blog" };
this.TextBox.SetBinding(TextBox.TextProperty, new Binding("/") { Source = stringList, });
this.TextBox2.SetBinding(TextBox.TextProperty, new Binding("/Length") { Source = stringList, Mode = BindingMode.OneWay });
this.TextBox2.SetBinding(TextBox.TextProperty, new Binding("/[2]") { Source = stringList, Mode = BindingMode.OneWay });
如果集合元素的属性仍然还是一个集合,我们想把子级集合中的元素当作Path,则可以使用多级斜线的语法(即一路“斜线”下去,比如:
class City
{
public string Name { get; set; }
}
class Province
{
public string Name { get; set; }
public List<City> Cities { get; set; }
}
class Country
{
public string Name { get; set; }
public List<Province> Provinces { get; set; }
}
List<Country> countries = new List<Country>(); // initializing
this.TextBox.SetBinding(TextBox.TextProperty, new Binding("/Name"){ Source = countries});
this.TextBox2.SetBinding(TextBox.TextProperty, new Binding("/Provinces.Name"){ Source = countries});
this.TextBox3.SetBinding(TextBox.TextProperty, new Binding("/Provinces/Cities.Name"){ Source = countries});
6.3.4 没有 Path 的 Binding
有的时候我们会在代码中看到一些 Path 是一个 “.” 或者干脆没有 Path 的 Binding,着实让人摸不着头脑。这其实是一种特殊的情况——Binding 源本身就是数据且不需要 Path 来指明。典型的,string、int 等基本类型就是这样,它们的实例本身就是数据,无法通过它的哪个属性来访问这个数据,这时候我们只需要将 Path 的值设置为 “.” 就可以了。在 XAML 代码里这个 “.” 可以省略不写,但在 c# 代码中却不能省略。
<StackPanel>
<StackPanel.Resources>
<sys:String x:Key="MyString">
菩提本无树,明镜亦非台。
本来无一物,何处惹尘埃。
</sys:String>
</StackPanel.Resources>
<TextBlock x:Name="TextBlock" TextWrapping="Wrap" Text="{Binding Path=., Source={StaticResource ResourceKey=MyString}}" FontSize="16" Margin="5"/>
</StackPanel>
6.3.5 为 Binding 指定源的几种方法
Binding 的源是数据的来源,所以,只要一个对象包含数据并能通过属性把数据暴露出来,它就能当做 Binding 的源来使用。包含数据的对象比比皆是,但必须为 Binding 的 Source 指定合适的对象 Binding 才能正常工作,常见的办法有:
- 把普通 CLR 类型单个对象指定为 Source:包括 .NET Framework 自带类型的对象和用户自定义类型的对象。如果类型实现了 INotifyPropertyChanged 接口,则可通过,则可以通过在属性的 set 语句里激发 PropertyChanged 事件来通知 Binding 数据已被更新。
- 把普通 CLR 集合类型对象指定为 Source:包括数组、
ist<T>、bservableCollection<T>等集合类型。实际工作中,我们经常需要把一个集合作为 ItemsControl 派生类的数据源来使用,一般是把控件的 ItemsSource 属性使用 Binding 关联到一个集合对象上。 - 把 ADO.NET 数据对象指定为 Source:包括 DataTable 和 DataView 等对象。
- 使用 XmlDataProvider 把 XML 数据指定为 Source:XML 作为标准的数据存储和传输格式几乎无处不在,我们可以用它表示单个数据对象或集合;一些 WPF 控件是级联式的(如 TreeView),我们可以把树状结构的 XML 数据作为源指定给与之关联的 Binding。
- 把依赖对象(Dependency Object)指定为 Source:依赖对象不仅可以作为 Binding 的目标,同时可以作为 Binding 的源。这样就有可能形成 Binding 链。依赖对象中的依赖属性可以作为 Binding 的 Path。
- 把容器的 DataContext 指定为 Source(WPF Data Binding 的默认行为):有时候我们会遇到这样的情况——我们明确知道将从哪个属性获取数据,但具体把哪个对象作为 Binding 源还不能确定。这时候,我们只能先建立一个 Binding、只给它设置 Path 而不设置 Source,让这个 Binding 自己去寻找 Source。这时候 Binding 会自动把控件的 DataContext 当做自己的 Source(它会沿着控件树一层一层向外找,直到找到带有 Path 指定属性的对象为止)。
- 通过 ElementName 指定 Source:在 C# 代码里面可以直接把对象作为 Source 赋值给 Binding,但 XAML 无法访问对象,所以只能使用对象的 Name 属性来找到对象。
- 把 ObjectDataProvider 对象指定为 Source:当数据源的数据不是通过属性而是通过方法暴露给外界的时候,我们可以使用这两种对象来包装数据源再把它们指定为 Source。
- 把使用 LINQ 检索到的数据对象作为 Binding 的源。
下面我们就通过实例分述每种情况。
6.3.3.6 没有 Source 的 Binding——使用 DataContext 作为 Binding 的源
DataContext 属性被定义在 FrameworkElement 类里,这个类是 WPF 控件的基类,这意味着所有 WPF 控件(包括容器控件)都具备这个属性。如前所述,WPF 的 UI 布局是树形结构,这个树的每个结点都是控件,由此我们退出另一个结论——在 UI 元素树的每个结点都有 DataContext。这一点非常重要,因为当一个 Binding 只知道自己的 Path 而不直达自己的 Source 时,它会沿着 UI 元素树一路向树的根部找过去,没路过一个节点都要看看这个结点的 DataContext 是否具有 Path 所指定的属性。如果有,那就把这个对象作为自己的 Source;如果到了树的根部还没有找到,那么这个 Binding 就没有 Source,因而也不会得到数据。让我们看下面的例子:
先创建一个名为Student的类,它具有 Id、Name、Age 三个属性:
public class Student
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public int Age { get; set; }
}
然后在 XAML 创建程序的 UI。
<Window x:Class="DataBinding.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:DataBinding"
xmlns:sys="clr-namespace:System;assembly=System.Runtime"
xmlns:entity="clr-namespace:DataBinding.Entity"
Title="MainWindow" Height="135" Width="300">
<StackPanel>
<StackPanel.DataContext>
<entity:Student Id="6" Age="29" Name="Time" />
</StackPanel.DataContext>
<Grid>
<StackPanel>
<TextBox Text="{Binding Path=Id}" Margin="5"/>
<TextBox Text="{Binding Path=Name}" Margin="5"/>
<TextBox Text="{Binding Path=Age}" Margin="5"/>
</StackPanel>
</Grid>
</StackPanel>
</Window>
这个UI的布局可以用如图6-11所示的树状图来表示:
(图6-11)
使用 xmlns:local="clr-namespace:DataBinding",我们就可以在 XAML 代码中使用上面在 C# 代码中定义的 Student 类。使用这几行代码:
依赖属性有一个很重要的特点就是当你没有为控件的某个依赖属性显式赋值时,控件会把自己容器的属性值“借过来”当做自己的属性值。实际上是属性值沿着 UI 元素树向下传递了。这里有个简单的小例子,程序的UI部分是若干层Grid,最内层Grid里放置了一个Button,我们为最外层的Grid设置了DataContext属性值,因为内层的Grid和Button都没有设置DataContext属性值所以最外层Grid的DataContext属性值会一直传递到Button那里,单击Button就会显示这个值。
程序的 XAML 代码如下:
<Grid DataContext="6">
<Grid>
<Grid>
<Grid>
<Button x:Name="btn" Content="OK" Click="Btn_OnClick"></Button>
</Grid>
</Grid>
</Grid>
</Grid>
Button 的 Click 事件处理器代码如下:
private void Btn_OnClick(object sender, RoutedEventArgs e)
{
MessageBox.Show(this.btn.DataContext.ToString());
}
在实际工作中 DataContext 的用法是非常灵活的。比如:
- 当 UI 上的多个控件都使用 Binding 关注同一个对象时,不妨使用 DataContext。
- 当作为 Source 的对象不能被直接访问的时候——比如 B 窗体的控件想把 A 窗体内的控件当做自己的 Binding 源时,但 A 窗体内的控件是 private 访问级别,这时候就可以把这个控件作为窗体 A 的 DataContext 从而暴露数据。
形象地说,这时候外层容器的 DataConetxt 就相当于一个数据的“制高点”,只要把数据放上去,别的元素就能看见。另外,DataContext 本身也是一个依赖属性,我们可以使用 Binding 把它关联到一个数据源上。
6.3.7 使用集合对象作为列表控件的 ItemsSource
WPF 的列表控件们派生自 ItemsControl 类,自然也就继承了 ItemsSource 这个属性。ItemsSource 属性可以接收一个 IEnumeable 接口派生类的实例作为自己的值(所有可被迭代遍历的集合都实现了这个接口,包括数组、List<T>)。每个 ItemsControl 的派生类都具有自己对应的条目容器(Item Container),例如,ListBox的条目容器是ListBoxItem、ComboBox的条目容器是ComboBoxItem。ItemsSource 里存放的时一条一条的数据,要想把数据显示出来需要为它们穿上“外衣”,条目容器就起到据外衣的作用。怎样让每件数据外衣与它对应的数据条目关联起来呢?当然是依靠 Binding!只要我们为一个 ItemsControl 对象设置了 ItemsSource 属性值,ItemsControl 对象就会自动迭代其中的数据元素、为每个数据元素准备一个条目容器,并使用 Binding 在条目容器与数据元素之间建立起关联。让我们看这样一个例子:
它的 UI 代码如下:
<StackPanel x:Name="StackPanel" Background="LightBlue">
<TextBlock Text="Student ID:" FontWeight="Bold" Margin="5" />
<TextBox x:Name="TextBox" Margin="5"/>
<TextBlock Text="Student List:" FontWeight="Bold" Margin="5" />
<ListBox x:Name="ListBoxStudents" Height="110" Margin="5" />
</StackPanel>
我们要实现的效果是把一个 List<Student> 集合的实例作为 ListBox 的 ItemsSource,让 ListBox 显示 Student 的 Name 并使用 TextBox 显示 ListBox 当前选中条目的 Id。为了实现这样的功能,我们需要在Window1 的构造器中写几行代码:
public MainWindow()
{
InitializeComponent();
List<Student> students = 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 },
};
// 为 ListBox 设置 Biding
this.ListBoxStudents.ItemsSource = students;
this.ListBoxStudents.DisplayMemberPath = "Name";
// 为 TextBox 设置 Binding
Binding binding = new Binding("SelectedItem.Id") { Source = this.ListBoxStudents };
this.TextBox.SetBinding(TextBox.TextProperty, binding);
}
你可能会想:这个例子里并没有看到你刚才说的Binding。实际上,“this.listBoxStudents.DisplayMemberPath="Name";”这句代码还是露出了一些蛛丝马迹。注意到它包含“Path”这个单词了吗?这说明它是一个路径。当 DisplayMemberPath 属性被赋值后,ListBox 在获得 ItemsSource 的时候就会创建等量的 ListBoxItem 并以 DisplayMemberPath 属性值为 Path 创建 Binding,Binding 的目标是 ListBoxItem 的内容插件。
如果在 ItemsControl 类的代码里刨根问底,你会发现这个创建 Binding 的过程是在 DisplayMemberTemplateSelector 类的 SelectTemplate 方法里完成的。这个方法的定义格式如下:
public override DataTemplate SelectTemplate(object item, DependenyObject container)
{
// logical Code here...
}
在这里我们倒不必关心它的完整内容,注意到它的返回值了吗?是一个DataTemplate类型的值。数据的“外衣”就是由DataTemplate穿上的!当我们没有为ItemsControl显式地指定DataTemplate时SelectTemplate方法就会为我们创建一个默认的(也是最简单的)DataTemplate——就好像给数据穿上一件最简单的衣服一样。至于什么是DataTemplate以及这个方法的完整代码将会放到与Template相关的章节去仔细讨论,这里,我们只关心SelectTemplate内部与创建Binding相关的几行代码:
FrameworkElementFactory text = ContentPresenter.CreateTextBlockFactory();
Binding binding = new Binding();
binding.Path = new PropertyPath(_displayMemberPath);
binding.StringFormat = _stringFormat;
text.SetBinding(TextBlock.TextProperty, binding);
注意:这里只对新创建的 Binding 设定了 Path 而没有为它指定 Source,紧接着就把它关联到了 TextBlock 控件上。显然,要想得到 Source,这 Binding 要向 UI 去寻找包含 _displayMemberPath 指定属性的DataContext。
最后,我们再看一个显式地为数据设置DataTemplate的例子。先把C#代码中的“this.listBoxStudents.DisplayMemberPath="Name";”一句删除,再在XAML中添加几行代码,ListBox的ItemTemplate属性(继承自ItemsControl类)的类型是DataTemplate,下面的代码就是我们为Student类型实例“量身定做”衣服:
注意:最后特别提醒大家一点:在使用集合类型作为列表控件的 ItemsSource 时一般会考虑使用 ObservableCollection<T> 代替 List<T>,因为 ObservableCollection<T> 类实现了 INotifyCollectionChanged 和 INotifyPropertyChanged 接口,能把集合的变化立刻通知显示它的列表控件,改变会立刻显示出来。
6.3.8 使用 ADO.NET 对象作为 Binding 的源
在 .NET 开发中,我们使用 ADO.NET 类对数据库进行操作。常见的工作是从数据库中把数据读取到 DataTable 中,再把 DataTable 显示在 UI 列表控件里。尽管在流行的软件架构中并不把 DataTable 的数据直接显示在 UI 列表控件里而是先通过 LINQ 等手段把 DataTable 里的数据转换成恰当的用户自定义类型集合,但 WPF 也支持在列表控件与 DataTable 之间建立 Binding。
假设我们已经获得了一个DataTable的实例,并且它的数据内容如表6-1所示。
(表6-1)
现在我们把它显示在一个ListBox里。UI部分的XAML代码如下:
<StackPanel x:Name="StackPanel" Background="LightBlue">
<ListBox x:Name="ListBoxStudent" Height="130" Margin="250" />
<Button Content="Load" Height="25" Margin="5,0" Click="ButtonBase_OnClick"/>
</StackPanel>
C#部分我们只给出Button的Click事件处理器:
private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
DataTable dt = this.Load();
this.ListBoxStudent.DisplayMemberPath = "Name";
this.ListBoxStudent.ItemsSource = dt.DefaultView;
}
其中最重要的一句代码是“this.ListBoxStudents.ItemsSource=dt.DefaultView”。DataTable 的 DefaultView 属性是一个 DataView 类型的对象,DataView 类实现了 IEnumerable 接口,所以可以被赋值给 ListBox.ItemsSource 属性。
多数情况下我们会选择 ListView 控件来显示一个 DataTable,需要做的改动也不是很大。XAML部分的代码如下:
<StackPanel x:Name="StackPanel" Background="LightBlue">
<ListView x:Name="ListViewStudents" Height="130" Margin="5">
<ListView.View>
<GridView>
<GridViewColumn Header="Id" Width="60" DisplayMemberBinding="{Binding Id}"/>
<GridViewColumn Header="Name" Width="80" DisplayMemberBinding="{Binding Name}"/>
<GridViewColumn Header="Age" Width="60" DisplayMemberBinding="{Binding Age}"/>
</GridView>
</ListView.View>
</ListView>
<Button Content="Load" Height="25" Margin="5,0" Click="ButtonBase_OnClick"/>
</StackPanel>
这里有几个点需要注意的地方:
首先,从字面上理解 ListView 和 GridView 应该是同一级别的控件,实际上远非这样!ListView 是 ListBox 的派生类而 GridView 是 ViewBase 的派生类,ListView 的 View 属性是一个 ViewBase 类型的对象,所以,GridView 可以作为 ListView 的 View 来使用而不能当做独立的控件来使用。这里使用的理念是组合模式,即 ListView “有一个” View,至于这个 View 是 GridView 还是其他什么类型的 View 由程序员自由选择——目前只有一个 GridView 可以弄个,估计微软在这里还会有扩展。
其次,GridView 的内容属性是 Columns,这个属性是 GridViewColumnCollection 类型对象。因为 XAML 支持对内容属性的简写,所以省略了 <GridView.Columns>…</GridView.Columns> 这层标签,直接在 <GridView> 的内容部分定义了三个 GridViewColumn 对象。GridViewColumn 对象最重要的一个属性是 DisplayMemberBinding(类型为 BindingBase),使用者属性可以指定这一列使用什么样的 Binding 去关联数据——这与 ListBox 有点不同,ListBox 使用的是 DisplayMemberPath 属性(类型为 String)。如果想用更复杂的结构来表示这一列的标题(header)或数据,则可为 GridViewColumn 设置 HeaderTemplate 和 CellTemplate 属性,它们的类型都是 DataTemplate。
C#代码中,Button 的 Click 事件处理器基本没有变化:
private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
DataTable dt = this.Load();
this.ListViewStudents.ItemsSource = dt.DefaultView;
}
通过上面的例子我们已经知道DataTable对象的DefaultView属性可以作为ItemsSource使用。拿DataTable直接作为ItemsSource可以吗?如果把代码改成这样:
private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
DataTable dt = this.Load();
this.ListViewStudents.ItemsSource = dt;
}
会得到一个编译错误:Cannot implicitly convert type 'System.Data.DataTable' to 'System.Collection.IEnumerable'. An explicit conversion exists(are u missinga case?)
显然,DataTable 不能直接拿来为 ItemsSource 赋值。不过,当你把 DataTable 对象放在一个对象的 DataContext 属性里,并把 ItemsSource 与一个既没有指定 Source 又没有指定 Path 的 Binding 关联起来时,Binding 却能自动找到它的 DefaultView 并把它当做自己的 Source 来使用:
private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
DataTable dt = this.Load();
this.ListViewStudents.DataContext = dt;
this.ListViewStudents.SetBinding(ListView.ItemsSourceProperty, new Binding() {});
}
所以,如果你在代码中发现把 DataTbale 而不是 DefaultView 作为 DataContext 的值,并且为 ItemsSource 设置一个既无 Path 又无 Source 的 Binding 时,千万别感觉迷惑。
6.3.9 使用 XML 数据作为 Binding 的源
迄今为止,.NET Framework 提供了两套处理 XML 数据的类库:
- 符合 DOM(Document Object Modle,文档对象模型)标准的类库:包括 XmlDocument、XmlElement、XmlNode、XmlAttribute 等类。这套类库的特点是中规中矩、功能强大,但也背负了太多 XML 的传统和复杂。
- 以 LINQ(Language0Integrated Query,语言集成查询)为基础的类库:包括 XDocument、XElement、XNode、XAttribute 等类。这套类库的特点是可以使用 LINQ 进行查询和操作,方便快捷。
本小节我们主要讲解基于DOM标准的XML类库,基于LINQ的部分我们放在接下来的一节里讨论。
现代程序设计只要涉及数据传输就离不开 XML,因为大多数数据传输都基于 SOAP(Simple Object Access Protocal,简单对象访问协议)相关的协议,而 SOAP 又是通过将对象序列化为 XML 文本进行传输。XML 文本是树形结构的,所以 XML 可以方便地用于表示线性集合(如 Array、List等)和树形结构数据。
需要注意的是,当使用 XML 数据作为 Binding 的 Source 时我们将使用 XPath 属性而不是 Path 水泥杆来制定数据的数据源。
先来看一个线性集合的例子。下面的 XML 文本是一组学生信息(假设存放在 D:\RawData.xml 文件中),我要把它显示在一个 ListView 控件里:
<?xml version="1.0" encoding="utf-8" ?>
<StudentList>
<Student Id="1">
<Name>Tim</Name>
</Student>
<Student Id="2">
<Name>Tom</Name>
</Student>
<Student Id="3">
<Name>Vim</Name>
</Student>
<Student Id="4">
<Name>Emly</Name>
</Student>
</StudentList>
程序的 XAML 部分如下:
<StackPanel x:Name="StackPanel" Background="LightBlue">
<ListView x:Name="ListViewStudents" Height="130" Margin="5">
<ListView.View>
<GridView>
<GridViewColumn Header="Id" Width="80" DisplayMemberBinding="{Binding XPath=@Id}"/>
<GridViewColumn Header="Name" Width="120" DisplayMemberBinding="{Binding XPath=Name}"/>
</GridView>
</ListView.View>
</ListView>
<Button Content="Load" Height="25" Margin="5,0" Click="ButtonBase_OnClick"/>
</StackPanel>
Button的Click事件处理器代码如下:
private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
XmlDocument doc = new XmlDocument();
doc.Load(@"D:\RawData.xml");
XmlDataProvider xdp = new XmlDataProvider();
xdp.Document = doc;
xdp.XPath = @"/StudentList/Student";
this.ListViewStudents.DataContext = xdp;
this.ListViewStudents.SetBinding(ListView.ItemsSourceProperty, new Binding());
}
XmlDataProvider 还有一个名为 Source 的属性,可以用它直接指定 XML 文档所在的位置(无论 MXL 文档存储在本地硬盘还是网络上),所以 Click 事件处理器也可以写成这样:
(代码)
XAML 代码中最关键两句是“DisplayMemberBinding="Binding XPath=@Id}";” 和 “DisplayMemberBinding="{Binding XPath=Name}";”, 它们分别为 GridView 的两列指明了关注的 XML 路径——很明显,使用 @ 符号加字符串表示的是 XML 元素的 Attribute,不加 @ 符号的字符串表示的是子级元素。
XPath作为XML语言的功能有着一整套语法,讲述这些语法走出了本书的范围。MSDN里有对XPath很详尽的讲解可以查阅。
XML 语言可以方便地表示树形结构,下面的例子是使用 TreeView 控件来显示拥有若干层目录的文件系统,而且,这次是把 XML 数据和 XmlDataProvider 对象直接写在 XAML 代码里。diamante中用到了 HierarchicalDataTemplate 类,这个类具由名为 ItemsSource 的属性,可见由这种Template 展示的数据是可以拥有自己集合的。
<Window x:Class="DataBinding.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:DataBinding"
xmlns:sys="clr-namespace:System;assembly=System.Runtime"
xmlns:entity="clr-namespace:DataBinding.Entity"
mc:Ignorable="d"
Title="MainWindow" Height="240" Width="300">
<Window.Resources>
<XmlDataProvider x:Key="xdp" XPath="FileSystem/Folder">
<x:XData>
<FileSystem xmlns="">
<Folder Name="Books">
<Folder Name="Programming">
<Folder Name="Windows">
<Folder Name="WPF"/>
<Folder Name="MFC"/>
<Folder Name="Delphi"/>
</Folder>
</Folder>
<Folder Name="Tools">
<Folder Name="Development"/>
<Folder Name="Designment"/>
<Folder Name="Players"/>
</Folder>
</Folder>
</FileSystem>
</x:XData>
</XmlDataProvider>
</Window.Resources>
<Grid>
<TreeView ItemsSource="{Binding Source={StaticResource xdp}}">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding XPath=Folder}">
<TextBlock Text="{Binding XPath=@Name}"/>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
</Grid>
</Window>
需要注意的是,如果把 XmlDataProvider 直接写在 XAML 代码里,那么它的 XML 数据需要放在 <x:Data>...</x:Data> 标签里。
6.3.10 使用 LINQ 检索结果作为 Binding 的源
自 3.0 开始,.NET Framework 开始支持 LINQ(Language-Integrated Query,语言继承查询),使用 LINQ,我们可以方便地操作集合对象,DataTable 对象和 XML 对象而不必动辄就把好几层 foreach 循环嵌套在一起却只是为了完成一个很简单的任务。
LINQ 查询的结果是一个 IEnumerable<T> 类型对象,而 IEnumerable<T> 又派生自 IEnumerable,所以它可以作为列表控件的 ItemsSource 来使用。
我创建了一个名为 Student 的类:
public class Student
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public int Age { get; set; }
}
又设计了如下的 UI 用于在 Button 被单击的时候显示一个 Student 集合类型对象。
<StackPanel Background="LightBlue">
<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,0" Click="ButtonBase_OnClick" />
</StackPanel>
先来看查询集合对象。要从一个已经填充好的 List<Student> 对象中检索出所有名字以 T 开头的学生,代码如下:
private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
List<Student> students = 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 = "Vim", Age = 25 },
new Student() { Id = 5, Name = "Mike", Age = 24 },
};
this.ListViewStudents.ItemsSource = from stu in students where stu.Name.StartsWith("T") select stu;
}
如果数据存放在一个已经填充好的DataTable对象里,则代码是这样:
private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
DataTable dt = this.GetDataTable();
this.ListViewStudents.ItemsSource = from row in dt.Rows.Cast<DataRow>()
where Convert.ToString(row["Name"]).StartsWith("T")
select new Student()
{
Id = int.Parse(row["Id"].ToString()),
Name = row["Name"].ToString(),
Age = int.Parse(row["Age"].ToString()),
};
}
如果数据存储在 XML 文件里(D:\RawData.xml)如下:
<?xml version="1.0" encoding="utf-8" ?>
<StudentList>
<Class>
<Student Id="0" Name="Tim" Age="29"/>
<Student Id="1" Name="Tom" Age="28"/>
<Student Id="2" Name="Mess" Age="27"/>
</Class>
<Class>
<Student Id="3" Name="Tony" Age="26"/>
<Student Id="4" Name="Vim" Age="25"/>
<Student Id="5" Name="Emily" Age="24"/>
</Class>
</StudentList>
则代码会是这样(注意 xdoc.Descendants("Student")这个方法。他可以跨越 XML 的层级):
private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
XDocument xdoc = XDocument.Load(@"D:\RawData.xml");
this.ListViewStudents.ItemsSource =
from Element in xdoc.Descendants("Student")
where Element.Attribute("Name").Value.StartsWith("T")
select new Student()
{
Id = int.Parse(Element.Attribute("Id").Value),
Name = Element.Attribute("Name").Value,
Age = int.Parse(Element.Attribute("Age").Value)
};
}
6.3.11 使用 ObjectDataProvider 对象作为 Binding 的 Source
理想的情况下,上游程序员把类设计好、使用属性把数据暴露出来,下游程序员把这些类的实例作为 Binding 的 Source、把属性作为 Binding 的 Path 来消费这些类。但很难保证一个类的所有数据都使用属性暴露出来,比如我们需要的数据可能是方法的返回值。而中心设计底层类的风险和成本会比较高,况且黑盒引用类库的情况下也不可能更改已经编译好的类,这时候就需要使用 ObjectDataProvider 来包装作为 Binding 源的数据对象了。
ObjectDataProvider,顾名思义就是把对象多维数据源提供给 Binding。前面还提到过 XmlDataProvider。前面还提到过 XmlDataProvider,也就是把 XML 数据作为数据源提供给 Binding。这两个类的父类都是 DataSourceProvider 抽象类。
现在有一个名为 Calculator 的类,它具有计算加减乘除的方法:
public class Calculator
{
public string Add(string arg1, string arg2)
{
double x = 0;
double y = 0;
double z = 0;
if (double.TryParse(arg1, out x) && double.TryParse(arg2, out y))
{
z = x + y;
return z.ToString();
}
return "Input Error!";
}
}
我们先写一个非常简单的小例子来了解ObjectDataProvider类。随便新建一个WPF项目,然后在UI里添加一个Button,Button的Click事件处理器如下:
private void Button_OnClick(object sender, RoutedEventArgs e)
{
System.Windows.Data.ObjectDataProvider odp = new System.Windows.Data.ObjectDataProvider();
odp.ObjectInstance = new Calendar();
odp.MethodName = "Add";
odp.MethodParameters.Add("100");
odp.MethodParameters.Add("200");
MessageBox.Show(odp.Data.ToString());
}
通过这个程序我们可以了解到ObjectDataProvider对象与被它包装的对象关系如图所示。
(图6-24)
了解了ObjectDataProvider的使用方法,现在让我们看看如何把它当作Binding的Source来使用。程序的XAML代码和截图如下:
这个程序需要实现的功能是在上面两个TextBox输入数字后,第3个TextBox能实时地显示数字的和。把代码写在一个名为SetBinding的方法里,然后在窗体的构造器里调用这个方法:
public MainWindow()
{
InitializeComponent();
this.SetBinding();
}
private void SetBinding()
{
ObjectDataProvider odp = new ObjectDataProvider();
odp.ObjectInstance = new Calculator();
odp.MethodName = "Add";
odp.MethodParameters.Add("0");
odp.MethodParameters.Add("0");
Binding bindingtoArg1 = new Binding("MethodParameters[0]")
{
Source = odp,
BindsDirectlyToSource = true,
UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged
};
Binding bindingtoArg2 = new Binding("MethodParameters[1]")
{
Source = odp,
BindsDirectlyToSource = true,
UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged
};
Binding bindtoResult = new Binding(".") { Source = odp };
this.TextBoxArg1.SetBinding(TextBox.TextProperty, bindingtoArg1);
this.TextBoxArg2.SetBinding(TextBox.TextProperty, bindingtoArg2);
this.Result.SetBinding(TextBox.TextProperty, bindtoResult);
}
让我们来分析一下这个方法。前面说过,ObjectDataProvider 类的作用是用来包装一个以方法暴露数据的对象,这里我们先是创建了一个 ObjectDataProvider 对象,然后用一个 Calculator 对象为其 ObjectInstance 属性赋值——这就把一个 Calculator 对象包装在了 ObjectDataProvider 对象里。还有另外一个办法来创建被包装的对象,那就是告诉 ObjectDataProvider 将被包装对象的类型和希望调用的构造器,让 ObjectDataProvider 自己去创建被包装对象,代码大概是这样:
//...
odp.ObjectType = typeof("YourClass");
odp.ConstructorParameters.Add(arg1);
odp.ConstructorParameters.Add(arg2);
//...
因为在 XAML 里创建和使用对象比较麻烦、可读性差,所以一般会在 XAML 代码中使用这种指定类型和构造器的办法。
接着,我们使用 MethodName 属性指定将要调用 Calculator 对象中名为Add的方法——问题又来了,如果 Calculator 类里有多个重载的 Add 方法应该怎么区分呢?我们知道,重载方法的区别在于参数列表,紧接着的两句代码向 MethodParameters 属性中加入了两个 string 类型的对象,这就相当于告诉 ObjectDataProvider 对象去调用 Calculator 对象中具有两个 string 类型参数的 Add 方法,换句话说,MethodParameters 属性是类型敏感的。
准备好数据源后,我们开始创建 Binding。在前面我们已经学习过使用索引器作为 Binding 的 Path,第一个 Binding 它的 Source 是 ObjectDataProvider 对象、Path 是 ObjDataProvider 的 MethodParameters 属性所引用的集合中的第一个元素。BindDirectlyToSource=true 这句话的意思是告诉 Binding 对象只负责把 UI 元素收集到的数据直接写入其 Source 而不是被 ObjectDataProvider 对象包装着的 CalCulator 对象。同时,UpdateSourceTrigger 属性被设置为一有更新立刻将值传回 Source。第二个 Binding 对象是第一个的翻版,只是把 Path 指向了第二个参数。第三个 Binding 对象仍然使用 ObjectDataProvider 对象作为 Source,但使用“.”作为Path——前面说过,当数据源本身就代表数据的时候就使用“.”作 Path,并且“.”在 XAML 代码里可以省略不写。
一般情况下,数据从哪里来哪里就是 Binding 的 Source、数据到哪里去哪里就是 Target。按这个理论,前两个TextBox 应该是 ObjectDataProvider 对象的数据源,而 ObjectDataProvider 对象又是最后一个TextBox的数据源。但实际上,三个 TextBox 都以 ObjectDataProvider 对象为数据源,只是前两个 TextBox 在 Binding 的数据流向上做了限制。这样做的原因不外乎有两个:
- ObjectDataProvider 的 MethodParameters 不是依赖属性,不能作为 Binding 的目标。
- 数据驱动 UI 的理念要求尽可能使用数据对象作为 Binding 的 Source 而把 UI 元素当做 Binding 的 Target。
6.3.12 使用 Binding 的 RelativeSource
当一个 Binding 有民却的数据来源时我们可以通过 Source 或者 ElementName 赋值的办法让 Binding 与之关联。有些时候我们不能确定作为 Source 的对象叫什么名字,但知道它与作为 Binding 目标的对象在 UI 布局上有相对关系,比如控件自己关联自己的某个数据、关联自己某级容器的数据。这时候我们就要使用 Binding 的 RelativeSource 属性。
RelativeSource 属性的数据类型为 RelativeSource 类,通过这个类的几个静态或非静态属性我们可以控制它搜索相对数据源的方式。下面这段 XAML 代码表示的是多层布局控件内放置着一个 TextBox:
<Grid x:Name="g1" Background="red" Margin="10">
<DockPanel x:Name="d1" Background="Orange" Margin="10">
<Grid x:Name="g2" Background="Yellow" Margin="10">
<DockPanel x:Name="d2" Background="LawnGreen" Margin="10">
<TextBox x:Name="TextBox" FontSize="24" Margin="10"/>
</DockPanel>
</Grid>
</DockPanel>
</Grid>
我们把 TextBox 的 Text 属性关联到外层容器的 Name 上。在窗体的构造器里添加几行代码:
<Grid x:Name="g1" Background="red" Margin="10">
<DockPanel x:Name="d1" Background="Orange" Margin="10">
<Grid x:Name="g2" Background="Yellow" Margin="10">
<DockPanel x:Name="d2" Background="LawnGreen" Margin="10">
<TextBox x:Name="TextBox" FontSize="24" Margin="10"/>
</DockPanel>
</Grid>
</DockPanel>
</Grid>
或在XAML中插入等效代码:
<TextBox x:Name="TextBox" FontSize="24" Margin="10" Text="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Grid}, AncestorLevel=1}, Path=Name}"/>
AncesstorLevel 属性指的是 Binding 目标控件为起点的层级偏移量——d2 的偏移量是1、g2 的偏移量是2,依次类推。AncesstorType 属性告诉 Binding 寻找哪个类型对象作为自己的源,不是这个类型的对象会被跳过。上面这段代码的意思是告诉 Binding 从自己的第一层依次向外找,找到第一个 Grid 类型对象把它当做自己的源。
如果 TextBox 需要关联自身的 Name 属性,则代码应该是这样:
public MainWindow()
{
InitializeComponent();
RelativeSource rs = new RelativeSource();
rs.Mode = RelativeSourceMode.Self;
Binding binding = new Binding("Name") { RelativeSource = rs };
this.TextBox.SetBinding(TextBox.TextProperty, binding);
}
RelativeSource 类的 Mode 属性的类型是 RelativeSourceMode 枚举,它的取值有:PreviousData、TemplateParent、Self 和 FindAncestor。RelativeSource 类还有 3 个静态属性:PreviousData、Self 和 TemplateParent,他们的类型是 RelativeResource。实际上这 3 个静态属性就是创建一个 RelativeSource 实例、把实例的 Mode 属性设置为相对应的值,然后返回这个实例。之所以准备着 3 个静态属性是为了在 XAML 代码里直接获取 RelativeSource 实施。下面是它们的源码:
在DataTemplate中会经常用到这3个静态属性,学习DataTemplate时候请留意它们的使用方法。