WPF之数据绑定

Windows GUI运行的机理是使用消息(Mesage)来驱使程序向前运行,消息的主要来源是用户的操作(如单击鼠标、按下按钮),消息又会被Windows翻译并送达目标程序然后被程序所处理。程序是被来自UI的事件(即封装过的消息)驱使问前的,简称“消息驱动”或“事件驱动”。因为消息和事件大都来自于UI,所以统称它们为“UI驱动程序”。使用“UI驱动程序”开发程序是“为了GUI而GUI”、单纯地为了实现程序的GUI化,已经背离了程序的本质一一数据加算法。

Data Binding在WPF中的地位

一般情况下,应用程序会具有三层结构:

  • 数据存储层:由数据库和文件系统构成;
  • 数据处理层:也叫逻辑层,与业务逻辑相关、用于加工处理数据的算法都集中在这里;
  • 数据展示层:把加工后的数据通过可视的界面展示给用户或者通过其他种类的接口展示给别的应用程序(界面和接口两个词在英文中均为interface),还需要收集用户的操作、把它们反馈给逻辑层。

WPF作为一种专门的展示层技术,华丽的外观和动画只是它的表层现象。WPF引入了Data Binding概念以及与之配套的Dependency Property系统和DataTemplate,帮助程序员把思维的重心固定在了逻辑层、让展示层永远处于逻辑层的从属地位。

展示层与逻辑层的沟通使用Data Binding来实现,加工好的数据会自动送达用户界面加以显示,被用户修改过的数据也会自动传回逻辑层,一旦数据被加工好又会被送达用户界面。用加工好的数据驱动程序的用户界面以文字、图形、动画等形式把数据显示出来——这就是“数据驱动UI”。

经过Data Binding优化,所有与业务逻辑相关的算法都处在数据逻辑层,逻辑层成为一个能够独立运转的、完整的体系,而用户界面层则不含任何代码、完全依赖和从属于数据逻辑层。

Binding基础

Binding:音译“绑定”,英文中有“捆绑”、“关联”和“键联”的含义,Binding更注重表达它是一种像桥梁一样的关联关系

把Binding比作数据的桥梁,它的两端分别是Binding的源(Source)目标(Target)。数据从哪里来哪里就是源,Binding是架在中间的桥梁,Binding目标是数据要往哪儿去。一般情况下,Binding源是逻辑层的对象,Binding目标是UI层的控件对象

先创建一个Student类,类的实例作为数据源,代码如下:

public class Student : INotifyPropertyChanged
{
    private string name;

    public string Name
    {
        get { return name; }
        set
        {
            name = value;
            if (PropertyChanged != null)
            {
                this.PropertyChanged.Invoke(this, new PropertyChangedEventArgs("Name"));
            }
        }
    }
    public event PropertyChangedEventHandler PropertyChanged;
}

准备一个TextBox和Button,代码如下:

<StackPanel Margin="0,0,2,0">
    <TextBox x:Name="textBoxName" BorderBrush="Black" Margin="5"/>
    <Button Content="Add Age"  Margin="5"  Click="Button_Click"/>
</StackPanel>

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

public partial class MainWindow : Window
{
    Student stu = null;
    public MainWindow()
    {
        InitializeComponent();
        //准备数据源
        stu = new Student();
        //准备Binding
        Binding binding = new Binding();
        binding.Source = stu;
        binding.Path = new PropertyPath("Name");

        //使用Binding 连接数据源与Binding目标
        BindingOperations.SetBinding(this.textBoxName,TextBox.TextProperty,binding);
        //或
        this.textBoxName.SetBinding(TextBox.TextProperty, binding);
    }

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

上面这段代码可以简化成这样:

public partial class MainWindow : Window
{
    Student stu = null;
    public MainWindow()
    {
        InitializeComponent();
        //三合一操作
        this.textBoxName.SetBinding(TextBox.TextProperty,new Binding("Name") { Source = stu = new Student()});
    }

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

UI上的元素关心的是哪个属性值的变化,这个属性就称为Binding的路径(Path)。Binding是一种自动机制,当值变化后属性要有能力通知Binding,让Binding把变化传递给UI元素,需要在属性的set 语句中激发一个PropertyChanged事件。这个事件不需要自己声明,让作为数据源的类实现System.ComponentModel名称空间中的INotifyPropertyChanged接口,Binding会自动侦听来自这个接口的PropertyChanged事件。

Binding模型如下所示:
WPF之数据绑定

Binding的源与路径

Binding的源也就是数据的源头,只要它是一个对象,并且通过属性(Property)公开自己的数据,它就能作为Binding的源。如果想让作为Binding源的对象具有自动通知Binding自己的属性值已经变化的能力,那么就需要让类实现INotifyPropertyChanged 接口并在属性的set 语句中激发PropertyChanged事件。

除了使用这种对象作为数据源外,还有:

