WPF数据绑定 Binding

简介

这是一篇记录笔者阅读学习刘铁猛老师的《深入浅出WPF》的读书笔记,如果文中内容阅读不畅,推荐购买正版书籍详细阅读。

本文引用了大量官方文档,如需详细了解请自行查阅MSDN。

什么是数据绑定?

数据绑定是在应用 UI 与其显示的数据之间建立连接的过程,是一座连接应用UI与其显示的数据之间的桥梁。

“一桥飞架南北,天堑变通途”,我们可以想象Binding这座桥梁上铺设了高速公路,我们不但可以控制公路是在源与目标之间双向同行还是某个方向的单行道,还可以控制对数据放行的事件,甚至可以在桥上假设一些“关卡”用来转换数据类型或者校验数据的正确性。

如果把Binding比作数据的桥梁,那么它的两端分别是Binding的源(Source)和目标(Target)。数据从哪里来哪里就是源,Bindng是架在中间的桥梁,Binding目标是数据要往哪儿去(所以我们就要把桥架向哪里)。一般情况下,Binding源是逻辑层的对象,Binding目标是UI层的控件对象,这样,数据就会源源不断通过Binding送达UI层、被UI层展现,也就完成了数据驱动UI的过程。

数据绑定基本概念

不论要绑定什么元素,也不论数据源是什么性质,每个绑定都始终遵循下图所示的模型。

显示基本数据绑定模型的图。

如图所示,数据绑定实质上是绑定目标与绑定源之间的桥梁。 该图演示了以下基本的 WPF 数据绑定概念:

  • 通常情况下,每个绑定具有四个组件:

    • 绑定目标对象。
    • 目标属性。
    • 绑定源。
    • 指向绑定源中要使用的值的路径。

    例如,如果要将 TextBox 的内容绑定到 Employee.Name 属性,则目标对象是 TextBox,目标属性是 Text 属性,要使用的值是 Name,源对象是 Employee 对象。

  • 目标属性必须为依赖属性。 大多数 UIElement 属性都是依赖属性,而大多数依赖属性(只读属性除外)默认支持数据绑定。 (仅派生自 DependencyObject 的类型才能定义依赖属性;所有 UIElement 类型都派生自 DependencyObject。)

  • 尽管未在图中显示,但请注意,绑定源对象不限于自定义 .NET 对象。 WPF 数据绑定支持 .NET 对象和 XML 形式的数据。 例如,绑定源可以是 UIElement、任何列表对象、ADO.NET 或 Web 服务对象,或包含 XML 数据的 XmlNode。 有关详细信息,请参阅绑定源概述

请务必记住,在建立绑定时,需要将绑定目标绑定到绑定源。 例如,如果要使用数据绑定在 ListBox 中显示一些基础 XML 数据,则需要将 ListBox 绑定到 XML 数据。

若要建立绑定,请使用 Binding 对象。

简单示例

Binding的标记扩展:

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

与之等价的C#代码是:

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

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

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

控制Binding的方向及数据更新

Binding的常用属性

属性 说明
Converter 转换器
ElementName 绑定的源对象
Fallback Value 绑定无法返回有效值时的默认显示
Mode 绑定方式
Path 路径
RelativeSource 常用于自身绑定或者数据模板中指定绑定的源对象
Source 源对象
StringFormat 格式化表达式
UpdateSourceTrigger 在绑定将发生的对象上设置事件
ValidationRules 验证规则

控制Binding数据流的方向-Mode

“一桥飞架南北,天堑变通途”,我们可以想象Binding这座桥梁上铺设了高速公路,我们不但可以控制公路是在源与目标之间双向同行还是某个方向的单行道,还可以控制对数据放行的事件,甚至可以在桥上假设一些“关卡”用来转换数据类型或者校验数据的正确性。

此图演示了不同类型的数据流:

