1.Binding功能的简介

Binding可以连接两个对象的数据,它的两端分别是Source(源)和Target(目标)。Source代表数据从哪里来,Target表示数据要到哪里去。一般的情况下,Source是逻辑层的对象,Target是UI层的对象。
Binding关联的对象可能有许多数据,通过路径(Path)可以决定选取对象的哪个数据用来关联。

2.创建一个支持Binding的类

首先,创建一个很简单的类。

  1. class Student
  2. {
  3. private string name;
  4. public string Name
  5. {
  6. get { return name; }
  7. set { name = value; }
  8. }
  9. }

此时,我们的类满足了可以使用Binding的条件之一——拥有属性。除了拥有属性以外,我们的类还需要拥有能够通知Binding属性的值已经变化的能力。
实现这个能力的方法是在属性的set语句中激发一个PropertyChanged事件。这个事件不需要我们自己声明,我们要做的是让作为数据源的类实现System.ComponentModel名称空间中的INotifyPropertyChanged接口。
当为Binding设置了数据源后,Binding就会自动侦听来自这个接口的PropertyChanged事件。
下面是实现的代码:

    class Student:INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        private string name;

        public string Name 
        {
            get { return name; }
            set 
            { 
                name = value;

                //激发事件
                if (PropertyChanged!=null)
                {
                    PropertyChanged.Invoke(this, new PropertyChangedEventArgs("Name"));
                }
            } 
        }
    }

此时,当Name属性的值发生变化时,PropertyChanged事件就会被激发,Binding接收到这个事件后,发现事件的消息告诉它是名为Name的属性发生了值的改变,于是就会通知Binding目标端的UI元素显示新的值。

3.一个简单的Binding例子

首先,准备一个窗体,窗体中有一个TextBox和一个Button,TextBox将作为Binding的Target。我们将会在Button的Click事件发生时改变Student对象的Name属性值。

<Window x:Class="深入浅出WPF.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"
    mc:Ignorable="d"
    Title="Simple Binding" Height="120" Width="300">
    <StackPanel>
        <TextBox x:Name="textBoxName" BorderBrush="Black" Margin="5"/>
        <Button Content="Add Age" Margin="5" Click="Button_Click"/>
    </StackPanel>
</Window>

效果如图所示:
image.png

接下来,我们将使用Binding把数据源和UI元素连接起来。
C#代码如下:

    public partial class MainWindow : Window
    {
        Student stu;

        public MainWindow()
        {
            InitializeComponent();

            //准备数据源
            stu = new Student();

            //准备Binding
            Binding binding = new Binding(); //创建Binding实例
            binding.Source = stu; //为Binding实例指定Source
            binding.Path = new PropertyPath("Name"); //为Binding实例指定Source的Path

            //使用Binding连接数据源与Binding目标
            BindingOperations.SetBinding(this.textBoxName, TextBox.TextProperty, binding);
            //参数1 Binding的Target
            //参数2 Binding的Target的Path
            //参数3 使用哪个Binding实例将二者连接起来
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            stu.Name += "Name";
        }
    }

由于TextBox这类UI元素的基类FrameworkElement对BindingOperations.SetBinding(…)的方法进行了封装,封装的结果也叫SetBinding,只是参数列表发生了变化。
因此,上述的代码还可以简化为:

    public partial class MainWindow : Window
    {
        Student stu;

        public MainWindow()
        {
            InitializeComponent();

            textBoxName.SetBinding(TextBox.TextProperty, new Binding("Name") { Source = stu = new Student() }); //简化代码
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            stu.Name += "Name";
        }
    }

4.Binding的Source与Path

Binding对于Source要求:只要一个对象通过属性(Property)公开自己的数据,它就能作为Binding的Source。

4.1 把控件作为Binding源与Binding标记扩展

Binding可以用于在控件间建立联系。
下面的代码是把一个TextBox的Text属性关联在了Slider的Value属性上:

<Window x:Class="深入浅出WPF.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"
    mc:Ignorable="d"
    Height="120" Width="300">
    <StackPanel>
        <TextBox x:Name="textBox1" Text="{Binding ElementName=slider1, Path=Value}" BorderBrush="Black" Margin="5"/>
        <Slider x:Name="slider1" Maximum="100" Minimum="0" Margin="5"/>
    </StackPanel>
</Window>

运行效果如图所示:
image.png

除了在C#代码中建立Binding外,在XAML代码中也可以方便地设置Binding。
但需要注意的是,在C#代码中可以访问XAML代码中声明的变量,但XAML代码中却无法访问C#代码中声明的变量。
因此,要想在XAML中建立UI元素与逻辑层对象的Binding还需要把逻辑层对象声明为XAML代码中的Resource(资源)。

回过头来看这句XAML代码,它使用了Binding标记扩展语法:

<TextBox x:Name="textBox1" Text="{Binding ElementName=slider1, Path=Value}" BorderBrush="Black" Margin="5"/>

与之等价的C#代码是:

textBox1.SetBinding(TextBox.TextProperty, new Binding("Value") { ElementName = "slider1" });

因为在C#代码中我们可以直接访问控件对象,所以一般不会使用Binding的ElementName属性,而是直接把对象赋值给Binding的Source属性:

textBox1.SetBinding(TextBox.TextProperty, new Binding("Value") { Source = slider1 });

因为Binding类的构造器本身可以接收Path作为参数,所以也可以写为:

<TextBox x:Name="textBox1" Text="{Binding Value,ElementName=slider1}" BorderBrush="Black" Margin="5"/>

4.2 控制Binding的方向及数据更新

控制Binding数据流向的属性是Mode,它的类型是BindingMode枚举。BindingMode枚举共有五个值:

枚举值 效果
Default 根据控件的类型自动选择以下其中之一。
OneTime 只在建立Binding关系时读取一次数据。
OneWay 从Source到Target单向Binding。
OneWayToSource 从Target到Source单向Binding。
TwoWay Source到Target之间双向绑定。

接上述的例子程序,当我们拖动Slider的手柄时,TextBox里会显示出Slider当前的值;如果我们在TextBox里输入一个恰当的值后,按下Tab键,让焦点离开TextBox,则Slider的手柄会跳到相应的值那里。
为什么一定要在TextBox失去焦点之后Slider的值才会改变呢?这是由Binding的另一个属性——UpdateSourceTrigger决定的。它的类型是UpdateSourceTrigger枚举,UpdateSourceTrigger枚举共有四个值:

枚举值 效果
Default 绑定目标属性的默认 UpdateSourceTrigger 值。 大多数依赖属性的默认值为 PropertyChanged,而 Text 属性的默认值为 LostFocus。
Explicit 仅在调用 UpdateSource() 方法时更新绑定源。
LostFocus 每当绑定目标元素失去焦点时,都会更新绑定源。
PropertyChanged 每当绑定目标属性发生更改时,都会更新绑定源。

除此之外,Binding还具有NotifyOnSourceUpdated和NotifyOnTargetUpdated两个bool类型的属性,如果设为true,则当源或目标被更新后Binding会激发相应的SourceUpdated事件和TargetUpdated事件。实际工作中,我们可以通过监听这两个事件来找出有哪些数据或控件被更新了。

4.3 Binding的Path(路径)

尽管在XAML代码或Binding类的构造器参数列表中,我们以一个字符串来表示Path,但Path的实际类型是PropertyPath。
下面让我们看看如何创建Path来应对各种情况。

关联在Binding源的属性上

最简单的情况就是直接把Binding关联在Binding源的属性上,前面的例子就是这样。语法如下:

<TextBox x:Name="textBox1" Text="{Binding Path=Value,ElementName=slider1}"/>

等效的C#代码是:

Binding binding = new Binding() { Path = new PropertyPath("Value"), Source = slider1 };
textBox1.SetBinding(TextBox.TextProperty, binding);