  • 把自己或自己的容器或子级元素当源
  • 用一个控件作为另一个控件的数据源
  • 把集合作为ItemsControl的数据源
  • 使用XML作为TreeView或Menu的数据源
  • 把多个控件关联到一个“数据制高点”上,甚至干脆不给Binding 指定数据源、让它自己去找

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

为了让UI元素产生一些联动效果会使用Binding在控件间建立关联,下面把一个TextBox的Text属性关联在Slider的Value属性上,代码如下:

<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>

上面的代码使用了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的ElementName属性,而是直接把对象赋值给Binding的Source属性,方便重构
this.textBox1.SetBinding(TextBox.TextProperty, new Binding("Value") {Source=slider1});

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

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

控制Binding的方向及数据更新

控制Binding数据流向的属性是Mode,它的类型是BindingMode枚举,可取值为:

  • TwoWay:导致更改源属性或目标属性时自动更新另一方,双向模式
  • OneWay:在更改绑定源(源)时更新绑定目标(目标),从源向目标的单向沟通
  • OnTime:在应用程序启动或数据上下文更改时,更新绑定目标,只在Binding关系确立时读取一次数据
  • OneWayToSource:在目标属性更改时,更新源属性,从目标向源的单向沟通
  • Default:使用绑定目标的默认 Mode 值,Binding的模式会根据目标的实际情况来确定(若是可编辑的就采用双向模式,如TextBox.Text属性;若是只读的则采用单向模式,如TextBlock.Text)。

控制Binding数据更新的属性是UpdateSourceTrigger,它的类型是UpdateSourceTrigger枚举,可取值为:

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

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

Binding的路径(Path)

作为Binding源的对象可能有很多属性,需要由Binding的Path属性来指定Binding到底需要关注哪个属性的值,如前面把Slider控件对象当作源、把它的Value属性作为路径。尽管在XAML代码中或者Binding类的构造器参数列表中以一个字符串来表示Path,但Path的实际类型是PropertyPath

关联在Binding源的属性上

XAML语法如下:

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

等效的C#代码如下:

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

使用Binding的构造器简写为:

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

Binding的多级路径

Binding支持多级路径(通俗地讲就是一路“点”下去),如让一个TextBox显示另外一个TextBox的文本长度,XAML代码如下:

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

等效的C#代码是:

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

效果如下:
WPF之数据绑定

使用索引器作为Path

集合类型的索引器(Indexer)又称为带参属性,也能作为Path来使用。如让一个TextBox显示另一个TextBox文本的第四个字符,XAML代码如下:

<!--XAML编辑器会认为这是错误的语法,但不影响编译-->
<TextBox x:Name="textBox2" Text="{Binding Path=Text.[3],ElementName=textBox1,Mode=OneWay}" BorderBrush="Black" Margin="5"/>

等效的C#代码是:

//可以把Text与[3]之间的那个“.”省略掉,一样可以正确工作
this.textBox2.SetBinding(TextBox.TextProperty, new Binding("Text.[3]")
{
    Source = this.textBox1,
    Mode =BindingMode.OneWay
});

效果如下:
WPF之数据绑定

使用集合或者DataView作为Binding源

当使用一个集合或者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 });