img

  • 通过 OneWay 绑定,对源属性的更改会自动更新目标属性,但对目标属性的更改不会传播回源属性。 如果绑定的控件为隐式只读,则此类型的绑定适用。 例如,可能会绑定到股票行情自动收录器这样的源,也可能目标属性没有用于进行更改的控件接口(例如表的数据绑定背景色)。 如果无需监视目标属性的更改,则使用 OneWay 绑定模式可避免 TwoWay 绑定模式的系统开销
  • 通过 TwoWay 绑定,更改源属性或目标属性时会自动更新另一方。 此类型的绑定适用于可编辑窗体或其他完全交互式 UI 方案。 大多数属性默认为 OneWay 绑定,但某些依赖属性(通常为用户可编辑控件的属性,例如 TextBox.TextCheckBox.IsChecked)默认为 TwoWay 绑定。 用于确定依赖属性绑定在默认情况下是单向还是双向的编程方法是:使用 DependencyProperty.GetMetadata 获取属性元数据,然后检查 FrameworkPropertyMetadata.BindsTwoWayByDefault 属性的布尔值。
  • OneWayToSource 绑定与 OneWay 绑定相反;当目标属性更改时,它会更新源属性。 一个示例方案是只需要从 UI 重新计算源值的情况。
  • OneTime 该绑定会使源属性初始化目标属性,但不传播后续更改。 如果数据上下文发生更改,或者数据上下文中的对象发生更改,则更改不会在目标属性中反映。 如果适合使用当前状态的快照或数据实际为静态数据,则此类型的绑定适合。 如果你想使用源属性中的某个值来初始化目标属性,且提前不知道数据上下文,则此类型的绑定也有用。 此模式实质上是 OneWay 绑定的一种简化形式,它在源值不更改的情况下提供更好的性能。

若要检测源更改(适用于 OneWayTwoWay 绑定),则源必须实现合适的属性更改通知机制,例如 INotifyPropertyChanged。 请参阅如何:实现属性更改通知,获取 INotifyPropertyChanged 实现的示例。

触发源更新的因素-UpdateSourceTrigger

“一桥飞架南北,天堑变通途”,我们可以想象Binding这座桥梁上铺设了高速公路,我们不但可以控制公路是在源与目标之间双向同行还是某个方向的单行道,还可以控制对数据放行的事件,甚至可以在桥上假设一些“关卡”用来转换数据类型或者校验数据的正确性。

TwoWayOneWayToSource 绑定侦听目标属性中的更改,并将更改传播回源(称为更新源)。 例如,可以编辑文本框的文本以更改基础源值。

但是,在编辑文本时或完成文本编辑后控件失去焦点时,源值是否会更新? Binding.UpdateSourceTrigger 属性确定触发源更新的因素。 下图中右箭头的点说明了 Binding.UpdateSourceTrigger 属性的角色。

显示 UpdateSourceTrigger 属性的角色的图。

如果 UpdateSourceTrigger 值为 UpdateSourceTrigger.PropertyChanged,则目标属性更改后,TwoWayOneWayToSource 绑定的右箭头指向的值会立即更新。 但是,如果 UpdateSourceTrigger 值为 LostFocus,则仅当目标属性失去焦点时才会使用新值更新该值。

Mode 属性类似,不同的依赖属性具有不同的默认 UpdateSourceTrigger 值。 大多数依赖属性的默认值为 PropertyChanged,而 TextBox.Text 属性的默认值为 LostFocusPropertyChanged 表示源更新通常在每次目标属性更改时发生。 即时更改适用于 CheckBox 和其他简单控件。 但对于文本字段,每次击键后都进行更新会降低性能,用户也没有机会在提交新值之前使用 Backspace 键修改键入错误。

UpdateSourceTrigger的取值:

UpdateSourceTrigger值 源值何时进行更新
LostFocus(默认) 当Text控件拾取焦点时
PropertyChanged 当键入到TextBox时
Explicit 当应用程序调用UpdateSource时

Binding对数据的转换与校验

“一桥飞架南北,天堑变通途”,我们可以想象Binding这座桥梁上铺设了高速公路,我们不但可以控制公路是在源与目标之间双向同行还是某个方向的单行道,还可以控制对数据放行的事件,甚至可以在桥上假设一些“关卡”用来转换数据类型或者校验数据的正确性。

数据转换-Converter