或者使用Binding的构造器简写为:

Binding binding = new Binding("Value") { Source = slider1 };
textBox1.SetBinding(TextBox.TextProperty, binding);

Binding多级Path

Binding还支持多级Path(通俗地讲就是一路“点”下去)。

    <StackPanel>
        <TextBox x:Name="textBox1" BorderBrush="Black" Margin="5" />
        <TextBox x:Name="textBox2" BorderBrush="Black" Margin="5" Text="{Binding Path=Text.Length,ElementName=textBox1,Mode=OneWay}"/>
    </StackPanel>

等效的C#代码是:

textBox2.SetBinding(TextBox.TextProperty, new Binding("Text.Length") { Source = textBox1, Mode = BindingMode.OneWay });

运行效果如图所示:
image.png

使用Indexer(索引器)作为Path

集合类型的Indexer(索引器)又称为带参属性。既然是属性,索引器也能作为Path来使用。
比如我想让一个TextBox显示另一个TextBox文本的第四个字符,可以这样写:

    <StackPanel>
        <TextBox x:Name="textBox1" BorderBrush="Black" Margin="5" />
        <TextBox x:Name="textBox2" BorderBrush="Black" Margin="5" Text="{Binding Path=Text.[3],ElementName=textBox1,Mode=OneWay}"/>
    </StackPanel>

等效的C#代码是:

textBox2.SetBinding(TextBox.TextProperty, new Binding("Text.[3]") { Source = textBox1, Mode = BindingMode.OneWay });

我们甚至可以把Text与[3]之间的那个“.”省略掉,它一样可以正确工作。(虽然在XAML编辑器中,XAML编辑器会认为这样是错误的语法,但不影响编译。)
最终运行效果如图所示:
image.png

使用集合或DataView作为Source

当使用一个集合或者DataView作为Binding源时,如果我们想把它的默认元素(应该也就是第一个元素)当做Path使用,则需要使用这样的语法:

List<string> stringList = new List<string>() { "Tim", "Tom", "Blog" };
textBox1.SetBinding(TextBox.TextProperty, new Binding("/") { Source = stringList });
textBox2.SetBinding(TextBox.TextProperty, new Binding("/Length") { Source = stringList,Mode=BindingMode.OneWay });
textBox3.SetBinding(TextBox.TextProperty, new Binding("/[2]") { Source = stringList,Mode=BindingMode.OneWay });
//等效代码
textBox4.SetBinding(TextBox.TextProperty, new Binding("[0]") { Source = stringList });
textBox5.SetBinding(TextBox.TextProperty, new Binding("[0].Length") { Source = stringList, Mode = BindingMode.OneWay });
textBox6.SetBinding(TextBox.TextProperty, new Binding("[0].[2]") { Source = stringList, Mode = BindingMode.OneWay });

运行效果如图所示:
image.png

如果集合元素的属性仍然还是一个集合,我们想把子级集合中的元素当做Path,则可以使用多级斜线的语法(即一路“斜线”下去),例如:

//相关类型
class LevelC
{
    public string Name { get; set; }
}

class LevelB
{
    public string Name { get; set; }
    public List<LevelC> LevelCList { get; set; }
}

class LevelA
{
    public string Name { get; set; }
    public List<LevelB> LevelBList { get; set; }
}
//初始化对象
List<LevelA> levelAList = new List<LevelA>()
{
    new LevelA()
    {
        Name="A1",
        LevelBList = new List<LevelB>()
        {
            new LevelB()
            {
                Name="A1_B1",
                LevelCList=new List<LevelC>()
                {
                    new LevelC(){Name="A1_B1_C1"},
                    new LevelC(){Name="A1_B1_C2"},
                    new LevelC(){Name="A1_B1_C3"},
                }
            },
            new LevelB()
            {
                Name="A1_B2",
                LevelCList=new List<LevelC>()
                {
                    new LevelC(){Name="A1_B2_C1"},
                    new LevelC(){Name="A1_B2_C2"},
                    new LevelC(){Name="A1_B2_C3"},
                }
            }
        }
    },
    new LevelA()
    {
        Name="A2",
        LevelBList = new List<LevelB>()
        {
            new LevelB()
            {
                Name="A2_B1",
                LevelCList=new List<LevelC>()
                {
                    new LevelC(){Name="A2_B1_C1"},
                    new LevelC(){Name="A2_B1_C2"},
                    new LevelC(){Name="A2_B1_C3"},
                }
            },
            new LevelB()
            {
                Name="A2_B2",
                LevelCList=new List<LevelC>()
                {
                    new LevelC(){Name="A2_B2_C1"},
                    new LevelC(){Name="A2_B2_C2"},
                    new LevelC(){Name="A2_B2_C3"},
                }
            }
        }
    }
};
//Binding
textBox1.SetBinding(TextBox.TextProperty, new Binding("/Name") { Source = levelAList });
textBox2.SetBinding(TextBox.TextProperty, new Binding("/LevelBList/Name") { Source = levelAList });
textBox3.SetBinding(TextBox.TextProperty, new Binding("/LevelBList/LevelCList/Name") { Source = levelAList });
//等效代码
textBox4.SetBinding(TextBox.TextProperty, new Binding("[0].Name") { Source = levelAList });
textBox5.SetBinding(TextBox.TextProperty, new Binding("[0].LevelBList[0].Name") { Source = levelAList });
textBox6.SetBinding(TextBox.TextProperty, new Binding("[0].LevelBList[0].LevelCList[0].Name") { Source = levelAList });

运行效果如图所示:
image.png

4.4 “没有Path”的Binding

当Binding的Source本身就是数据且不需要Path来指明时(例如string、int等基本类型,它们的实例本身就是数据,我们无法指出通过它的哪个属性来访问这个数据),我们只需要将Path的值设置为“.”就可以了。
在XAML代码里,这个“.”可以省略不写,但在C#代码里却不能省略。
代码如下:

    <StackPanel>
        <StackPanel.Resources>
            <sys:String x:Key="myString">
                菩提本无树,明镜亦非台。
                本来无一物,何处惹尘埃。
            </sys:String>
        </StackPanel.Resources>

        <TextBlock x:Name="textBlock1" TextWrapping="Wrap" Margin="5" FontSize="16" Text="{Binding Path=.,Source={StaticResource ResourceKey=myString}}"/>
    </StackPanel>

运行效果如图所示:
image.png
上面的代码可以简写成以下形式:

<--形式1-!>
Text="{Binding .,Source={StaticResource ResourceKey=myString}}"
<--形式2-!>
Text="{Binding Source={StaticResource ResourceKey=myString}}"

形式2这种简写方法很容易被误解为没有指定Path,其实只是省略掉了,与之等效的C#代码如下:

string myString = "菩提本无树,明镜亦非台。本来无一物,何处惹尘埃。";
//形式1
textBlock1.SetBinding(TextBlock.TextProperty, new Binding(".") { Source = myString });
//形式2
textBlock1.SetBinding(TextBlock.TextProperty, new Binding() { Source = myString });

4.5 为Binding指定Source的几种方法

4.5.1 没有Source的Binding——使用DataContext作为Binding的源

把单个CLR类型对象指定为Binding的Source有两种方法:把对象赋值给Binding.Source属性或把对象的Name赋值给Binding.ElementName。
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; }
    public int Age { get; set; }
}

然后在XAML创建程序的UI:

<Window x:Class="深入浅出WPF.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:深入浅出WPF" 
    mc:Ignorable="d"
    Title="Binding Source" Height="135" Width="300">
    <StackPanel Background="LightBlue">
        <StackPanel.DataContext>
            <local:Student Id="6" Age="29" Name="Tim"/>
        </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>