//等效代码
textBox1.SetBinding(TextBox.TextProperty, new Binding("[0]") { Source = stringList });
textBox2.SetBinding(TextBox.TextProperty, new Binding("[0].Length") { Source = stringList, Mode = BindingMode.OneWay });
textBox3.SetBinding(TextBox.TextProperty, new Binding("[0].[2]") { Source = stringList, Mode = BindingMode.OneWay });

效果如下:
WPF之数据绑定

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

//相关类型
class City
{
    public string Name { get; set; }
}
class Province
{
    public string Name { get; set; }
    public List<City> CityList { get; set; }
}
class Country
{
    public string Name { get; set; }
    public List<Province> ProvinceList { get; set; }
}


//Binding
List<Country> countryList = new List<Country> 
{ 
    new Country() 
    { 
        Name = "中国", 
        ProvinceList = new List<Province>() 
        { 
            new Province() 
            { 
                Name = "四川", 
                CityList = new List<City>() 
                { 
                    new City(){ Name = "成都" }
                } 
            } 
        } 
    } 
};
textBox1.SetBinding(TextBox.TextProperty, new Binding("/Name") { Source = countryList });
textBox2.SetBinding(TextBox.TextProperty, new Binding("/ProvinceList/Name") { Source = countryList });
textBox3.SetBinding(TextBox.TextProperty, new Binding("/ProvinceList/CityList/Name") { Source = countryList });
//等效C#代码
textBox1.SetBinding(TextBox.TextProperty, new Binding("[0].Name") { Source = countryList });
textBox2.SetBinding(TextBox.TextProperty, new Binding("[0].ProvinceList[0].Name") { Source = countryList });
textBox3.SetBinding(TextBox.TextProperty, new Binding("[0].ProvinceList[0].CityList[0].Name") { Source = countryList });

效果如下:
WPF之数据绑定

“没有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="textBlock1" TextWrapping="Wrap" Margin="5" FontSize="16" Text="{Binding Path=.,Source={StaticResource ResourceKey=myString}}"/>
</StackPanel>

效果如下:
WPF之数据绑定

上面的代码可以简写成以下形式:

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

等效的C#代码如下:

string myString = "菩提本无树,明镜亦非台。本来无一物,何处惹尘埃。";
//形式1
textBlock1.SetBinding(TextBlock.TextProperty, new Binding(".") { Source = myString });
//形式2,容易被误解为没有指定Path
textBlock1.SetBinding(TextBlock.TextProperty, new Binding() { Source = myString });

为Binding 指定源(Source)的几种方法

Binding的源是数据的来源,只要一个对象包含数据并能通过属性把数据暴露出来,它就能当作Binding的源来使用。常见的办法有:

  • 把普通CLR类型单个对象指定为Source:包括.NET Framework自带类型的对象和用户自定义类型的对象(需实现INotifyPropertyChanged接口),方法有两种——把对象赋值给Binding.Source属性或把对象的Name赋值给Binding.ElementName。
  • 把普通CLR集合类型对象指定为Source:包括数组、ListObservableCollection等集合类型(一般是把ItemsControl派生类控件的ItemsSource 属性关联到一个集合对象上)。
  • 把ADO.NET数据对象指定为Source:包括DataTableDataView等对象。
  • 使用XmlDataProvider把XML数据指定为Source:XML可以用来表示单个数据对象或者集合,可以把树状结构的XML数据作为源指定给与级联式的WPF控件(如TreeView和Menu)关联的Binding。
  • 把依赖对象(Dependency Object)指定为Source:依赖对象可以作为Binding的目标或源,有可能形成Binding链,依赖对象中的依赖属性可以作为Binding的Path
  • 把容器的DataContext 指定为Source(WPF Data Binding的默认行为):建立一个只设置Path不设置Source的Binding(只确定属性、不确定对象),Binding会自动把控件的DataContext当作自己的Source(沿着控件树向外层寻找带有Path指定属性的对象)。
  • 通过ElementName 指定Source:在C#代码里可以直接把对象作为Source赋值给Binding,但XAML无法访问对象,所以只能使用对象的Name属性来找到对象
  • 通过Binding的RelativeSource属性相对地指定Source:当控件需要关注自己的、自己容器的或者自己内部元素的某个值就需要使用这种办法。
  • 把ObjectDataProvider对象指定为Source:当数据源的数据不是通过属性而是通过方法暴露给外界的时候,可以使用这两种对象来包装数据源再把它们指定为Source。
  • 把使用LINQ检索得到的数据对象作为Binding的源:LINQ查询的结果是一个IEnumerable类型对象

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