创建绑定部分中,该按钮为红色,因为其 Background 属性绑定到值为“Red”的字符串属性。 此字符串值有效是因为 Brush 类型中存在类型转换器,可用于将字符串值转换为 Brush

将此信息添加到创建绑定部分的关系图中,使其如下所示。

显示数据绑定 Default 属性的图。

但是,如果绑定源对象拥有的不是字符串类型的属性,而是 Color 类型的 Color 属性,该怎么办? 在这种情况下,为了使绑定正常工作,首先需要将 Color 属性值转换为 Background 属性可接受的值。 需要通过实现 IValueConverter 接口来创建一个自定义转换器,如以下示例所示。

C#复制

[ValueConversion(typeof(Color), typeof(SolidColorBrush))]
public class ColorBrushConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        Color color = (Color)value;
        return new SolidColorBrush(color);
    }

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        return null;
    }
}

有关更多信息,请参见IValueConverter

现在,使用的是自定义转换器而不是默认转换,关系图如下所示。

显示数据绑定自定义转换器的图。

重申一下,由于要绑定到的类型中提供了类型转换器,因此可以使用默认转换。 此行为取决于目标中可用的类型转换器。 如果无法确定,请创建自己的转换器。

数据校验-ValidationRules

接受用户输入的大多数应用都需要具有验证逻辑,以确保用户输入了预期信息。 可基于类型、范围、格式或特定于应用的其他要求执行验证检查。WPF 数据绑定模型允许将 ValidationRulesBinding 对象关联。

ValidationRule 对象检查属性的值是否有效。 WPF 有两种类型的内置 ValidationRule 对象:

还可以通过从 ValidationRule 类派生并实现 Validate 方法来创建自己的验证规则

调试机制

可以在与绑定相关的对象上设置附加属性 TraceLevel,以接收有关特定绑定状态的信息。

示例

Binding基本示例

  1. 准备Binding Target
  2. 准备Binding Source
  3. 准备Binding
  4. 使用Binding连接数据源与Binding目标
// 摘要:
//     创建 System.Windows.Data.BindingExpressionBase 的新实例,并将其与指定的绑定目标属性关联。
//
// 参数:
//   target:
//     绑定的绑定目标。
//
//   dp:
//     绑定的目标属性。
//
//   binding:
//     描述绑定的 System.Windows.Data.BindingBase 对象。
//
// 返回结果:
//     为指定的属性创建并与之相关联的 System.Windows.Data.BindingExpressionBase 的实例。 System.Windows.Data.BindingExpressionBase
//     类是 System.Windows.Data.BindingExpression、System.Windows.Data.MultiBindingExpression
//     和 System.Windows.Data.PriorityBindingExpression 的基类。
//
// 异常:
//   T:System.ArgumentNullException:
//     binding 参数不能为 null。
public static BindingExpressionBase SetBinding(DependencyObject target, DependencyProperty dp, BindingBase binding);

01 准备Binding Target

<Grid>
    <StackPanel>
        <TextBox x:Name="textBoxname" BorderBrush="Black" Margin="5"/>
        <Button Content="Add Age" Margin="5" Click="Button_Click"/>
    </StackPanel>
</Grid>

02 准备Binding Source

03 准备Binding

04 使用Binding连接数据源与Binding目标

public partial class MainWindow : Window
    {
        Student stu;
        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);

            //UI元素的基类FrameworkElement对BindingOperations.SetBinding()方法进行了封装,同时借助Binding类的构造器及C#3.0对象初始化语法简化代码
            this.textBoxname.SetBinding(TextBox.TextProperty,new Binding("Name") { Source = stu = new Student()});
        }

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

class Student:INotifyPropertyChanged
    {       
        public event PropertyChangedEventHandler PropertyChanged;

        private string name;

        public string Name
        {
            get { return name; }
            set
            {
                name = value;
                //激发事件
                PropertyChanged ?.Invoke(this, new PropertyChangedEventArgs("Name"));
            }
        }

    }

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

示例:将TextBox与Slider进行Binding

<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类的构造器本身可以接收Path作为参数,所以也常写为:

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

控制Binding的方向与数据更新

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