image.png
使用xmlns:local="clr-namespace:深入浅出WPF",我们就可以在XAML代码中使用上面在C#代码中定义的Student类。
使用下面的代码:

        <StackPanel.DataContext>
            <local:Student Id="6" Age="29" Name="Tim"/>
        </StackPanel.DataContext>

就为外层StackPanel的DataContext进行了赋值——它是一个Student对象。
三个TextBox的Text通过Binding获取值,但只为Binding指定了Path,没有指定Source。所以可以简写为:

                <TextBox Text="{Binding Id}" Margin="5"/>
                <TextBox Text="{Binding Name}" Margin="5"/>
                <TextBox Text="{Binding Age}" Margin="5"/>

这样,这3个TextBox的Binding就会自动向UI元素树的上层去寻找可用的DataContext对象。
最终,它们可以在最外层的StackPanel身上找到可用的DataContext对象。
运行效果如图所示:
image.png

前面我们提过,当Binding的Source本身就是数据时,是不需要属性来暴露数据的。此时Binding的Path可以设置为“.”或者省略不写。现在Source也可以省略不写了。
这样,当某个DataContext是一个简单类型对象的时候,我们完全可能看到一个“既没有Path又没有Source的”Binding:

<Window x:Class="深入浅出WPF.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:sys="clr-namespace:System;assembly=mscorlib"
    mc:Ignorable="d"
    Title="Binding Source" Height="135" Width="300" FontSize="16" FontWeight="Bold">
    <StackPanel>
        <StackPanel.DataContext>
            <sys:String>Hello DataContext!</sys:String>
        </StackPanel.DataContext>
        <Grid>
            <StackPanel>
                <TextBlock Text="{Binding}" Margin="5"/>
                <TextBlock Text="{Binding}" Margin="5"/>
                <TextBlock Text="{Binding}" Margin="5"/>
            </StackPanel>
        </Grid>
    </StackPanel>
</Window>

运行效果如图所示:
image.png

你可能会想,Binding是怎样自动向UI元素树的上层寻找DataContext对象并把它作为Source的呢?
其实,“Binding沿着UI元素树向上找”只是WPF给我们的一个错觉,Binding并没有那么智能。之所以会有这种效果是因为DataContext是一个“依赖属性”,依赖属性有一个很重要的特点就是当你没有为控件的某个依赖属性显式赋值时,控件会把自己容器的属性值“借过来”当作自己的属性值。实际上是属性值沿着UI元素树向下传递了。

原书中这个例子中,给Window设置了FontSize和FontWeight两个属性,但内部的TextBlock并没有设置这两个属性。 而在程序运行后的效果中不难看出,TextBlock的字体是发生改变了的。 再根据书中所提到的依赖属性,不难理解TextBlock的FontSize属性和FontWeight属性依赖在了Window的属性上。

这里有个简单的小例子,程序的UI部分是若干层Grid,最内层Grid里放置了一个Button,我们为最外层的Grid设置了DataContext属性值,因为内层的Grid和Button都没有设置DataContext属性值,所以最外层Grid的DataContext属性值会一直传递到Button哪里,单击Button就会显示这个值。
程序的XAML代码如下:

<Window x:Class="深入浅出WPF.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"
    mc:Ignorable="d"
    Title="DataContext" Height="120" Width="240">
    <Grid DataContext="依赖上了!">
        <Grid>
            <Grid>
                <Grid>
                    <Button x:Name="btn" Content="OK" Click="Button_Click"/>
                </Grid>
            </Grid>
        </Grid>
    </Grid>
</Window>

Button的Click事件处理器代码如下:

private void Button_Click(object sender, RoutedEventArgs e)
{
    MessageBox.Show(btn.DataContext.ToString());
}

运行效果如图所示:
image.pngimage.png

在实际工作中DataContext的用法是非常灵活的。比如:
(1)当UI上的多个控件都是用Binding关注同一个对象时,不妨使用DataContext。
(2)当作为Source的对象不能被直接访问的时候——比如B窗体内的控件想把A窗体内的控件当作自己的Binding源时,但A窗体内的控件private访问级别,这时候就可以把这个控件(或者控件的值)作为窗体A的DataContext(这个属性是public访问级别的)从而暴露数据。
形象地说,这时候外层容器的DataContext就相当于一个数据的“制高点”,只要把数据放上去,别的元素就都能看见。另外,DataContext本身也是一个依赖属性,我们可以使用Binding把它关联到一个数据源上。

4.5.2 使用集合对象作为列表控件的ItemsSource

WPF中的列表式控件们派生自ItemsControl类,自然也就继承了ItemsSource这个属性。
ItemsSource属性可以接收一个IEnumerable接口派生类的实例作为自己的值(所有可被迭代遍历的集合都实现了这个接口,包括数组、List等)。

每一个ItemsControl的派生类都具有自己对应的Item Container(条目容器)。例如,ListBox的条目容器是ListBoxItem,ComboBox的条目容器是ComboBoxItem。
ItemsSource里存放的是一条一条的数据,要想把数据显示出来需要为它们穿上“外衣”,条目容器就起到数据外衣的作用。

怎样让每件数据外衣与它对应的数据条目关联起来呢?当然是依靠Binding!
只要我们为一个ItemsControl对象设置了ItemsSource属性值,ItemsControl对象就会自动迭代其中的数据元素,为每个数据元素准备一个条目容器,并使用Binding在条目容器与数据元素之间建立起关联。

让我们看这样一个例子,它的UI代码如下:

<Window x:Class="深入浅出WPF.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:深入浅出WPF" 
    xmlns:sys="clr-namespace:System;assembly=mscorlib"
    mc:Ignorable="d"
    Title="Binding Source" Height="250" Width="300">
    <StackPanel>
        <TextBlock Text="Student ID:" FontWeight="Bold" Margin="5"/>
        <TextBox x:Name="textBoxId" Margin="5"/>
        <TextBlock Text="Student List:" FontWeight="Bold" Margin="5"/>
        <ListBox x:Name="listBoxStudents" Height="110" Margin="5"/>
    </StackPanel>
</Window>

效果如图所示:
image.png
我们要实现的效果是把一个List集合的实例作为ListBox的ItemsSource,让ListBox显示Student的Name并使用TextBox显示ListBox当前选中条目的Id。
为了实现这样的功能,我们需要在MainWindow的构造器中写几行代码:

public MainWindow()
{
    InitializeComponent();

    //准备数据源
    List<Student> stuList = 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设置Binding
    listBoxStudents.ItemsSource = stuList;
    listBoxStudents.DisplayMemberPath = "Name";

    //为TextBox设置Binding
    Binding binding = new Binding("SelectedItem.Id") { Source = listBoxStudents };
    textBoxId.SetBinding(TextBox.TextProperty, binding);
}

运行效果如图所示:
image.png
这个例子中看起来似乎并没有用到Binding,然而listBoxStudents.DisplayMemberPath = "Name";这句代码还是露出了蛛丝马迹。注意到它包含“Path”这个单词了吗?这说明它是一个路径。
当DisplayMember属性被赋值后,ListBox在获得ItemsSource的时候就会创建等量的ListBoxItem并以DisplayMemberPath属性值为Path创建Binding,Binding的目标是ListBoxItem的内容插件(实际上是一个TextBox,下面就会看到)。

如果在ItemsControl类的代码里刨根问题,你会发现这个创建Binding的过程是在DisplayMemberTemplateSelector类的SelectTemplate方法里完成的。这个方法定义的格式如下:

public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
    //逻辑代码...
}

这一部分代码无法通过在代码中F12找到。 通过http://whatdll.com/type/DisplayMemberTemplateSelector.html可以查到,原书中所提到的方法是在PresentationFramework.dll中的MS.Internal.Data.DisplayMemberTemplateSelector下。 通过https://referencesource.microsoft.com/#PresentationFramework/src/Framework/MS/Internal/Data/DisplayMemberTemplateSelector.cs则可以看到具体的代码。