DataContext属性被定义在FrameworkElement类(WPF控件的基类)里,所有WPF控件(包括容器控件)都具备这个属性。WPF的UI布局是树形结构,每个结点都是控件——在UI元素树的每个结点都有DataContext。当一个Binding只知道自己的Path而不知道自己的Soruce时,它会沿着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; }
}
<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>

UI布局的树状图如下:
WPF之数据绑定

三个TextBox的Text通过Binding获取值,但只为Binding指定了Path,没有指定Source,可以简写为:

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

效果如下:
WPF之数据绑定

当某个DataContext是一个简单类型对象的时候,Path可以设置为“.”或者省略不写,可能看到一个“既没有Path又没有Source的”Binding:

<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>

DataContext是一个“依赖属性”,当没有为控件的某个依赖属性显式赋值时,控件会把自己容器的属性值“借过来”当作自己的属性值。“Binding沿着UI元素树向上找”只是一个错觉,实际上是属性值沿着UI元素树向下传递了

在实际工作中DataContext的用法是非常灵活的,比如:

  • 当UI上的多个控件都是用Binding关注同一个对象时,不妨使用DataContext。
  • 当作为Source的对象不能被直接访问的时候——比如B窗体内的控件想把A窗体内的控件(private访问级别)当作自己的Binding源时,可以把这个控件(或者控件的值)作为窗体A的DataContext(public访问级别)从而暴露数据。

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

WPF中的列表式控件们派生自ItemsControl类,继承了ItemsSource属性。ItemsSource属性可以接收一个IEnumerable接口派生类的实例作为自己的值(所有可被迭代遍历的集合都实现了这个接口,包括数组、List等)。每一个ItemsControl的派生类都具有自己对应的Item Container(条目容器,如ListBox的ListBoxItem、ComboBox的ComboBoxItem)。只要为一个ItemsControl对象设置了ItemsSource属性值,ItemsControl对象就会自动迭代其中的数据元素,为每个数据元素准备一个条目容器,并使用Binding在条目容器与数据元素之间建立起关联。

使用DisplayMemberPath

XAML代码如下:

<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>

C#代码如下:

//准备数据源
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);

效果如下:
WPF之数据绑定

当DisplayMember属性被赋值后,ListBox在获得ItemsSource的时候就会创建等量的ListBoxItem并以DisplayMemberPath属性值为Path创建Binding,Binding的目标是ListBoxItem的内容插件(实际上是一个TextBox)。这个创建Binding的过程是在DisplayMemberTemplateSelector类的SelectTemplate方法里完成的,方法定义的格式如下:

public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
    //...
    FrameworkElementFactory text = ContentPresenter.CreateTextBlockFactory();
    Binding binding = new Binding();
    binding.XPath = _displayMemberPath;
    binding.StringFormat = _stringFormat;
    text.SetBinding(TextBlock.TextProperty, binding);
    //...
}

使用DataTemplate

删除C#代码中的listBoxStudents.DisplayMemberPath = "Name";,XAML代码如下:

<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>

效果如下:
WPF之数据绑定

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

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

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

Id Name Age
1 Tim 29
2 Tom 28
3 Tony 27
4 Kyle 26
5 Vina 25
6 Emily 24

加载方法如下所示:

private DataTable Load()
{
    DataTable dt = new DataTable();
    dt.Columns.Add("Id");
    dt.Columns.Add("Name");
    dt.Columns.Add("Age");
    dt.Rows.Add("1", "Tim", "29");
    dt.Rows.Add("2", "Tom", "28");
    dt.Rows.Add("3", "Tony", "27");
    dt.Rows.Add("4", "Kyle", "26");
    dt.Rows.Add("5", "Vina", "25");
    dt.Rows.Add("6", "Emily", "24");
    return dt;
}