在TextBox里输入一个恰当的值,然后按Tab键、让焦点离开TextBox,则Slider的手柄会跳到响应的值哪里。

为什么伊丁要TextBox失去焦点之后Slider的值才会改变呢?这就引出了Binding的两一个属性—UpdateSourceTrigger,它的类型是UpdateSourceTrigger枚举,可取值为PropertyChanged、LosetFocus、Explicit、Default。显然,对于TextBox默认值Default的行为与LostFocus一致,我们只需要把这个属性改为PropertyChanged,则Slider的手柄就会随着我们在TextBox里的输入而改变位置。

Binding的路径(Path)

Binding Path 就是你想要关注的Binding源中的指定属性,关注的属性=路径。

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

Binding多级路径

通俗地讲就是一路“点”下去,比如我们想让一个TextBox显示另外一个TextBox的文本长度:

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

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

索引器作为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#

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

默认元素作为Path

当使用一个集合或者DataView作为Binding源,用它们的默认元素当作Path使用:

List<string> stringList = new List<string>() { "张三","李四","王麻子"};
this.textBox1.SetBinding(TextBox.TextProperty,new Binding("/") { Source = stringList});
this.textBox2.SetBinding(TextBox.TextProperty,new Binding("/Length") { Source= stringList,Mode= BindingMode.OneWay});
this.textBox3.SetBinding(TextBox.TextProperty,new Binding("/[1]") { Source= stringList,Mode= BindingMode.OneWay});

集合元素的子集元素作为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; }
}


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

“没有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" FontSize="16" Margin="5"
                       Text="{Binding Source={StaticResource myString}}"/>
        </StackPanel>

等效的C#代码

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

Binding的源

  1. 把普通CLR类型单个对象指定为Source。
  2. 把普通CLR集合类型指定为Source。
  3. 把ADO.NET数据对象指定为Source。
  4. 使用XmlDataProvider把XML数据指定为Source。
  5. 把依赖对象(Dependency Object)指定为Source。
  6. 把容器的DataContext指定为Source(WPF Data Binding的默认行为)。
  7. 通过ElementName指定Source。
  8. 通过Binding的RelativeSource属性相对的指定Source。
  9. 把ObjectDataProvider对象指定为Source。
  10. 把使用LINQ检索得到的数据对象作为Binding的源。

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

所有WPF空间(包括容器控件)都具备DataContext属性,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; }
}
<StackPanel Background="LightBlue">
        <StackPanel.DataContext>
            <local:Student Id="6" Age="20" Name="张三"/>
        </StackPanel.DataContext>
        <Grid>
            <StackPanel>
                <TextBox Text="{Binding Path=Id}" Margin="5"/>
                <TextBox Text="{Binding Path=Age}" Margin="5"/>
                <TextBox Text="{Binding Path=Name}" Margin="5"/>
            </StackPanel>
        </Grid>
    </StackPanel>

在前面学习Binding路径的时候我们已经知道,当Binding的Source本身就是数据、不需要使用属性来暴露数据时,Binding的Path可以设置为“.”,亦可以省略不写。现在Source也可以省略不写了,这样,当某个DataContext是一个简单类型对象的时候,我们完全可能看到一个“既没有Path有没有Source的”Binding

<StackPanel Background="LightBlue">
        <StackPanel.DataContext>
            <sys:String>
                你好,吃了没!
            </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是一个“依赖属性”,依赖属性有一个很重要的特点就是当你没有为控件的某个依赖属性显示赋值时,控件会把自己容器的属性值“借过来”当作自己的属性值。实际上时属性值沿着UI元素树向下传递了。

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

  1. 当UI上的多个控件都使用Binding关注同一个对象是,不妨使用DataContext。

  2. 当作为Source的对象不能被直接访问的时候—比如B窗体内的控件想把A窗体内的控件当作自己的Binding源时,但A窗体内的控件时private访问级别,这时候就可以把这个而空间(或者控件的值)作为窗体A的DataContext(这个属性时public访问级别的)从而暴露数据。

    形象地说,这时候外层容器的DataContext就相当于一个数据的“制高点”,只要把数据放上去,别的元素就都能看见。另外DataContext本身也是一个依赖属性,我们可以使用Binding把它关联到一个数据源上。

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