在这里我们不必关心它的完整内容,需要注意的是它的返回值——是一个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到底是个啥玩意儿。 其实我们看一下最终窗口的效果就知道了,listBoxStudents.DisplayMemberPath = "Name";做的其实就是,把Source中的每个元素的Name当做一个TextBlock的Text,然后把TextBlock加入到ListView之中。 也就是说,DataTemplate就是靠什么样的控件组合出来的模板来显示我们的数据。只要把下面的例子看完就可以领会到了。

最后,我们再看一个显式地为数据设置DataTemplate的例子。
先把C#代码中的listBoxStudents.DisplayMemberPath = "Name";一句删除,再在XAML中添加几行代码。
ListBox的ItemTemplate属性(继承自ItemsControl类)的类型是DataTemplate,下面的代码就是我们为Student类型实例“量身定做”衣服:

<Window x:Class="深入浅出WPF.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"
    mc:Ignorable="d"
    Title="Binding Source" Height="320" Width="300">
    <StackPanel>
        <TextBlock Text="Student ID:" FontWeight="Bold" Margin="5"/>
        <TextBox x:Name="textBoxId" Margin="5"/>
        <TextBlock Text="Student List:" FontWeight="Bold" Margin="5"/>
        <ListBox x:Name="listBoxStudents" Height="150" Margin="5">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Horizontal">
                        <TextBlock Text="{Binding Path=Id}" Width="30"/>
                        <TextBlock Text="{Binding Path=Name}" Width="60"/>
                        <TextBlock Text="{Binding Path=Age}" Width="30"/>
                    </StackPanel>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
    </StackPanel>
</Window>

运行效果如图所示:
image.png

这里就是自己搭建了DataTemplate。这个DataTemplate由一个StackPanel组成,StackPanel中又有三个TextBlock,每个TextBlock关联着一个数据。 最终的效果就是创建了6个StackPanel填充到了ListBox中。

最后特别提醒大家一点:在使用集合类型作为列表控件的ItemsSource时一般会考虑使用ObservableCollection代替List,因为ObservableCollection类实现了INotifyCollectionChanged和INotifyPropertyChanged接口,能把集合的变化立刻通知显示它的列表控件,改变会立刻显现出来。

4.5.3 使用ADO.NET对象作为Binding的源

在.NET开发中,我们使用ADO.NET类对数据库进行操作。常见的工作是从数据库中把数据读取到DataTable中,再把DataTable显示在UI列表控件里(如成绩单、博客文章列表、论坛帖子列表等)。
尽管在流行的软件架构中并不把DataTable的数据直接显示在UI列表控件里,而是先通过LINQ等手段把DataTable里的数据转换成恰当的用户自定义类型集合,但WPF也支持在列表控件与DataTable之间直接建立Binding。

假设我们已经获得了一个DataTable的实例,并且它的数据内容如表所示:
image.png
现在我们把它显示在一个ListBox里,UI部分的XAML代码如下:

<Window x:Class="深入浅出WPF.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:深入浅出WPF" 
    xmlns:sys="clr-namespace:System;assembly=mscorlib"
    mc:Ignorable="d"
    Title="DataTable Source" Height="206" Width="250">
    <StackPanel Background="LightBlue">
        <ListBox x:Name="listBoxStudents" Height="130" Margin="5"/>
        <Button Content="Load" Height="25" Margin="5,0" Click="Button_Click"/>
    </StackPanel>
</Window>

C#部分我们只给出Button的Click事件处理器:

private void Button_Click(object sender, RoutedEventArgs e)
{
    //获取DataTable实例
    DataTable dt = Load();

    listBoxStudents.DisplayMemberPath = "Name";
    listBoxStudents.ItemsSource = dt.DefaultView;
}

Load方法中的代码原书中作者并没有给出,笔者也不是很了解DataTable,因此就照猫画虎写了一段代码:

private DataTable Load()
{
    DataTable dt = new DataTable();

    dt.Columns.Add("Id", typeof(string));
    dt.Columns.Add("Name", typeof(string));
    dt.Columns.Add("Age", typeof(string));

    DataRow dr1 = dt.NewRow();
    dr1[0] = "1";
    dr1[1] = "Tim";
    dr1[2] = "29";
    DataRow dr2 = dt.NewRow();
    dr2[0] = "2";
    dr2[1] = "Tom";
    dr2[2] = "28";
    DataRow dr3 = dt.NewRow();
    dr3[0] = "3";
    dr3[1] = "Tony";
    dr3[2] = "27";
    DataRow dr4 = dt.NewRow();
    dr4[0] = "4";
    dr4[1] = "Kyle";
    dr4[2] = "26";
    DataRow dr5 = dt.NewRow();
    dr5[0] = "5";
    dr5[1] = "Vina";
    dr5[2] = "25";
    DataRow dr6 = dt.NewRow();
    dr6[0] = "6";
    dr6[1] = "Emily";
    dr6[2] = "24";

    dt.Rows.Add(dr1);
    dt.Rows.Add(dr2);
    dt.Rows.Add(dr3);
    dt.Rows.Add(dr4);
    dt.Rows.Add(dr5);
    dt.Rows.Add(dr6);

    return dt;
}

运行效果如图所示:
image.png
其中最重要的一句代码是listBoxStudents.ItemsSource = dt.DefaultView;
DataTable的DefaultView属性是一个DataView类型的对象,DataView类实现了IEnumerable接口,所以可以被赋值给ListBox.ItemsSource属性。

多数情况下我们会选择ListView控件来显示一个DataTable,需要做的改动也不是很大。XAML部分的代码如下:

<Window x:Class="深入浅出WPF.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"
    mc:Ignorable="d"
    Title="DataTable Source" Height="280" Width="250">
    <StackPanel Background="LightBlue">
        <ListView x:Name="listViewStudents" Height="200" 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="Button_Click"/>
    </StackPanel>
</Window>

这里有几点需要注意的地方:
首先,从字面上理解,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 Button_Click(object sender, RoutedEventArgs e)
{
    //获取DataTable实例
    DataTable dt = Load();

    listViewStudents.ItemsSource = dt.DefaultView;
}

运行效果如图所示:
image.png

通过上面的例子,我们已经知道DataTable对象的DefaultView属性可以作为ItemsSource使用。那么用DataTable直接作为ItemsSource可以吗?
我们改一下代码尝试一下:

private void Button_Click(object sender, RoutedEventArgs e)
{
    //获取DataTable实例
    DataTable dt = Load();

    listViewStudents.ItemsSource = dt;
}

但当编译时就会出现一个错误:
“无法将类型“System.Data.DataTable”隐式转换为“System.Collections.IEnumerable”。存在一个显式转换(是否缺少强制转换?)”
显然,DataTable不能直接拿来为ItemsSource赋值。

不过,当你把DataTable对象放在一个对象的DataContext属性里,并把ItemsSource与一个既没有指定Source又没有指定Path的Binding关联起来时,Binding却能自动找到它的DefaultView并当作自己的Source来使用:

private void Button_Click(object sender, RoutedEventArgs e)
{
    //获取DataTable实例
    DataTable dt = Load();

    listViewStudents.DataContext = dt;
    listViewStudents.SetBinding(ListView.ItemsSourceProperty, new Binding());
}

所以,如果你在代码中发现把DataTable而不是DefaultView作为DataContext的值,并且为ItemsSource设置一个既无Path又无Source的Binding时,千万别感觉迷惑。

4.5.4 使用XML数据作为Binding源