使用ListBox显示DataTable

XAML代码如下:

<StackPanel Background="LightBlue">
    <ListBox x:Name="listBoxStudents" Height="130" Margin="5"/>
    <Button Content="Load" Height="25" Margin="5,0" Click="Button_Click"/>
</StackPanel>

C#代码如下:

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

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

效果如下:
WPF之数据绑定

最重要的代码是listBoxStudents.ItemsSource = dt.DefaultView;,DataTable的DefaultView属性是一个DataView类型的对象(实现了IEnumerable接口),可以被赋值给ListBox.ItemsSource属性。

使用ListView显示DataTable

多数情况下会选择ListView控件来显示一个DataTable,XAML代码如下:

<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>

C#代码如下:

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

    listViewStudents.ItemsSource = dt.DefaultView;  
}

效果如下:
WPF之数据绑定

有几点需要注意的地方:

  • ListView和GridView不是同一级别的控件,ListView是ListBox的派生类,而GridView是ViewBase的派生类。ListView的View属性是一个ViewBase类型的对象,GridView可以作为ListView的View来使用,而不能当作独立的控件来使用。目前,ListView的View只有一个GridView可用,估计微软在这里还会有扩展。
  • GridView的内容属性是Columns(GridViewColumnCollection类型对象),此处省略了<GridView.Columns>...</GridView.Columns>这层标签(XAML支持对内容属性的简写),直接在的内容部分定义了三个GridViewColumn对象。
  • GridViewColumn对象最重要的一个属性是DisplayMemberBinding(类型为BindingBase),可以指定这一列使用什么样的Binding去关联数据(与ListBox不同,ListBox使用的是DisplayMemberPath属性)。
  • 如果想用更复杂的结构来表示这一列的标题(Header)或数据,则可以为GridViewColumn设置HeaderTemplate和CellTemplate属性,它们的类型都是DataTemplate。

DataTable不能直接拿来为ItemsSource赋值。不过可以把DataTable对象放在一个对象的DataContext属性里,并且为ItemsSource设置一个既无Path又无Source的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());
}

使用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的部分放在下一节里讨论。

现代程序设计的大多数数据传输都基于SOAP(Simple Object Access Protocol,简单对象访问协议)相关的协议,而SOAP又是通过将对象序列化为XML文本进行传输。XML文本是树形结构的,所以XML可以方便地用于表示线性集合(如Array、List等)和树形结构数据**。

当使用XML数据作为Binding的Source时将使用XPath属性而不是Path属性来指定数据的来源,XPath作为XML语言的功能有着一整套语法,详尽的讲解可以查阅:

从XML文档显示线性集合

下面的XML文本是一组学生的信息(假设存放在D:\RawData.xml文件中),内容如下:

<?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>

把它显示在一个ListView控件里,XAML代码如下:

<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>

Button的Click事件处理器中可以通过XmlDataProviderDocumentSource属性指定XML,代码如下:

//第一种:手动加载XML文档并赋值给XmlDataProvider的Document属性
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());
}

//第二种:直接指定XML文档所在的位置(无论XML文档存储在本地硬盘还是网络上)给XmlDataProvider的Source属性
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());
}

效果如下:
WPF之数据绑定

XAML代码中关键的是DisplayMemberBinding="{Binding XPath=@Id}"DisplayMemberBinding="{Binding XPath=Name},它们分别为GridView的两列指明了关注的XML路径,使用@符号加字符串表示的是XML元素的Attribute,不加@符号的字符串表示的是子级元素。

从XAML代码显示树形数据结构

XAML代码如下:

<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>

效果如下:
WPF之数据绑定

如果把XmlDataProvider直接写在XAML代码里,它的XML数据需要放在<x:XData>...</x:XData>标签里,StaticResourceHierarchicalDataTemplate等学完ResourceTemplate才会明白。

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