XAML代码:

<StackPanel x:Name="stackPanel" Background="LightBlue">
        <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#代码:

public partial class MainWindow : Window
{
    public MainWindow()
        {
            InitializeComponent();

            //准备数据源
            List<Student> stuList = new List<Student>() 
            {
                new Student(){ Id = 0,Name = "Tim",Age = 29},
                new Student(){ Id = 1,Name = "Tam",Age = 21},
                new Student(){ Id = 2,Name = "Tbm",Age = 22},
                new Student(){ Id = 3,Name = "Tcm",Age = 23},
                new Student(){ Id = 4,Name = "Tdm",Age = 25},
                new Student(){ Id = 5,Name = "Tem",Age = 28},
                new Student(){ Id = 6,Name = "Tfm",Age = 23},
            };

            //为ListBox设置Binding
            this.listBoxStudents.ItemsSource = stuList;
            this.listBoxStudents.DisplayMemberPath = "Name";

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

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

显式的为数据设置DataTemplate:

<StackPanel x:Name="stackPanel" Background="LightBlue">
        <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">
            <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>

C#代码:

public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            //准备数据源
            List<Student> stuList = new List<Student>() 
            {
                new Student(){ Id = 0,Name = "Tim",Age = 29},
                new Student(){ Id = 1,Name = "Tam",Age = 21},
                new Student(){ Id = 2,Name = "Tbm",Age = 22},
                new Student(){ Id = 3,Name = "Tcm",Age = 23},
                new Student(){ Id = 4,Name = "Tdm",Age = 25},
                new Student(){ Id = 5,Name = "Tem",Age = 28},
                new Student(){ Id = 6,Name = "Tfm",Age = 23},
            };

            //为ListBox设置Binding
            this.listBoxStudents.ItemsSource = stuList;
            

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

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

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

XAML代码:

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

C#代码:

private void Button_Click(object sender, RoutedEventArgs e)
        {
            string connStr = ConfigurationManager.ConnectionStrings["connStr"].ConnectionString;
            try
            {
                using (SqlConnection conn = new SqlConnection(connStr))
                {
                    conn.Open();
                    MessageBox.Show("数据库连接成功");
                    string sql = "SELECT Name FROM [dbo].tb_Student";
                    SqlDataAdapter sda = new SqlDataAdapter(sql,conn);
                    DataSet ds = new DataSet();
                    sda.Fill(ds);
                    //获取DataTable实例
                    DataTable dt = ds.Tables[0];
                    this.listBoxStudents.DisplayMemberPath = "Name";
                    this.listBoxStudents.ItemsSource = dt.DefaultView;
                }
            }
            catch (Exception ex)
            {

                MessageBox.Show("数据库连接失败"+ex.Message);
            }
        }

多数情况下我们会选择ListView控件来显示DataTable:

XAML代码:

<StackPanel Background="LightBlue">
        <ListView x:Name="listViewStudents" Height="120" Margin="5">
            <ListView.View>
                <GridView>
                    <GridViewColumn Header="Id" Width="60" DisplayMemberBinding="{Binding id}"/>
                    <GridViewColumn Header="姓名" Width="80" DisplayMemberBinding="{Binding Name}"/>
                    <GridViewColumn Header="年龄" Width="60" DisplayMemberBinding="{Binding Age}"/>
                </GridView>
            </ListView.View>
        </ListView>
        <Button Content="加载" Height="25" Margin="5" Click="Button_Click"/>
    </StackPanel>

C#代码:

this.listViewStudents.ItemsSource = dt.DefaultView;

直接用DataTable作为源,把DataTable对象放在一个对象的DataContext属性里,并把ItemsSource与一个既没有指定Source有没有指定Path的Binding关联起来时,Binding可以自动找到它的DefaultView并当作自己的Source来使用:

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

使用XML数据作为Binding的源

当使用XML数据作为Binding的Source时我们将使用XPath属性而不是Path属性来指定数据的来源。

ListView显示XML数据

XAML代码:

<StackPanel Background="LightBlue">
        <ListView x:Name="listViewStudents" Height="130" Margin="5">
            <ListView.View>
                <GridView>
                    <GridViewColumn Header="Id" Width="80" DisplayMemberBinding="{Binding XPath=@Id}"/>
                    <GridViewColumn Header="姓名" Width="120" DisplayMemberBinding="{Binding XPath=Name}"/>
                </GridView>
            </ListView.View>
        </ListView>
        <Button Content="加载" Click="Button_Click" Height="25" Margin="5"/>
    </StackPanel>

C#代码:

private void Button_Click(object sender, RoutedEventArgs e)
{
    XmlDocument doc = new XmlDocument();
    doc.Load(@"G:\VsProject\Binding基本示例\使用XAML数据作为Binding的源\RawData.xml");

    XmlDataProvider xdp = new XmlDataProvider();
    xdp.Document = doc;
    //使用XPath选择需要暴露的数据
    //现在时需要暴露一组Student
    xdp.XPath = @"/StudentList/Student";

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

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

XmlDataProvider xdp = new XmlDataProvider();
xdp.Source = new Uri(@"G:\VsProject\Binding基本示例\使用XAML数据作为Binding的源\RawData.xml");
xdp.XPath = @"/StudentList/Student";
this.listViewStudents.DataContext = xdp;
this.listViewStudents.SetBinding(ListView.ItemsSourceProperty, new Binding());

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

TreeView显示XML数据

<Window.Resources>
        <XmlDataProvider x:Key="xdp" XPath="FileSystem/Folder">
            <x:XData>
                <FileSystem xmlns="">
                    <Folder Name="Books">
                        <Folder Name ="Programming">
                            <Folder Name ="WPF"/>
                            <Folder Name ="MFC"/>
                            <Folder Name ="Delphi"/>
                        </Folder>
                    </Folder>
                    <Folder Name ="Tooles">
                        <Folder Name ="Development"/>
                        <Folder Name ="Designment"/>
                        <Folder Name ="Players"/>
                    </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>

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

XAML代码:

<StackPanel Background="LightBlue">
        <ListView x:Name="listViewStudents" Height="143" Margin="5">
            <ListView.View>
                <GridView>
                    <GridViewColumn Header="Id" Width="60" DisplayMemberBinding="{Binding Id}"/>
                    <GridViewColumn Header="姓名" Width="100" DisplayMemberBinding="{Binding Name}"/>
                    <GridViewColumn Header="年龄" Width="80" DisplayMemberBinding="{Binding Age}"/>
                </GridView>
            </ListView.View>
        </ListView>
        <Button Content="加载" Height="25" Margin="5" Click="Button_Click"/>
    </StackPanel>

C#代码:

public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            List<Student> stuList = new List<Student>()
            {
                new Student(){Id=0,Name="Tim",Age=14},
                new Student(){Id=1,Name="Tom",Age=15},
                new Student(){Id=2,Name="Kyle",Age=18},
                new Student(){Id=3,Name="Tony",Age=20},
                new Student(){Id=4,Name="Vina",Age=29},
                new Student(){Id=5,Name="Mike",Age=24},
            };

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

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

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

DataTable dt = this.GetDataTable();

this.listViewStudents.ItemsSource =
    from row in dt.Rows.Cast<DataRow>()
    where Convert.ToString(row["Name"]).StartsWith("T")
    select new Student()
    {
        Id = int.Parse(row["Id"].ToString()),
        Name = row["Name"].ToString(),
        Age = int.Parse(row["Age"].ToString())
    };

如果数据存储在XML文件里(D:RawData.xml)如下:

<?xml version="1.0" encoding="utf-8" ?> 
<StudentList>
  <Class>
    <Student Id="0" Name="Tim" Age="29"/>
    <Student Id="1" Name="Tom" Age="29"/>
    <Student Id="2" Name="mem" Age="29"/>
  </Class>
</StudentList>

则代码会是这样:

XDocument xdoc = XDocument.Load(@"D:\RawData.xml");

this.listViewStudents.ItemsSource =
    from element in xdoc.Descendants("Student")
    where element.Attribute("Name").Value.StartsWith("T")
    select new Student()
    {
        Id = int.Parse(element.Attribute("Id").Value),
        Name = element.Attribute("Name").Value,
        Age = int.Parse(element.Attribute("Age").Value)
    };

使用ObjectDataProvider对象作为Binding的Source

XAML代码:

<Grid>
    <Button Content="点击" Click="Button_Click"/>
</Grid>

C#代码:

public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

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

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 "输入错误!";
        }
    }

示例:

<StackPanel Background="LightBlue">
    <TextBox x:Name="textBoxArg1" Margin="5"/>
    <TextBox x:Name="textBoxArg2" Margin="5"/>
    <TextBox x:Name="textBoxArg3" Margin="5"/>
</StackPanel>
public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            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元素上
            this.textBoxArg1.SetBinding(TextBox.TextProperty, bindingToArg1);
            this.textBoxArg2.SetBinding(TextBox.TextProperty, bindingToArg2);
            this.textBoxArg3.SetBinding(TextBox.TextProperty, bindingToResult);
        }


    }

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 "输入错误!";
        }
    }

