这一章介绍数据绑定。
本章共计27个示例,全都在VS2008下.NET3.5测试通过,点击这里下载:ConnectedData.rar
1.ShowDataWithoutBinding
注:
<?Mapping XmlNamespace="local" ClrNamespace="TestBinding" ?>
语法已经升级为:
xmlns:local="clr-namespace:TestBinding"
这个例子讲的是在WPF中使用传统的绑定方式
数据绑定,就是要保持数据对象和UI界面的同步。
.NET事件绑定是基于Observer模式的。在.NET2.0中,对Observer进行了一次包装,可以引用System.Component命名空间,实现INotifyPropertyChanged接口,可以获得事件PropertyChanged,以及PropertyChangedEventArgs。于是在这套体系下,事件机制事先搭建好了。接口如下:
namespace System.ComponentModel


{
public delegate void PropertyChangedEventHandler(object sender, PropertyChangedEventArgs e);

public interface INotifyPropertyChanged

{
event PropertyChangedEventHandler PropertyChanged;
}

public class PropertyChangedEventArgs : EventArgs

{
public PropertyChangedEventArgs(string propertyName);

public virtual string PropertyName
{ get; }
}
}
从数据对象到UI
界面:
当实现了INotifyPropertyChanged接口的对象有所改变时,会激发OnPropertyChanged这个接口方法,该方法保证了UI界面的数据同步。
从UI界面到数据对象:
在控件的事件方法中,改变数据对象。原理同上,由于使用了控件内嵌的事件机制,从而更加简单了。

示例1正是这种绑定方式的展现。现在的代码量还可以容忍,当对象数量的增加或者对象属性的增加时,我们就要额外添加更多的代码。这就造成了代码爆炸。所以,我们需要一种更灵活的绑定方式。
2.ShowDataWithBinding
绑定有两种情形,一种是绑定一个对象到一个简单控件,另一种是绑定一组数据到一个数据列表控件。
这个例子讲的是前者。
WPF的数据绑定机制既保证了数据的同步性,又使得数据类型的相应转换正常进行。如下所示:

左边是XMAL的UI元素,右边是C#程序中的Object,中间是数据绑定层,将左右两层连接起来。
那么, 两个层次的语法如下:
UI层,有3种表示方式:
方法1:
<TextBox Text="{Binding Path=Age}" />
方法2:
<TextBox Text="{BindingAge}" />
方式3:
<TextBox Width="100" Height="25">
<TextBox.Text>
<Binding Source="{StaticResource myDataSource}" Path="Age"/>
</TextBox.Text>
</TextBox>

注:绑定后从底层向上开始找数据源,直到发现位置为止,最上层是<Window>
Object层,要设置数据源:
grid1.DataContext = person; // person为对象,可写在XAML的资源中而省略这句,见下
相应前面的XAML中的TextBox控件:<TextBox Text="{Binding Age}" />,于是可以看到显示的是person对象的Age属性
结合Resource技术,可以全都写在XAML中而不用编写后台程序,这样做的前提是这个Object,在这里是Person,有一个用来初始化的构造函数。这时,DataContext绑定的是静态资源{StaticResource Tom},Tom是资源的Key:
<Window
xmlns:local ="clr-namespace:ShowDataWithBinding">

<Window.Resources>
<local:Person x:Key="Tom" Name="Tom" Age="9" />
</Window.Resources>
<Grid DataContext="{StaticResource Tom}">
<TextBlock
>Name:</TextBlock>
<TextBox
Text="{Binding Path=Name}" />
<TextBlock
>Age:</TextBlock>
<TextBox
Text="{Binding Age}" />
<Button
x:Name="birthdayButton">Birthday</Button>
</Grid>
</Window>
当然,按下Button后,后台还是有代码的:

public partial class Window1 : Window
{

void birthdayButton_Click(object sender, RoutedEventArgs e)
{
Person person = (Person)this.FindResource("Tom"));

++person.Age;

MessageBox.Show();
}
}
注意这个FindResource()方法,找到key为Tom的资源后,强制类型转换为Person
以上介绍的都是隐式的数据源,因为只有一个DataSource,所以可以不指定Source属性;当数据源多于1个的时候,这时要指定具体绑定那一个数据源了——称之为显示数据源,关键的是Source属性
<TextBox Text="{Binding Path=Name, Source={StaticResource Tom}}" />
绑定其他类型数据
以上介绍的都只是文本。接下来说的是如何绑定ForeColor这样的类型数据。
现在考虑的是如果 Age〉25,则名字显示为红色。ForeColor是Brush类型,Age为整型。
WPF提供了接口IValueConverter,只要实现了该接口的两个方法,就可以完成这件工作:一个是Convert,另一个是ConvertBack,分别控制正反两个方向的转换。对于当前情况,新建一个类AgeToForegroundConverter,实现如下:
public class AgeToForegroundConverter : IValueConverter