迄今为止,.NET Framework提供了两套处理XML数据的类库:

  • 符合DOM(Document Object Model,文档对象模型)标准的类库:包括XmlDocument、XmlElement、XmlNode、XMLAttribute等类。这套类库的特点是中规中矩、功能强大,但也背负了太多XML的传统和复杂。
  • 以LINQ(Language-Integrated Query,语言集成查询)为基础的类库:包括XDocument、XElement、XNode、XAttribute等类。这套类库的特点是可以使用LINQ进行查询和操作,方便快捷。

本小节我们主要讲解基于DOM标准的XML类库,基于LINQ的部分我们放在接下来的一节里讨论。

现代程序设计只要涉及数据传输就离不开XML,因为大多数数据传输都基于SOAP(Simple Object Access Protocol,简单对象访问协议)相关的协议,而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>Vina</Name>
  </Student>
  <Student Id="4">
    <Name>Emily</Name>
  </Student>
</StudentList>

程序的XAML部分如下:

<Window x:Class="深入浅出WPF.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"
    mc:Ignorable="d"
    Title="XML Source" Height="280" Width="250">
    <StackPanel Background="LightBlue">
        <ListView x:Name="listViewStudents" Height="200" Margin="5">
            <ListView.View>
                <GridView>
                    <GridView.Columns>
                        <GridViewColumn Header="Id" Width="60" DisplayMemberBinding="{Binding XPath=@Id}"/>
                        <GridViewColumn Header="Name" Width="80" DisplayMemberBinding="{Binding XPath=Name}"/>
                    </GridView.Columns>
                </GridView>
            </ListView.View>
        </ListView>
        <Button Content="Load" Height="25" Margin="5,0" Click="Button_Click"/>
    </StackPanel>
</Window>

Button的Click事件处理器代码如下:

private void Button_Click(object sender, RoutedEventArgs e)
{
    XmlDocument doc = new XmlDocument();
    doc.Load(@"D:\RawData.xml");

    XmlDataProvider xdp = new XmlDataProvider();
    xdp.Document = doc;

    //使用XPath选择需要暴露的数据
    //现在是需要暴露一组Student
    xdp.XPath = @"/StudentList/Student";

    listViewStudents.DataContext = xdp;
    listViewStudents.SetBinding(ListView.ItemsSourceProperty, new Binding());
}

程序运行效果如图所示:
image.png

XmlDataProvider还有一个名为Source的属性,可以用它直接指定XML文档坐在的位置(无论XML文档存储在本地硬盘还是网络上),所以,Click事件处理器也可以写成这样:

private void Button_Click(object sender, RoutedEventArgs e)
{
    XmlDataProvider xdp = new XmlDataProvider();
    xdp.Source = new Uri(@"D:\RawData.xml");
    xdp.XPath = @"/StudentList/Student";

    listViewStudents.DataContext = xdp;
    listViewStudents.SetBinding(ListView.ItemsSourceProperty, new Binding());
}

XAML代码中最关键的两句是DisplayMemberBinding="{Binding XPath=@Id}"DisplayMemberBinding="{Binding XPath=Name},它们分别为GridView的两列指明了关注的XML路径——很明显,使用@符号加字符串表示的是XML元素的Attribute,不加@符号的字符串表示的是子级元素。
XPath作为XML语言的功能有着一整套语法,讲述这些语法走出了原书的范围。MSDN里有对XPath很详尽的讲解可以查阅。

XML语言可以方便地表示树形数据结构,下面的例子是使用TreeView控件来显示拥有若干层目录的文件系统。而且,这次是把XML数据和XmlDataProvider对象直接写在XAML代码里。代码中用到了HierarchicalDataTemplate类,这个类具有名为ItemsSource的属性,可见由这种Template展示的数据是可以拥有子级集合的。
代码如下:

<Window x:Class="深入浅出WPF.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"
    mc:Ignorable="d"
    Title="XML Source" Height="280" Width="260">
    <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:XData>...</x:XData>标签里。
由于这个例子设计的StaticResource和HierarchicalDataTemplate,都是后面的内容,所以相对比较难懂,等学习完后面的Resource和Template相关章节再回来看便会了然于胸。
程序运行效果如图所示:
image.png

4.5.5 使用LINQ检索结果作为Binding的源

自3.0版开始,.NET Framework开始支持LINQ(Language-Integrated Query,语言集成查询)。
使用LINQ,我们可以方便地操作集合对象、DataTable对象和XML对象,而不必动辄就把好几层foreach循环嵌套在一起却只是为了完成一个很简单的任务。
LINQ查询的结果是一个IEnumerable类型对象,而IEnumerable又派生自IEnumerable,所以它可以作为列表控件的ItemsSource来使用。

我创建了一个名为Student的类:

public class Student
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int Age { get; set; }
}

又设计了如下的UI用于在Button被单击的时候显示一个Student集合类型对象:

<Window x:Class="深入浅出WPF.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"
    mc:Ignorable="d"
    Title="LINQ Source" Height="230" Width="280">
    <StackPanel Background="LightBlue">
        <ListView x:Name="listViewStudents" Height="145" 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="Button_Click"/>
    </StackPanel>
</Window>

先来看查询集合对象。
要从一个已经填充好的List对象中检索出所有名字以字母T开头的学生,代码如下:

private void Button_Click(object sender, RoutedEventArgs e)
{
    List<Student> stuList = 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}
    };

    listViewStudents.ItemsSource = from stu in stuList where stu.Name.StartsWith("T") select stu;
}

如果数据存放在一个已经填充好的DataTable对象里,则代码是这样:

private void Button_Click(object sender, RoutedEventArgs e)
{
    DataTable dt = this.GetDataTable();
    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="Vina" Age="25"/>
    <Student Id="5" Name="Emily" Age="24"/>
  </Class>
</StudentList>

则代码会是这样(注意xdoc.Descendants("Student")这个方法,它可以跨越XML的层级):

private void Button_Click(object sender, RoutedEventArgs e)
{
    XDocument xdoc = XDocument.Load(@"D:\RawData.xml");

    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)
        };
}

程序的运行效果如图所示:
image.png

4.5.6 使用ObjectDataProvider对象作为Binding的Source

理想的情况下,上游程序员把类设计好、使用属性把数据暴露出来,下游程序员把这些类的实例作为Binding的Source、把属性作为Binding的Path来消费这些类。但很难保证一个类的所有数据都使用属性暴露出来,比如我们需要的数据可能是方法的返回值。而重新设计底层类的风险和成本会比较高,况且黑盒引用类库的情况下我们也不可能改变已经编译好的类,这时候就需要使用ObjectDataProvider来包装作为Binding源的数据对象了。
ObjectDataProvider,顾名思义就是把对象作为数据源提供给Binding。前面还提到过XmlDataProvider,也就是把XML数据作为数据源提供给Binding。这两个类的父类都是DataSourceProvider抽象类。

现在有一个名为Calculator的类,它具有计算加、减、乘、除的方法:

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_Click(object sender, RoutedEventArgs e)
{
    ObjectDataProvider odp = new ObjectDataProvider();
    odp.ObjectInstance = new Calculator();
    odp.MethodName = "Add";
    odp.MethodParameters.Add("100");
    odp.MethodParameters.Add("200");
    MessageBox.Show(odp.Data.ToString());
}

运行程序、点击Button,效果如图所示:
image.png
通过这个程序我们可以了解到ObjectDataProvider对象与被它包装的对象关系如图所示:
image.png

了解了ObjectDataProvider的使用方法,现在让我们看看如何把它当作Binding的Source来使用。程序的XAML代码和截图如下:

<Window x:Class="深入浅出WPF.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"
    mc:Ignorable="d"
    Title="ObjectDataProvider Source" Height="130" Width="280">
    <StackPanel Background="LightBlue">
        <TextBox x:Name="textBoxArg1" Margin="5"/>
        <TextBox x:Name="textBoxArg2" Margin="5"/>
        <TextBox x:Name="textBoxResult" Margin="5"/>
    </StackPanel>