使用Binding的RelativeSource

XAML代码:

<Grid x:Name="g1" Background="Red" Margin="20">
        <DockPanel x:Name="d1" Background="Orange" Margin="20">
            <Grid x:Name="g2" Background="Yellow" Margin="20">
                <DockPanel x:Name="d2" Background="LawnGreen" Margin="20">
                    <TextBlock x:Name="textBox1" FontSize="24" Margin="20"/>
                </DockPanel>
            </Grid>
        </DockPanel>
    </Grid>

C#代码:

public MainWindow()
{
    InitializeComponent();

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

或者在XAML中插入等效代码:

<TextBox x:Name="textBox1" FontSize="24" Margin="20"
         Text="{Binding RelativeSource={RelativeSource FindAncestor,AncestorType={x:Type Grid},AncestorLevel=1},Path=Name}"/>

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

Binding对数据的转换和校验

Binding的数据校验

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

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

XAML代码:

<StackPanel>
    <TextBox x:Name="textBox1" Margin="5"/>
    <Slider x:Name="slider1" Minimum="0" Maximum="100" Margin="5"/>
</StackPanel>

C#代码:

public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            Binding binding = new Binding("Value") { Source=this.slider1};
            binding.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
            RangeValidationRule rvr = new RangeValidationRule();
            binding.ValidationRules.Add(rvr);
            this.textBox1.SetBinding(TextBox.TextProperty,binding);
        }
    }

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>=0 && d<=100)
                {
                    return new ValidationResult(true, null);
                }
            }
            return new ValidationResult(false, "Validation Failed");
        }
    }