{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)

{
Debug.Assert(targetType == typeof(Brush));
// DANGER! After 25, it's all down hill
int age = int.Parse(value.ToString());
return (age > 25 ? Brushes.Red : Brushes.Black);
}

public object ConvertBack…
}
于是,在XAML中添加这个类AgeToForegroundConverter的资源,并设置相应的Convert属性即可:
<Window.Resources>
<local:AgeToForegroundConverter x:Key="ATFC" />
</Window.Resources>
<TextBox…Text="{Binding Age}"
Foreground="{Binding Path=Age,Converter={StaticResource ATFC}}" />

总结,以上介绍的技术,只限于单独一个对象的绑定,可以取代前面介绍的INotifyPropertyChanged实现模式。
3.ShowDataWithMultiBinding
这个例子讲的是多笔数据绑定。为此,将上节的Person聚集为泛型People类:
class People : List<Person> { }
Window1展示的是如何显示当前项:
通过在XAML资源中添加数据源:
<Window.Resources>
<local:People x:Key="Family">
<local:Person Name="Melissa" Age="36" />
<local:Person Name="Tom" Age="9" />
<local:Person Name="John" Age="11" />
</local:People>
</Window.Resources>
于是,可以直接绑定:
<Grid DataContext="{StaticResource Family}" Name="grid1">
<TextBox Name="nameTextBox" Text="{Binding Path=Name}" />
注意,这次Grid绑定的是Famliy这个集合对象,因为只有一个TextBox,所以默认显示第一项"Melissa"。
为了遍历这个数据集合,我们在其上建立view,重写按钮的Click方法:
private void birthdayButton_Click(Object sender, RoutedEventArgs e)

{
People people = (People)this.FindResource("Family");

ICollectionView view = CollectionViewSource.GetDefaultView(people);

Person person = (Person)view.CurrentItem;

++person.Age;

MessageBox.Show(string.Format("Happy Birthday, {0}, age {1}!", person.Name, person.Age), "Birthday");
}
也就是通过CollectionViewSource类的静态GetDefaultView方法访问这个数据上的视图,获取到当前项,显示在UI上。
Window2展示的是向前和向后移动当前项。
将Window1的一些公用语句抽象成GetFamilyView方法:
ICollectionView GetFamilyView()

{
People people = (People)this.FindResource("Family");
return CollectionViewSource.GetDefaultView(people);
}
观察新添加的Back按钮事件方法:
private void backButton_Click(object sender, RoutedEventArgs e)

{
ICollectionView view = GetFamilyView();
view.MoveCurrentToPrevious();
if (view.IsCurrentBeforeFirst)

{
view.MoveCurrentToFirst();
}
}
为了方式越界,在移动前要判断是否到了尽头。
Prev的方法与Back方法大同小异。
Window3展示了如何把多笔数据绑定到列表控件,如ListBox。
<ListBox… ItemsSource="{Binding}" IsSynchronizedWithCurrentItem="True" />
这里没有Path属性,意为绑定到当前的所有对象。
ItemsSource属性设为"{Binding}",不用设置具体是哪一个Source,默认为找到的第一个数据源。
IsSynchronizedWithCurrentItem属性设置为True,保证了自身选项变化,其他绑定控件也相应跟着改变。这里,当点击Back按钮,ListBox的当前选中项也会随着一起改变。
遗憾的是,显示的并不是我们需要的数据,而是直接把每个Object的Type输出了。
Window4使用了数据模板,解决了Window3窗体遗留的问题,
<ListBox …ItemsSource="{Binding}" IsSynchronizedWithCurrentItem="True">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Path=Name}" />
<TextBlock Text=" (age: " />
<TextBlock Text="{Binding Path=Age}"
Foreground="{Binding Path=Age, Converter={StaticResource ATFC}}" />
<TextBlock Text=")" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
通过设置ItemTemplate属性,可以显示任何形式的数据。使用方法很像ASP.NET中的ListBox模板。