</Window>

image.png
这个程序需要实现的功能是在上面两个TextBox输入数字后,第三个TextBox能实时地显示数字的和。把代码写在一个名为SetBinding的方法里,然后在窗体的构造器里调用这个方法:

public MainWindow()
{
    InitializeComponent();

    this.SetBinding();
}

private void SetBinding()
{
    //创建并配置ObjectDataProvider对象
    ObjectDataProvider odp = new ObjectDataProvider();
    odp.ObjectInstance = new Calculator();
    odp.MethodName = "Add";
    odp.MethodParameters.Add("0");
    odp.MethodParameters.Add("0");

    //以ObjectDataProvider对象为Source创建Binding
    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 bindingToResult = new Binding(".") { Source = odp };

    //将Binding关联到UI元素上
    textBoxArg1.SetBinding(TextBox.TextProperty, bindingToArg1);
    textBoxArg2.SetBinding(TextBox.TextProperty, bindingToArg2);
    textBoxResult.SetBinding(TextBox.TextProperty, bindingToResult);
}

让我们来分析一下这个方法。
前面说过,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是ObjectDataProvider对象MethodParameters属性所引用的集合中的第一个元素,BindsDirectlyToSource = true这句的意思是告诉Binding对象只负责把从UI收集到的数据写入其直接Source(即ObjectDataProvider对象)而不是被ObjectDataProvider对象包装着的Calculator对象。同时,UpdataSourceTrigger属性被设置为一有更新立刻将值传回Source。
第二个Binding对象是第一个的翻版,只是把Path指向了第二个参数,
第三个Binding对象仍然使用ObjectDataProvider对象作为Source,但使用.作为Path——前面说过,当数据源本身就代表数据的时候就使用.作Path,并且.在XAML代码里可以省略不写。
这里要注意的是,在把ObjectDataProvider对象当做Binding的Source来使用时,这个对象本身就代表了数据,所以这里的Path使用的是.而非其Data属性。
最后一步是把Binding对象关联到3个TextBox对象上。完成后在窗体类的构造器中调用这个方法,程序运行的时候就能看到如图所示的效果:
image.png

一般情况下,数据从哪里来哪里就是Binding的Source、数据到哪里去哪里就应该是Binding的Target。按照这个理论,前两个TextBox应该是ObjectDataProvider对象的数据源,而ObjectDataProvider对象又是最后一个TextBox的数据源。但实际上,三个TextBox都以ObjectDataProvider对象为数据源,只是前两个TextBox在Binding的数据流向上做了限制。这样做的原因不外乎有两个:

  • ObjectDataProvider的MethodParameters不是依赖属性,不能作为Binding的目标。
  • 数据驱动UI的理念要求尽可能地使用数据对象作为Binding的Source而把UI元素当做Binding的Target。

4.5.7 使用Binding的RelativeSource

当一个Binding有明确的数据来源时,我们可以通过为Source或ElementName赋值的办法让Binding与之关联。
有些时候我们不能确定Source的对象叫什么名字,但知道它与作为Binding目标的对象在UI布局上有相对关系,比如控件自己关联自己的某个属性、关联自己某级容器的数据。这时候我们就要使用Binding的RelativeSource属性。
RelativeSource属性的数据类型为RelativeSource类,通过这个类的几个静态或非静态属性我们可以控制它搜索相对数据源的方式。
下面这段XAML代码表示的是多层布局控件内放置着一个TextBox:

<Window x:Class="深入浅出WPF.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"
    mc:Ignorable="d"
    Title="RelativeSource" Height="300" Width="300">
    <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="textBox1" FontSize="24" Margin="10"/>
                </DockPanel>
            </Grid>
        </DockPanel>
    </Grid>
</Window>

我们把TextBox的Text属性关联到外层容器的Name属性上。在窗体的构造器里添加几行代码:

public MainWindow()
{
    InitializeComponent();

    RelativeSource rs = new RelativeSource(RelativeSourceMode.FindAncestor);
    rs.AncestorLevel = 1;
    rs.AncestorType = typeof(Grid);
    Binding binding = new Binding("Name") { RelativeSource = rs };
    textBox1.SetBinding(TextBox.TextProperty, binding);
}

或在XAML中插入等效代码:

Text="{Binding RelativeSource={RelativeSource Mode=FindAncestor,AncestorType={x:Type Grid},AncestorLevel=1},Path=Name}"

AncestorLevel属性指的是以Binding目标控件为起点的层级偏移量——d2的偏移量是1、g2的偏移量为2,依次类推。
AncestorType属性告诉Binding寻找哪个类型的对象作为自己的源,不是这个类型的对象会被跳过。
上面这段代码的意思是告诉Binding从自己的第一层依此向外找,找到第一个Grid类型对象后把它当做自己的源。
运行效果如图所示:
image.png

如果把代码更改为这样:

public MainWindow()
{
    InitializeComponent();

    RelativeSource rs = new RelativeSource(RelativeSourceMode.FindAncestor);
    rs.AncestorLevel = 2;
    rs.AncestorType = typeof(DockPanel);
    Binding binding = new Binding("Name") { RelativeSource = rs };
    textBox1.SetBinding(TextBox.TextProperty, binding);
}

或在XAML中插入等效代码:

Text="{Binding RelativeSource={RelativeSource Mode=FindAncestor,AncestorType={x:Type DockPanel},AncestorLevel=2},Path=Name}"

运行效果如图所示:
image.png

如果TextBox需要关联自身的Name属性,则代码应该是这样:

public MainWindow()
{
    InitializeComponent();

    RelativeSource rs = new RelativeSource();
    rs.Mode = RelativeSourceMode.Self;
    Binding binding = new Binding("Name") { RelativeSource = rs };
    textBox1.SetBinding(TextBox.TextProperty, binding);
}

image.png

RelativeSource类的Mode属性的类型是RelativeSourceMode枚举,它的取值有:PreviousDataTemplatedParentSelfFindAncestor
RelativeSource类还有3个静态属性:PreviousSelfTemplateParent,它们的类型是RelativeSource类。实际上这3个静态属性就是创建一个RelativeSource实例、把实例的Mode属性设置为相应的值,然后返回这个实例。
之所以准备着3个静态属性是为了在XAML代码里直接获取RelativeSource实例。
下面是它们的源码:

public static RelativeSource PreviousData
{
    get
    {
        if (s_previousData == null)
        {
            s_previousData = new RelativeSource(RelativeSourceMode.PreviousData);
        }

        return s_previousData;
    }
}

public static RelativeSource TemplatedParent
{
    get
    {
        if (s_templatedParent == null)
        {
            s_templatedParent = new RelativeSource(RelativeSourceMode.TemplatedParent);
        }

        return s_templatedParent;
    }
}

public static RelativeSource Self
{
    get
    {
        if (s_self == null)
        {
            s_self = new RelativeSource(RelativeSourceMode.Self);
        }

        return s_self;
    }
}

在DataTemplate中会经常用到这3个静态属性,学习DataTemplate时候请留意它们的使用方法。

5.Binding对数据的转换与校验

Binding的作用就像是架在Source和Target之间的桥梁,数据可以在这座桥梁的帮助下来流通。然而就像现实世界中的桥梁可能会设置一些关卡进行安检一样,我们也可以为Binding添加“关卡”对数据有效性进行校验。不仅如此,当Binding两端要求使用不同的数据类型时,我们还可以为数据设置转换器。
Binding用于数据有效性校验的关卡是它的ValidationRules属性,用于数据类型转换的关卡是它的Converter属性。
下面就让我们来学习使用它们。

5.1 Binding的数据校验