Binding只在Target被外部方法更新时校验数据,而来自Binding的Source数据更新Targer时是不会进行校验的。如果想改变这种行为,或者说当来自Source的数据也有可能出问题时,我们就需要将校验条件的ValidationOnTargerUpdated属性设为true。

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

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

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

Binding binding = new Binding("Value") { Source=this.slider1};
binding.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
RangeValidationRule rvr = new RangeValidationRule();
rvr.ValidatesOnTargetUpdated = true;
binding.ValidationRules.Add(rvr);
this.textBox1.SetBinding(TextBox.TextProperty,binding);

当校验错误的时候Validate方法返回的ValidationResult对象携带着一条错误信息,使用路由事件即可显示出来。首先,在创建Binding时要把Binding对象的NotifyOnValidationError属性设为true,这样,当数据校验失败的时候Binding会发出一个新号,这个信号会以Binding对象的Target为起点在UI元素树上以路由的形式传播,遇到由侦听器的就会触发处理这个新号。

XAML代码如下:

<StackPanel>
    <TextBox x:Name="textBox1" Margin="5" Validation.Error="ValidationError"/>
    <Slider x:Name="slider1" Minimum="-10" Maximum="110" Margin="5"/>
</StackPanel>

建立Binding的代码如下:

Binding binding = new Binding("Value") { Source=this.slider1};
binding.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
RangeValidationRule rvr = new RangeValidationRule();
rvr.ValidatesOnTargetUpdated = true;
binding.ValidationRules.Add(rvr);
binding.NotifyOnValidationError = true;
this.textBox1.SetBinding(TextBox.TextProperty,binding);

