[四] Binding
所有内容均出自于《深入浅出WPF》一书,其作者在b站也有相关视频,本文为个人读后总结,仅供参考
WPF 最重要的部分,灵魂所在,当连接一端的数据发生变化时,他会自动的在另一端刷新数据,它的存在相当于 web 中的前后端分离,至此,设计师只管UI,开发者只管业务逻辑,binding 成为它们中的桥梁,一端连接 UI,一端连接业务,重要的是,这都是自动的,这个桥梁不需要工程师去维护和设计,由此开始数据驱动UI
Bing的基础使用
binding: 翻译为 绑定,关键为 绑 ,原意中还包含 关联 的意思
binding是桥梁,两端分别是 源(source 从哪里来) ,目标(target 到哪里去),一般情况下,binding负责把逻辑层的数据运输到 UI 层,即数据驱动 UI
演示:
1.首先准备一个 Stu 类,这个类的 实例 将作为数据源,同时由于 binding 是一种自动机制,可以自动把变化传递给 UI,但是需要数据在变化是通知它一声,所以这里的类需要继承 INotifyPropertyChanged 通知类,当数据变化时,变化的属性 将会发出 PropertyChanged 事件,binding 会监听这个事件。
class Stu : 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"));
}
}
}
}
建立前端页面,后台建立binding 对象,然后把binding 绑到UI上
// UI
<StackPanel>
<TextBox x:Name="tb" Margin="5"/>
<Button Content="添加" Click="Button_Click" Margin="5" />
</StackPanel>
//后台代码
Stu stu = new Stu();
public MainWindow()
{
InitializeComponent();
Binding binding = new Binding(); //创建 binding 对象
binding.Source = stu; //为 binding 添加数据源
binding.Path = new PropertyPath("Name"); //Path 是指我要绑定数据源中的哪一个属性,他的类型是 PropertyPath ,所以我们也new 一个 // PropertyPath,并指向 Name 属性
//通过 BindingOperations 把 binding 设置到Textbox上 ,这里要注意的是,binding 虽然是桥梁,但它不是一边连接数据源,一边连接目标,而是binding 连接数据源,然后把 binding 添加到目标上
BindingOperations.SetBinding(this.tb,TextBox.TextProperty,binding);
}
private void Button_Click(object sender, RoutedEventArgs e)
{
stu.Name += "hello"; //点击按钮,数据变化,通知 binding, 传递到 UI,UI变化
}
效果:
补充:
把数据源和目标连接到一起使用了 BindingOperations.SetBinding(),现在解析一下
- 第一个参数:指定 binding 的目标 ,这里就指定了 tb (textbox)
- 第二个参数:指定数据要运输到目标的那个属性上,与binding 的 Path 很像, 这里接受一个 DependencyProperty 对象(依赖对象),他是一个比较特殊的对象,专门用来和 binding 联动,这一类属性就可以被数据驱动, UI 的控件基本都实现了这个属性,也可以自定义依赖属性
- 第三个参数:指定那个 binding 实例将数据源和目标联系起来
Binding 的源(source)
指定 binding 的源包括指定 source 和 path
binding 数据源的要求:
- Object 对象
- 属性公开 (可以被访问到)
控件作为 binding的源 + binding 扩展标记的用法
<StackPanel>
<TextBox Text="{Binding ElementName=slider1, Path=Value}" Margin="10"/>
<Slider x:Name="slider1" Maximum="100" Margin="10"/>
</StackPanel>
控件作为 binding的源 需要使用 ElementName 来指定源
控制 binding 的方向及数据更新
控制数据流动方向: mode 属性,该类型是 Binding 枚举类
**数据更新:**在默认情况下,只有文本框失去焦点,数据才会更新,控制数据更新的的是 UpdateSourceTrigger 属性,它的类型是 UpdateSourceTrigger 枚举类,当把值改为 PropertyChanged 时,就可以实现实时变化了
演示:
这里将TextBox 的 Text 属性和 Slider 的 Value 属性绑定,并且设置 mode 为双向,UpdateSourceTrigger=PropertyChanged 实时更新,当我们滑动滑块时,文本框中的数值就会发生变化,并且一旦在文本框中修改值,立即就能同步到 salider 上
<TextBox Text="{Binding ElementName=slider1,Path=Value,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}" Margin="5"/>
<Slider x:Name="slider1" Maximum="100" Margin="5"/>
Binding 的路径
Path 用于指定我们需要绑定数据源的哪个属性
Path 的类型是
1.Path 的不同写法:
//xmal:
<TextBox Text="{Binding ElementName=slider1,Path=Value}"/>
//c#
Binding binding = new Binding() { Path = new PropertyPath("Value"), Source = slider1 };
this.textBox.SetBinding(TextBox.TextProperty, binding);
//c# 简写 Binding 有很多重载,拥有直接接受path参数的构造器
Binding binding = new Binding("Value") { Source = slider1 };
this.textBox.SetBinding(TextBox.TextProperty, binding);
// 继续简化
this.textBox.SetBinding(TextBox.TextProperty, new Binding("Value") { Source = slider1 });
2.Path 支持多级路径
// 通过 . 号可以像后台代码一样逐级往下点
<TextBox x:Name="textBox" Text="{Binding ElementName=tb1,Path=Text.Length,Mode=OneWay}"/>
<TextBox x:Name="tb1"/>
//等效 c# 代码
Binding binding = new Binding("Text.Length") { Source = tb1 };
this.textBox.SetBinding(TextBox.TextProperty, binding);
//集合类型的索引器又称为带参属性,只要是属性就可以作为 Path
//xaml:
<TextBox x:Name="textBox" Text="{Binding ElementName=tb1,Path=Text.[2],Mode=OneWay}"/>
<TextBox x:Name="tb1"/>
//c#
Binding binding = new Binding("Text.[2]") { Source = tb1 };
this.textBox.SetBinding(TextBox.TextProperty, binding);
//简写 后台同理
<TextBox x:Name="textBox" Text="{Binding ElementName=tb1,Path=Text[2],Mode=OneWay}"/>
<TextBox x:Name="tb1"/>
3.Path 支持集合
Path 支持多级路径,那么同样扩展一下,集合也可以使用,多级集合也可以
简单使用:
//xaml:
<StackPanel>
<TextBox x:Name="tb1" Margin="5"/>
<TextBox x:Name="tb2" Margin="5"/>
<TextBox x:Name="tb3" Margin="5"/>
</StackPanel>
//c#
List<string> list = new List<string>() { "Hello", "Boy" };
this.tb1.SetBinding(TextBox.TextProperty, new Binding("/") { Source = list });
this.tb2.SetBinding(TextBox.TextProperty, new Binding("/Length") { Source = list, Mode = BindingMode.OneWay });
this.tb3.SetBinding(TextBox.TextProperty, new Binding("/[2]") { Source = list, Mode = BindingMode.OneWay });
效果:
进阶 – 集合的集合:
public MainWindow()
{
InitializeComponent();
County county = new County() {
Name = "中国",
citys = new List<City>() { new City() { Name = "西安" } , new City() { Name = "汉中" }} };
List<County> list = new List<County>() { county};
this.tb1.SetBinding(TextBox.TextProperty, new Binding("/Name") { Source = list });
this.tb2.SetBinding(TextBox.TextProperty, new Binding("/citys.Name") { Source = list});
}
}
class County {
public string Name { get; set; }
public List<City> citys { get; set; }
}
class City {
public string Name { get; set; }
}
可以省略 Path 的情况
在数据源本身就是数据的情况下,就可以省略 path 比如 string 字符串示例本身就是数据,在zaml中,此时 path 可以用 . 代替。也可以直接不写,但是在 后台代码中,path 就只能使用 . 。
<Window.Resources>
<c:String x:Key="str">Hello</c:String>
</Window.Resources>
<StackPanel>
<TextBlock Text="{Binding Path=.,Source={StaticResource ResourceKey=str}}"></TextBlock>
</StackPanel>
为 binding 指定源的几种方式
上面是如何通过 path 寻找属性,现在我们关注如何指定数据 source
binding 的源是数据的来源,只要一个对象包含数据并且能通过属性把数据暴漏出来,就可以作为 binding 的源,这里不过多解释,纯做记录,加粗表示个人用的较多
- 普通的单个对象:包括 .net自带类型的的对象和自定义对象,如果类型实现了 INotifyPropertyChange 对象,就可以激发事件通知 bInding 更新
- 集合对象:包括 数组 List 等,经常把此类数据作为 ItemControl 派生类的数据源使用,一般是赋给ItemSource
- ADO.Net数据对象:包括DataTable 和 DataView 等
- XmlDataProvider 把XML 数据指定为数据源:XML 使用广泛,尤其在级联式的控件中就可以把 XML作为源
- 使用 Linq 检索结果作为源:Linq 类似于lamda表达式,在指定源时可以使用linq 来简化操作
- 把依赖对象指定为 Source :前面有讲到,依赖属性是比较特殊的属性,专门用来和Binding联动,既可以作为目标,也可以作为源
- 把容器的 DateContext 作为源:在没有指定源时, binding 会沿着控件树向上寻找Source
- 通过 ElementName 指定 Source:在c#代码中可以直接指定对象作为源,但是 zaml 中无法访问对象,可以使用 ElementName 指定对象的名字
- 通过 Binding 的 RelativeSource :把自己或者自己的属性指定为自己的源
- 把 ObjectDataProvider 对象指定为源:特殊,把方法的返回值作为源,使用时需要创建ObjectDataProvider 对象,和方法所属类对象,把这个类传给ObjectDataProvider 并设定参数,就可以使用ObjectDataProvider 对象了
binding 没有 source 的情况
前面说过,WPF 的控件是一棵树,**每一个节点都有一个 DateContext 的属性,**当控件没有指定 source 时,他就会寻找沿着这棵树一直往上找 DateContext 的属性,看里面有没有对应的属性,找到就返回,没有找到就一直延续,直到树根还没找到,那就没有数据
<Grid>
<StackPanel >
<StackPanel.DataContext>
<local:Stu Name="Hello" Age="23"></local:Stu>
</StackPanel.DataContext>
<TextBox Text="{Binding Path=Name}" Margin="5"> </TextBox>
<TextBox Text="{Binding Path=Age}" Margin="5"></TextBox>
</StackPanel>
</Grid>
- Binding 本身也有 path为参数的构造函数,所以在只有一个 path 时可以省略
<Grid>
<StackPanel >
<StackPanel.DataContext>
<local:Stu Name="Hello" Age="23"></local:Stu>
</StackPanel.DataContext>
<TextBox Text="{Binding Name}" Margin="5"> </TextBox>
<TextBox Text="{Binding Age}" Margin="5"></TextBox>
</StackPanel>
</Grid>
- path 本身就是数据,不需要通过属性暴漏数据时**,可以用 . ,也可以省略**
<Grid>
<StackPanel >
<StackPanel.DataContext>
<c:String >Hello</c:String>
</StackPanel.DataContext>
<TextBox Text="{Binding}" Margin="5"> </TextBox>
<TextBox Text="{Binding}" Margin="5"></TextBox>
</StackPanel>
</Grid>
现在来细看一下指定源的几种方式
1.集合对象
- WPF 列表式控件派生自 ItemControl 类,继承了 ItemSource 的这个属性,**而 ItemSource 属性可以接受一个 IEnumerable 类型的值 **(可以接受集合的原因)
- 并且 ItemControl 的派生类都具有他们的的条目容器,比如 LIstBox 的条目容器是 ListBoxItem
演示:
//zaml:
<StackPanel Background="AliceBlue">
<TextBox x:Name="tb1" Margin="5" />
<ListBox x:Name="lb1" Margin="5"/>
</StackPanel>
//c#
InitializeComponent();
List<Stu> stus = new List<Stu>()
{
new Stu(){ ID = 1, Name = "Tim"},
new Stu(){ ID = 2, Name = "LIk"},
};
this.lb1.ItemsSource = stus;
this.lb1.DisplayMemberPath = "Name";
Binding binding = new Binding("SelectedItem.ID") { Source = this.lb1 };
this.tb1.SetBinding(TextBox.TextProperty, binding);
//实体类
class Stu
{
public int ID { get; set; }
public String Name { get; set; }
}
给ItemsSource指定集合数据源,路径设置为 “Name”, TextBox 绑定ListBox 的 SelectedItem.ID,显示被选中的item的对象(Stu)的 ID
在 ItemsSource 中没看见 Binding 但是却实现了 binding 的效果,事实上,当DisplayMemberPath被赋值时,就会自动创建 Binding 并以DisplayMemberPath的值作为 path, 创建的 Binding 数据源就是 List中的每一个对象,path 就是DisplayMemberPath的值,目标就是 每一个 ListBoxItem(容器),默认情况下 ListBoxItem 的内部就是一个 TextBox,所以 Binding 实际绑定的是ListBoxItem下的TextBox
事实上大概就是下面这个样子,效果和 指定DisplayMemberPath后一样
<ListBox x:Name="lb1" Margin="5">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
在使用集合类型的控件时,一般会使用 ObservableCollection 替代List ,因为 ObservableCollection 自带通知
2. XmlDataProvider 把XML 数据指定为数据源
.net 提供了Dom(Document Object Mode 文档对象类型) 和 LINQ (Language-Integreted Query 语言集成查询)两种类库操作xml
这里以dom 为准
Binding 数据转换和校验 - MultiBinding(多路 Binding)
无
</DataTemplate>
</ListBox.ItemTemplate>
```
在使用集合类型的控件时,一般会使用 ObservableCollection 替代List ,因为 ObservableCollection 自带通知
2. XmlDataProvider 把XML 数据指定为数据源
.net 提供了Dom(Document Object Mode 文档对象类型) 和 LINQ (Language-Integreted Query 语言集成查询)两种类库操作xml
这里以dom 为准
Binding 数据转换和校验 - MultiBinding(多路 Binding)
无
MultiBinding(多路 Binding) 和单 binding 并无区别,就是把多个 binding 集中在一起,统一操作,比如为 MultiBinding 设置一个校验器,他就可以应用到所有添加的 单个 binding 上