Binding的ValidationRules属性类型是Collection,从它的名称和数据类型可以得知,我们是可以为每个Binding设置多个数据校验条件的,其中每一个条件是一个ValidationRule类型对象。
ValidationRule类是个抽象类,在使用的时候我们需要创建它的派生类并实现它的Validate方法。Validate方法的返回值是ValidationResult类型对象,如果校验通过,就把ValidationResult对象的IsValid属性设置为true,反之,需要把IsValid属性设置为false并为其ErrorContent属性设置一个合适的消息内容(一般是个字符串)。

下面这个程序是在UI上绘制一个TextBox和一个Slider,然后在后台C#代码里使用Binding把它们关联起来——以Slider为源、TextBox为目标。Slider的取值范围是0到100,也就是说,我们需要校验TextBox里输入的值是不是在0到100这个范围内。
程序的XAML部分:

<Window x:Class="深入浅出WPF.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"
    mc:Ignorable="d"
    Title="Validation" Height="120" Width="300">
    <StackPanel>
        <TextBox x:Name="textBox1" Margin="5"/>
        <Slider x:Name="slider1" Minimum="0" Maximum="100" Margin="5"/>
    </StackPanel>
</Window>

为了进行校验,需要准备一个ValidationRule的派生类:

public class RangeValidationRule : ValidationRule
{
    //需要实现Validate方法
    public override ValidationResult Validate(object value, CultureInfo cultureInfo)
    {
        double d = 0;
        if (double.TryParse(value.ToString(),out d))
        {
            if (d>=00 && d<=100)
            {
                return new ValidationResult(true, null);
            }
        }

        return new ValidationResult(false, "Validation Failed");
    }
}

然后在窗体的构造器里这样建立Binding:

public MainWindow()
{
    InitializeComponent();

    Binding binding = new Binding("Value") { Source = slider1 };
    binding.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;

    RangeValidationRule rvr = new RangeValidationRule();
    binding.ValidationRules.Add(rvr);

    textBox1.SetBinding(TextBox.TextProperty, binding);
}

完成后运行程序,当输入0到100之间的值时程序正常显示,但输入这个区间之外的值或不能被解析的值时TextBox会显示红色边框,表示值是错误的,不能把它传递给Source。效果如图所示:
image.pngimage.png

Binding进行校验时的默认行为是认为来自Source的数据总是正确的,只有来自Target的数据(因为Target多为UI控件,所以等价于用户输入的数据)才有可能有问题,为了不让有问题的数据污染Source所以需要校验。换句话说,Binding只在Target被外部方法更新时校验数据,而来自Binding的Source属性更新Target时是不会进行校验的。
如果想改变这种行为,或者说当来自Source的数据也有可能出问题时,我们就需要将校验条件的ValidatesOnTargetUpdated属性设置为true

先把slider1的取值范围由0到100改成-10到110:

<Slider x:Name="slider1" Minimum="-10" Maximum="110" Margin="5"/>

然后把设置Binding的代码改为:

public MainWindow()
{
    InitializeComponent();

    Binding binding = new Binding("Value") { Source = slider1 };
    binding.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;

    RangeValidationRule rvr = new RangeValidationRule();
    rvr.ValidatesOnTargetUpdated = true;
    binding.ValidationRules.Add(rvr);

    textBox1.SetBinding(TextBox.TextProperty, binding);
}

这样,当Slider的滑块移出有效范围时,TextBox也会显示校验失败的效果,如图所示:
image.pngimage.png
你可能会想:当校验错误的时候Validate方法返回的ValidationResult对象携带着一条错误消息,如何显示这条消息呢?想要做到这一点,需要用到后面才会详细讲解的知识——路由事件(Routed Event)。
首先,在创建Binding时要把Binding对象的NotifyOnValidationError属性设为true。这样,当数据校验失败的时候Binding会像报警器一样发出一个信号,这个信号会以Binding对象的Target为起点在UI元素树上传播。信号每到达一个结点,如果这个结点上设置有对这种信号的侦听器(事件处理器),那么这个侦听器就会被触发用以处理这个信号。信号处理完后,程序员还可以选择是让信号继续向下传播还是就此终止——这就是路由事件,信号在UI元素树上的传递过程就称为路由(Route)。
建立Binding的代码如下:

public MainWindow()
{
    InitializeComponent();

    Binding binding = new Binding("Value") { Source = slider1 };
    binding.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;

    RangeValidationRule rvr = new RangeValidationRule();
    rvr.ValidatesOnTargetUpdated = true;
    binding.ValidationRules.Add(rvr);
    binding.NotifyOnValidationError = true;

    textBox1.SetBinding(TextBox.TextProperty, binding);
    textBox1.AddHandler(Validation.ErrorEvent, new RoutedEventHandler(ValidationError));

}

用于侦听校验错误事件的事件处理器如下:

private void ValidationError(object sender, RoutedEventArgs e)
{
    if (Validation.GetErrors(textBox1).Count>0)
    {
        textBox1.ToolTip = Validation.GetErrors(textBox1)[0].ErrorContent.ToString();
    }
}

程序运行时如果校验失败,TextBox的ToolTip就会提示用户,如图所示:
image.png

5.2 Binding的数据转换

前面的很多例子中我们都使用Binding在Slider控件与TextBox控件之间建立关联——Slider控件作为Source(Path是Value属性),TextBox控件作为Target(目标属性为Text)。
不知道大家有没有注意到,Slider的Value属性是double类型值、TextBox的Text属性是string类型值,在C#这种强类型(strong-typed)语言中却可以往来自如,这是怎么回事呢?
原来,Binding还有另外一种机制称为数据转换(Data Convert),当Source端Path所关联的数据与Target端目标属性数据类型不一致时,我们可以添加数据转换器(Data Converter)。上面提到的问题实际上是double类型与string类型互相转换的问题,因为处理起来比较简单,所以WPF类库就自动替我们做了。但有些类型之间的转换就不是WPF能替我们做的了,例如下面这些情况:

  • Source里的数据是Y、N和X三个值(可能是char类型、string类型或自定义枚举类型),UI上对应的是CheckBox控件,需要把这三个值映射为它的IsChecked属性值(bool?类型)
  • 当TextBox里已经输入了文字时用于登陆的Button才会出现,这是string类型与Visibility枚举类型或bool类型之间的转换(Binding的Mode将是OneWay)。
  • Source里的数据可能是Male或Female(string或枚举),UI上对应的是用于显示头像的Image控件,这时候需要把Source里的值转换成对应的头像图片URI(亦是OneWay)。

当遇到这些情况时,我们只能自己动手写Converter,方法是创建一个类并让这个类实现IValueConverter接口。IValueConverter接口定义如下:

public interface IValueConverter
{
    object Convert(object value, Type targetType, object parameter, CultureInfo culture);

    object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture);
}

当数据从Binding的Source流向Target时,Converter方法将被调用;反之,ConvertBack方法将被调用。

这两个方法的参数列表一模一样:
第一个参数为object,最大限度地保证了Converter的重用性(可以在方法体内对实际类型进行判断);
第二个参数用于确定方法的返回类型(个人认为形参名字叫outputType比targetType要好,可以避免与Binding的Target混淆);
第三个参数用于把额外的信息传入方法,若需要传递多个信息则可把信息放入一个集合对象来传入方法。

Binding对象的Mode属性会影响到这两个方法的调用。如果Mode为TwoWay或Default行为与TwoWay一致,则两个方法都有可能被调用;如果Mode为OneWay或Default行为与OneWay一致则只有Convert方法会被调用,其他情况同理。

下面这个例子是一个Converter的综合实例,程序的用途是在列表里向玩家显示一些军用飞机的状态。
首先创建几个自定义类型数据:

//种类
public enum Category
{
    Bomber,
    Fighter
}

//状态
public enum State
{
    Available,
    Locked,
    Unknown
}

//飞机
public class Plane
{
    public Category Category { get; set; }
    public string Name { get; set; }
    public State State { get; set; }
}