this.textBox1.AddHandler(Validation.ErrorEvent,new RoutedEventHandler(this.ValidationError));

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

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

Binding的数据转换

手动写Converter,方法是创建一个类并让这个类实现IValueConverter接口。IValueConverter接口定义如下:

//
    // 摘要:
    //     提供将自定义逻辑应用于绑定的方法。
    public interface IValueConverter
{
    //
        // 摘要:
        //     转换值。
        //
        // 参数:
        //   value:
        //     绑定源生成的值。
        //
        //   targetType:
        //     绑定目标属性的类型。
        //
        //   parameter:
        //     要使用的转换器参数。
        //
        //   culture:
        //     要用在转换器中的区域性。
        //
        // 返回结果:
        //     转换后的值。 如果该方法返回 null,则使用有效的 null 值。
        object Convert(object value, Type targetType, object parameter, CultureInfo culture);
    //
        // 摘要:
        //     转换值。
        //
        // 参数:
        //   value:
        //     绑定目标生成的值。
        //
        //   targetType:
        //     要转换为的类型。
        //
        //   parameter:
        //     要使用的转换器参数。
        //
        //   culture:
        //     要用在转换器中的区域性。
        //
        // 返回结果:
        //     转换后的值。 如果该方法返回 null,则使用有效的 null 值。
        object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture);
}

当数据从Binding的Source流向Target时,Convert方法将被调用;反之,ConvertBack方法将被调用。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; }
}

飞机的State属性在UI里被映射为CheckBox。因为存在以上两种映射关系,我们需要提供两个Converter;一个是由Category类型单向转换为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 @"G:\VsProject\Binding基本示例\数据转换\Img\Bomber.jpg";
            case Category.Fighter:
                return @"G:\VsProject\Binding基本示例\数据转换\Img\Fighter.jpg";
            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的ListBox控件是我们工作的重点,需要为它添加用于显示数据的DataTemplate。我们把焦点集中在ListBox控件的ItemTemplate属性上:

<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="20" Height="20" 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" Click="buttonLoad_Click"/>
        <Button x:Name="buttonSave" Content="Save" Height="25" Margin="5" Click="buttonSave_Click"/>
    </StackPanel>

Load按钮的Click事件处理器负责把一组飞机数据赋值给ListBox的ItemsSource属性,Save按钮的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-25",State=State.Unknown},
        new Plane(){Category=Category.Fighter,Name="J-20",State=State.Unknown},
    };
    this.listBoxPlane.ItemsSource = planeList;
}

private void buttonSave_Click(object sender, RoutedEventArgs e)
{
    StringBuilder sb = new StringBuilder();
    foreach (Plane p in listBoxPlane.Items)
    {
        sb.AppendLine(string.Format($"Category={p.Category},Name={p.Name},State={p.State}"));
    }
    //File.WriteAllText(@"D:\PlaneList",sb.ToString());
}

MultiBinding(多路Binding)

凡是能使用Binding对象的场合都能使用MultiBinding。MultiBinding具有一个名为Bindings的属性,其类型是Collection<BindingBase>,通过这个属性MultiBinding把一组Binding对象聚合起来,处在这个集合中的Binding对象可以拥有自己的数据校验与转换机制,它们汇集起来的数据将共同决定传往MultiBinding目标的数据。

考虑这样一个需求,有一个用于新用户注册的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"/>
        <TextBox x:Name="textBox3" Height="23" Margin="5"/>
        <TextBox x:Name="textBox4" Height="23" Margin="5"/>
        <Button x:Name="button1" Content="提交" Margin="5"/>
    </StackPanel>

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

public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            this.SetMultiBinding();
        }

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

            //准备MulltiBinding
            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对象关联
            this.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].Equals(values[1])
                && values[2].Equals(values[3]))
            {
                return true;
            }
            return false;
        }

        //不会被调用
        public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
posted @ 2020-09-10 19:56  AJun816  阅读(729)  评论(0编辑  收藏  举报