.NET Framework 3.0开始支持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; }
}

XAML代码如下:

<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>

从一个已经填充好的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对象中检索出所有名字以字母T开头的学生,代码如下:

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

private DataTable GetDataTable()
{
    DataTable dt = new DataTable();            
    dt.Columns.Add("Id");
    dt.Columns.Add("Name");
    dt.Columns.Add("Age");
    dt.Rows.Add("1", "Tim", "29");
    dt.Rows.Add("2", "Tom", "28");
    dt.Rows.Add("3", "Tony", "27");
    dt.Rows.Add("4", "Kyle", "26");
    dt.Rows.Add("5", "Vina", "25");
    dt.Rows.Add("6", "Emily", "24");
    return dt;
}

从一个存储数据的XML文件(D:RawData.xml)中检索出所有名字以字母T开头的学生,代码如下:

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

注:xdoc.Descendants("Student")这个方法可以跨越XML的层级。

效果如下:
WPF之数据绑定

使用ObjectDataProvider对象作为Binding的源

有时很难保证一个类的所有数据都使用属性暴露出来,比如需要的数据可能是方法的返回值,重新设计底层类的风险和成本会比较高且黑盒引用类库时不可能改变已经编译好的类,这时就需要使用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!";
    }

    //其他算法...
}

先随便新建一个WPF项目,添加一个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());
}

效果如下:
WPF之数据绑定

由上可知ObjectDataProvider对象与被它包装的对象关系如下所示:
WPF之数据绑定

把ObjectDataProvider当作Binding的Source来使用,实现第三个TextBox能实时地显示上面两个TextBox输入数字的和,代码和截图如下:

<StackPanel Background="LightBlue">
    <TextBox x:Name="textBoxArg1" Margin="5"/>
    <TextBox x:Name="textBoxArg2" Margin="5"/>
    <TextBox x:Name="textBoxResult" Margin="5"/>
</StackPanel>
//创建并配置ObjectDataProvider对象
ObjectDataProvider odp = new ObjectDataProvider();
odp.ObjectInstance = new Calculator();
odp.MethodName = "Add";
odp.MethodParameters.Add("0");
odp.MethodParameters.Add("0");

//以ObjectDataProvider对象为Source创建Binding,Path是ObjectDataProvider对象MethodParameters属性所引用的集合中的元素
//Binding对象只负责把从UI收集到的数据写入其直接Source(即ObjectDataProvider对象)而不是被ObjectDataProvider对象包装着的Calculator对象
//UpdataSourceTrigger属性设置为一有更新立刻将值传回Source
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
};

//以ObjectDataProvider对象为Source创建Binding,ObjectDataProvider对象本身就代表了数据,Path使用的是.而非其Data属性
Binding bindingToResult = new Binding(".") { Source = odp };

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

WPF之数据绑定

ObjectDataProvider类的作用是用来包装一个以方法暴露数据的对象,这里先创建了一个ObjectDataProvider对象,然后用一个Calculator对象为其ObjectInstance属性赋值,就把一个Calculator对象包装在了ObjectDataProvider对象里。

还有另一种办法创建被包装的对象,告诉ObjectDataProvider将被包装对象的类型和希望调用的构造器,让ObjectDataProvider自己去创建被包装对象,一般会在XAML代码中使用这种指定类型和构造器的办法。代码如下:

//...
odp.ObjectType = typeof(YourClass);
odp.ConstructorParameters.Add(arg1);
odp.ConstructorParameters.Add(arg2);
//...

重载方法的区别在于参数列表,上面代码中向MethodParameters属性中加入了两个string类型的对象,相当于告诉ObjectDataProvider对象去调用Calculator对象中具有两个string类型参数的Add方法(MethodParameters属性是类型敏感的)。

三个TextBox都以ObjectDataProvider对象为数据源,只是前两个TextBox在Binding的数据流向上做了限制,原因如下:

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

使用Binding的RelativeSource