在UI里Plane的Category属性被映射为轰炸机或战斗机的图标,这两个图标我已经加入了项目,如图所示:
image.png
同时,飞机的State属性在UI里被映射为CheckBox。因为存在以上两个映射关系,我们需要提供两个Converter:
一个是由Category类型单向转换为string类型(XAML编译器能够把string对象解析为图片资源);另一个是在State与bool?类型之间双向转换。
代码如下:

public class CategoryToSourceConverter : IValueConverter
{
    //将Category转换为Uri
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        Category c = (Category)value;
        switch (c)
        {
            case Category.Bomber:
                return @"\Icons\Bomber.png";
            case Category.Fighter:
                return @"\Icons\Fighter.png";
            default:
                return null;
        }
    }

    //不会被调用
    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

public class StateToNullableBoolConverter : IValueConverter
{
    //将State转换为bool?
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        State s = (State)value;
        switch (s)
        {
            case State.Available:
                return true;
            case State.Locked:
                return false;
            case State.Unknown:
            default:
                return null;
        }
    }

    //将bool?转换为State
    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        bool? nb = (bool?)value;
        switch (nb)
        {
            case true:
                return State.Available;
            case false:
                return State.Locked;
            case null:
            default:
                return State.Unknown;
        }
    }
}

下面我们看看如何在XAML里消费这些Converter。
XAML代码的框架如下:

<Window x:Class="深入浅出WPF.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:深入浅出WPF" 
    mc:Ignorable="d"
    Title="Data Converter" Height="400" Width="400">

    <Window.Resources>
        <local:CategoryToSourceConverter x:Key="cts"/>
        <local:StateToNullableBoolConverter x:Key="stnb"/>
    </Window.Resources>

    <StackPanel Background="LightBlue">
        <ListBox x:Name="listBoxPlane" Height="280" Margin="5"/>
        <Button x:Name="buttonLoad" Content="Load" Height="25" Margin="5,0" Click="buttonLoad_Click"/>
        <Button x:Name="buttonSave" Content="Save" Height="25" Margin="5,0" Click="buttonSave_Click"/>
    </StackPanel>
</Window>

XAML代码中已经添加了对于程序集的引用并映射为名称空间local,同时,以资源的形式创建了两个Converter的实例。名为listBoxPlane的ListBox空间是我们工作的重点,需要为它添加用于显示数据的DataTemplate。
我们把焦点集中在ListBox控件的ItemTemplate属性上:

<ListBox x:Name="listBoxPlane" Height="160" Margin="5">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <StackPanel Orientation="Horizontal">
                <Image Width="40" Height="40" Source="{Binding Path=Category,Converter={StaticResource cts}}"/>
                <TextBlock Text="{Binding Path=Name}" Width="60" Margin="80,0"/>
                <CheckBox IsThreeState="True" IsChecked="{Binding Path=State,Converter={StaticResource stnb}}"/>
            </StackPanel>
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

Load按钮的Click事件处理器负责把一组飞机的数据赋值给ListBox的ItemsSource属性,Save按钮的Click事件处理器负责把用户更改过的数据写入文件:

//Load按钮Click事件处理器
private void buttonLoad_Click(object sender, RoutedEventArgs e)
{
    List<Plane> planeList = new List<Plane>()
    {
        new Plane(){Category=Category.Bomber,Name="B-1",State=State.Unknown},
        new Plane(){Category=Category.Bomber,Name="B-2",State=State.Unknown},
        new Plane(){Category=Category.Fighter,Name="F-22",State=State.Unknown},
        new Plane(){Category=Category.Fighter,Name="Su-47",State=State.Unknown},
        new Plane(){Category=Category.Bomber,Name="B-52",State=State.Unknown},
        new Plane(){Category=Category.Fighter,Name="J-10",State=State.Unknown},
    };

    listBoxPlane.ItemsSource = planeList;
}

//Save按钮Click事件处理器
private void buttonSave_Click(object sender, RoutedEventArgs e)
{
    StringBuilder sb = new StringBuilder();
    foreach (Plane p in listBoxPlane.Items)
    {
        sb.AppendLine(string.Format("Category={0},Name={1},State={2}", p.Category, p.Name, p.State));
    }
    File.WriteAllText(@"D:\PlaneList.txt", sb.ToString());
}

运行程序并单击CheckBox更改飞机的State,效果如图所示:
image.pngimage.png
单击Save按钮后打开D:\PlaneList.txt,数据如图所示:
image.png

6.MultiBinding(多路Binding)

有的时候UI需要显示的信息由不止一个数据来源决定,这时候就需要使用MultiBinding,即多路Binding。
MultiBinding与Binding一样均以BindingBase为基类,也就是说,凡是能使用Binding对象的场合都能使用MultiBinding。MultiBinding具有一个名为Bindings的属性,其类型是Collection,通过这个属性MultiBinding把一组Binding对象聚合起来,处在这个集合中的Binding对象可以拥有自己的数据校验与转换机制,它们汇集起来的数据将共同决定传往MultiBinding目标的数据,如图所示:
image.png

考虑这样一个需求,有一个用于新用户注册的UI(包含4个TextBox和一个Button),还有如下一些限定:

  • 第一、二个TextBox输入用户名,要求内容一致。
  • 第三、四个TextBox输入用户E-Mail,要求内容一致。
  • 当TextBox的内容全部符合要求的时候,Button可用。

此UI的XAML代码如下:

<Window x:Class="深入浅出WPF.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:深入浅出WPF" 
    xmlns:sys="clr-namespace:System;assembly=mscorlib"
    mc:Ignorable="d"
    Title="MultiBinding" Height="185" Width="300">
    <StackPanel Background="LightBlue">
        <TextBox x:Name="textBox1" Height="23" Margin="5"/>
        <TextBox x:Name="textBox2" Height="23" Margin="5,0"/>
        <TextBox x:Name="textBox3" Height="23" Margin="5"/>
        <TextBox x:Name="textBox4" Height="23" Margin="5,0"/>
        <Button x:Name="button1" Content="Sumbit" Width="80" Margin="5"/>
    </StackPanel>
</Window>

然后把用于设置MultiBinding的代码写在名为SetMultiBinding的方法里并在窗体的构造器中调用:

public MainWindow()
{
    InitializeComponent();

    SetMultiBinding();
}

private void SetMultiBinding()
{
    //准备基础Binding
    Binding b1 = new Binding("Text") { Source = textBox1 };
    Binding b2 = new Binding("Text") { Source = textBox2 };
    Binding b3 = new Binding("Text") { Source = textBox3 };
    Binding b4 = new Binding("Text") { Source = textBox4 };

    //准备MultiBinding
    MultiBinding mb = new MultiBinding() { Mode = BindingMode.OneWay };
    mb.Bindings.Add(b1); //注意:MultiBinding对于Add子Binding的顺序是敏感的
    mb.Bindings.Add(b2);
    mb.Bindings.Add(b3);
    mb.Bindings.Add(b4);
    mb.Converter = new LogonMultiBindingConverter();

    //将Button与MultiBinding对象关联
    button1.SetBinding(Button.IsEnabledProperty, mb);
}

这里有几点需要注意的地方:
第一,MultiBinding对于添加子级Binding的顺序是敏感的,因为这个顺序决定了汇集到Converter里数据的顺序。
第二,MultiBinding的Converter实现的是IMultiValueConverter接口。

本例的Converter代码如下:

public class LogonMultiBindingConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        if (!values.Cast<string>().Any(text=>string.IsNullOrEmpty(text))
            && values[0].ToString()== values[1].ToString()
            && values[2].ToString()== values[3].ToString())
        {
            return true;
        }
        return false;
    }

    //不会被调用
    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

程序的运行效果如图所示:
image.pngimage.png