Window5把Window4使用的数据模板放置在Window资源中,从而
<Window.Resources>
<DataTemplate DataType="{x:Type local:Person}">
<StackPanel Orientation="Horizontal">
…
</DataTemplate>
</Window.Resources>
使用标签的DataType属性标志这个数据模板是类型化的。现在,除非另外通知,每当WPF看到Person对象的一个实例:
<ListBox… ItemsSource="{Binding}" IsSynchronizedWithCurrentItem="True" />
就会应用相应的数据模板。注意这个ItemTemplate属性。
Window6展示了排序技术
我们在前面使用到了ICollectionView接口view,可以对其进行排序,每种排序规则都是一个SortDescription对象。如Sort按钮事件方法:
private void sortButton_Click(object sender, RoutedEventArgs e)
{
ICollectionView view = GetFamilyView();
if (view.SortDescriptions.Count == 0)
{
view.SortDescriptions.Add(
new SortDescription("Name", ListSortDirection.Ascending));
view.SortDescriptions.Add(
new SortDescription("Age", ListSortDirection.Descending));
}
else
{
view.SortDescriptions.Clear();
}
}
Window7也是排序,但是排序规则是自定义的
实现IComparer接口的Compare(x, y)方法,可以自定义排序类,并赋给view的CustomSort属性:
class PersonSorter : IComparer
{
public int Compare(object x, object y)
{
Person lhs = (Person)x;
Person rhs = (Person)y;
// Sort Name ascending and Age descending
int nameCompare = lhs.Name.CompareTo(rhs.Name);
if (nameCompare != 0) return nameCompare;
return rhs.Age - lhs.Age;
}
}
下面我们使用这个新的排序规则,重写Sort按钮事件方法:
private void sortButton_Click(object sender, RoutedEventArgs e)
{
ListCollectionView view = (ListCollectionView)GetFamilyView();
if (view.CustomSort == null)
{
view.CustomSort = new PersonSorter();
}
else
{
view.CustomSort = null;
}
}
这时候,我们使用的是ListCollectionView,而不再是ICollectionView。ListCollectionView是WPF提供的类,已经实现了ICollectionView接口。
Window8展示了过滤技术
WPF使用Delegate来完成数据过滤功能。如Sort按钮事件方法:
ICollectionView view = GetFamilyView();
if (view.Filter == null)
{
view.Filter = delegate(object item)
{
return ((Person)item).Age >= 18;
};
}
else
{
view.Filter = null;
}
这样就找到大于18岁的所有人——那个item对象很神奇的。
注:查了一下Filter属性的类型:Predicate<object> Filter;而Predicate是一个委托:
public delegate bool Predicate<T>(T obj);
这就说明了Filter属性需要一个bool类型的回调方法,来逐项判断是否符合条件。
4.ShowDataWithMultiBindingAddNewData
不知道读者发现没有,示例3中Birthday按钮不能保证数据同步,这是因为Person没有实现INotifyPropertyChanged接口。
而为了实现添加新数据的功能,还要让People集合派生于ObservableCollection<Person>基类,而不再是List<Person>。这样,就可以添加一笔数据并保证同步了:
private void addButton_Click(object sender, RoutedEventArgs e)
{
People people = (People)this.FindResource("Family");
people.Add(new Person("Chris", 35));
}
注:INotifyPropertyChanged 适用于单个的类属性,ObservableCollection适用于监视某一堆的数据是否发生变化(add/remove),这是二者的区别。
接下来我们从数据源角度出发,研究数据绑定。
5.SimpleBinding
这个例子讲的是对象数据源ObjectDataSource。
<ObjectDataSource x:Key="myDataSource" TypeName="namespace.class" Asynchronous="True" />语法已经改变为:
<ObjectDataProvider x:Key="myDataSource" ObjectType="{x:Type src:class}" IsAsynchronous="True" />
其中x:Key是绑定源的名称,而ObjectType指出了要绑定到哪一个类,而这个类所在命名空间在这个XAML的开始位置指定。
而绑定的方式和正常绑定机制一样:
<Binding Source="{StaticResource myDataSource}" Path="Name"/>
注:数据绑定一般要在XAML中引用数据类的命名空间,如:
xmlns:src="clr-namespace: namespace "
6.SimpleBindingCompact
这个例子是数据绑定的另一种便捷写法,直接在控件上使用DataContext绑定数据源,而不用再指定Binding的Source属性。
见Window1窗体:
<DockPanel DataContext="{StaticResource myDataSource}">
<TextBlock… Text="{Binding Path=SimpleProperty, Mode=OneTime}"/>
</DockPanel>
如果XAML就是以DockPanel开始的,那么就不能在这个顶级标签上直接指定DataContext,改写如下:
<DockPanel>
<DockPanel.Resources>
<ObjectDataProvider x:Key="myDataSource" ObjectType="{x:Type src:SimpleBinding}" />
</DockPanel.Resources>
<DockPanel.DataContext>
<Binding Source="{StaticResource myDataSource}"/>
</DockPanel.DataContext>
<TextBlock Width="200" Text="{Binding Path=SimpleProperty, Mode=OneTime}"/>
</DockPanel>
可见,条条大路通罗马,更加体现了Binding的灵活性,但对阅读者的要求更高了,至此最少介绍了有5种绑定方式。(另外三种见示例2)
注:Mode=OneTime的使用参见下面的例子。
7.PropertyChangeNotification
This sample shows how to create data items that implement the INotifyPropertyChanged interface to enable propagating changed data to the binding target.
这个示例是示例4的改版。使用了同样的技术,只是这里在BidCollection数据集合中额外添加了计时器,时刻改变集合中元素的值。
8.ObservableCollection
This sample shows how to code the ObservableCollection and bind to elements in the collection.
此例仍然是示例4的改版,使用了同样的技术,只是逻辑上的不同,但是架构与示例4是一样的。
注意按钮事件,将sender装换为控件的基类FrameworkElement,这个类是具有DataContext属性的。
FrameworkElement fe = (FrameworkElement)sender;
NumberList nl = (NumberList)fe.DataContext;
9.ADODataSet
This sample shows how to implement data binding when the data being bound to is in an ADO DataSet. Demonstrates filling the DataSet by connecting to an Access .mdb file. Techniques are shown in both markup and code.
注:请把示例中的数据库文件BookData.mdb复制到C盘根目录下,才能确保正常运行。
ItemContainerStyle
这个例子讲的是WPF如何绑定Access数据库,当然推而广之,绑定到到其它数据库。
仍然使用System.Data.OleDb,仍然将数据集装入到DataSet的名为"BookTable"的DataTable中,然后将其绑定到ListBox:
lb.DataContext = _dataSet; //这里lb是一个ListBox控件
这样,就形成了一个层次关系:
dataSet数据集——"BookTable"数据表——列属性(Title、NumPages和ISBN)
那么在XAML中的绑定,ListBox的ItemsSource属性可以直接指向"BookTable"数据表:
<ListBox Name="lb" Height="500" Width="400"
ItemsSource="{Binding Path=BookTable}"
ItemTemplate ="{StaticResource BookItemTemplate}"
ItemContainerStyle="{StaticResource liStyle}" />
而数据模板中的可以绑定列的名字。
注意,这个ListBox使用了ItemContainerStyle属性,它指定了所有数据项的样式:
<Style x:Key="liStyle" TargetType="{x:Type ListBoxItem}">
<Setter Property="Width" Value="Auto"/>
</Style>
还有就是Click按钮的点击事件,添加了一笔数据,由于绑定是同步于数据对象和UI的,所以对dataSet数据集的修改能立刻反映到UI界面——多了一个ListBoxItem项。当然我们还可以进一步把这笔数据添加到Access数据库中。