有时候只知道Source对象与作为Binding目标的对象在UI布局上的相对关系,比如控件自己关联自己的某个属性、关联自己某级容器的数据,此时要使用Binding的RelativeSource属性。RelativeSource属性的数据类型为RelativeSource类,通过这个类的几个静态或非静态属性可以控制它搜索相对数据源的方式。

RelativeSource类的非静态属性有3个:

  • AncestorLevel属性:以Binding目标控件为起点的层级偏移量(如下面例子中d2的偏移量是1、g2的偏移量为2,依次类推)。
  • AncestorType属性:告诉Binding寻找哪个类型的对象作为自己的源,不是这个类型的对象会被跳过。
  • Mode属性:是RelativeSourceMode枚举,取值有:PreviousData 、TemplatedParent 、Self 和FindAncestor。

RelativeSource类的静态属性3个:PreviousSelfTemplateParent,它们的类型是RelativeSource类。这3个静态属性是为了在XAML代码里直接获取RelativeSource实例,本质是创建一个RelativeSource实例、把实例的Mode属性设置为相应的值,然后返回这个实例

RelativeSource类的源码参考RelativeSource,静态属性的部分源码如下:

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

        return s_previousData;
    }
}

下面举例说明,在多层布局控件内放置一个TextBox,XAML代码如下:

<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>

从Binding的第一层依此向外找,找到第一个Grid类型对象后把它当做自己的源,C#代码、等效XAML、效果截图如下:

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);
Text="{Binding RelativeSource={RelativeSource Mode=FindAncestor,AncestorType={x:Type Grid},AncestorLevel=1},Path=Name}"

WPF之数据绑定

从Binding的第一层依此向外找,找到第二个DockPanel类型对象后把它当做自己的源,C#代码、等效XAML、效果截图如下:

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);
Text="{Binding RelativeSource={RelativeSource Mode=FindAncestor,AncestorType={x:Type DockPanel},AncestorLevel=2},Path=Name}"

WPF之数据绑定

TextBox关联自身的Name属性,C#代码、等效XAML、效果截图如下:

RelativeSource rs = new RelativeSource();
rs.Mode = RelativeSourceMode.Self;
Binding binding = new Binding("Name") { RelativeSource = rs };
textBox1.SetBinding(TextBox.TextProperty, binding);
<!--使用非静态属性-->
Text="{Binding RelativeSource={RelativeSource Mode=Self},Path=Name}"
<!--使用静态属性-->
Text="{Binding RelativeSource={ x:Static RelativeSource.Self} ,Path=Name}"

WPF之数据绑定

Binding对数据的转换与校验

Binding可以通过ValidationRules属性对数据有效性进行校验,通过Converter属性为数据设置转换器(两端要求使用不同的数据类型时)。

Binding的数据校验

Binding的ValidationRules属性类型是Collection可以为每个Binding设置多个数据校验条件的,其中每一个条件是一个ValidationRule类型对象

ValidationRule类是个抽象类,在使用的时候需要创建它的派生类并实现它的Validate方法,对于Validate方法的返回值(ValidationResult类型对象):

  • 校验通过,则把返回值的IsValid属性设置为true。
  • 校验失败,则把返回值IsValid属性设置为false并为ErrorContent属性设置一个合适的消息内容(一般是个字符串)。

在UI上绘制一个TextBox和一个Slider,准备一个ValidationRule的派生类,代码如下:

<StackPanel>
    <TextBox x:Name="textBox1" Margin="5"/>
    <Slider x:Name="slider1" Minimum="0" Maximum="100" Margin="5"/>
</StackPanel>
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");
    }
}

在后台C#代码里使用Binding把Slider和TextBox关联起来——以Slider为源、TextBox为目标。

只在Target被外部方法更新时校验数据

在后台C#代码里使用Binding以Slider为源、TextBox为目标关联起来,Slider的取值范围是0到100,需要校验TextBox里输入的值是不是在0到100这个范围内,C#代码如下:

//建立Binding
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,效果如下所示:
WPF之数据绑定WPF之数据绑定

在Target被Source属性更新时也校验数据

Binding默认只在Target被外部方法更新时校验数据,而来自Binding的Source属性更新Target时是不会进行校验的。当来自Source的数据也有可能出问题时,需要将校验条件的ValidatesOnTargetUpdated属性设置为true

把slider1的取值范围由0到100改成-10到110,当Slider的滑块移出有效范围时,TextBox也会显示校验失败的效果,代码、效果如下:

<Slider x:Name="slider1" Minimum="-10" Maximum="110" Margin="5"/>
//建立Binding
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);

WPF之数据绑定WPF之数据绑定

显示校验失败的错误消息

当校验错误的时候Validate方法返回的ValidationResult对象携带着一条错误消息,显示错误消息需要用到后面才会详细讲解的知识——路由事件(Routed Event)

在创建Binding时要把Binding对象的NotifyOnValidationError属性设为true。这样,当数据校验失败的时候Binding会像报警器一样发出一个信号,这个信号会以Binding对象的Target为起点在UI元素树上传播。信号每到达一个结点,如果这个结点上设置有对这种信号的侦听器(事件处理器),那么这个侦听器就会被触发用以处理这个信号。信号处理完后,程序员还可以选择是让信号继续向下传播还是就此终止——这就是路由事件,信号在UI元素树上的传递过程就称为路由(Route)

代码、效果如下:

//建立Binding
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();
    }
}

WPF之数据绑定

Binding的数据转换

Binding有一种机制称为数据转换(Data Convert),当Source端Path所关联的数据与Target端目标属性数据类型不一致时(如Slider的Value属性是double类型值、TextBox的Text属性是string类型值),可以添加数据转换器(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避免与Binding的Target混淆
  • 第三个参数用于把额外的信息传入方法,若需要传递多个信息则可把信息放入一个集合对象来传入方法

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

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

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

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

//飞机
public class Plane
{
    //Category属性在UI里被映射为轰炸机或战斗机的图标,
    public Category Category { get; set; }
    public string Name { get; set; }
    //State属性在UI里被映射为CheckBox
    public State State { get; set; }
}

轰炸机(Bomber.png)或战斗机(Fighter.png)的图标如下:
WPF之数据绑定
WPF之数据绑定

需要提供两个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的实例,为listBoxPlane添加用于显示数据的DataTemplate,代码如下:

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

<StackPanel Background="LightBlue">
    <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>
    <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>

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,效果如下:
WPF之数据绑定WPF之数据绑定
单击Save按钮后打开D:\PlaneList.txt,数据如下:

Category=Bomber,Name=B-1,State=Locked
Category=Bomber,Name=B-2,State=Available
Category=Fighter,Name=F-22,State=Available
Category=Fighter,Name=Su-47,State=Unknown
Category=Bomber,Name=B-52,State=Unknown
Category=Fighter,Name=J-10,State=Unknown

MultiBinding(多路Binding)

当UI需要显示的信息由不止一个数据来源决定时,需要使用MultiBinding,即多路Binding。MultiBinding与Binding一样均以BindingBase为基类,凡是能使用Binding对象的场合都能使用MultiBinding。

MultiBinding具有一个名为Bindings的属性(类型是Collection),通过这个属性MultiBinding把一组Binding对象聚合起来,处在这个集合中的Binding对象可以拥有自己的数据校验与转换机制,它们汇集起来的数据将共同决定传往MultiBinding目标的数据,示意图如下所示:
WPF之数据绑定

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

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

UI的XAML代码如下:

<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>

设置MultiBinding的代码,实现Converter,代码如下:

//准备基础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);

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

需要注意以下几点:

  • MultiBinding对于添加子级Binding的顺序是敏感的,这个顺序决定了汇集到Converter里数据的顺序。
  • MultiBinding的Converter实现的是IMultiValueConverter接口

程序效果如下:
WPF之数据绑定WPF之数据绑定

参考资料

WPF Binding
XPath 语法
XML Path Language (XPath)
RelativeSource
RelativeSourceMode 枚举

WPF之数据绑定

上一篇:AC自动机 - AcWing 1282 - 搜索关键词


下一篇:2.33 在windows 2012中创建iSCSI虚拟磁盘和iSCSI目标