10.BindConversionMarkup
This sample shows how to add a value converter to a binding using Extensible Application Markup Language (XAML).
这个例子演示了如何自定义一个转换器。在示例1中已经介绍过。这里再详细说一下这个Convert方法,它的这几个参数的用法:
public object Convert(object o, Type type, object parameter, CultureInfo culture)
{
DateTime date = (DateTime)o;
switch (type.Name)
{
case "String":
return date.ToString("F", culture);
case "Brush":
return Brushes.Red;
default:
return o;
}
}
我们的目标很明确,就是将绑定的数据,根据一些逻辑判断转换成我们需要的新类对象,正如示例1:大于20的数字为红色。或者无需逻辑判断,对这个数据直接进行一些加工,如本例。
其中Object参数是绑定的数据,而type则是我们想要输出的新类型。
parameter是我们提供给转换逻辑的参数,这个值在XAML中表现如下:
<Binding Path="TheDate" Converter="{StaticResource MyConverterReference}" ConverterParameter="parameter" ConverterCulture="en-US" />
有关parameter的用法详见于示例——MultiBinding。
也许我们需要一些文化信息,就是这个culture参数,如果不指定就会从操作系统中得到。
11.MultiBinding
This sample shows how to implement parameterized multibinding. Techniques are shown in both markup and code.
这个示例是上一个例子BindConversionMarkup的继续。还记得上个例子仅仅是把数据源的一个属性进行转换,接口IValue的方法Convert的第一个参数为object o;当我们需要把数据源的多个属性同时进行转换,就需要实现IMultiValueConverter接口了。
object Convert(object[] values, Type targetType, object parameter,