用最少的文字来解释清楚每一行代码,加油 !
程序 = 数据 + 算法, 数据一直处于核心地位
但传统的程序设计都是UI驱动程序
,这反而使数据成为被动,如何使数据反变之为 主动 呢?
WPF 的核心理念就是数据驱动UI
, 要想让数据成功真正的核心,必须使用 Binding 机制。
下面的那些超链接没什么用,只是好看而已 ^ - ^
【文章目录】
要想进行下面的学习,我们最好理清一个基本的概念
什么是源 ? 什么是目标 ? 什么是路径?
我们把 Binding 比作数据的桥梁,那么它的两端分别是 Binding 的源(Source)
和目标(Target)
。
数据从哪里来哪里就是源,Binding是架在中间的桥梁,Binding目标是数据要往哪儿去,路径是数据源中需要绑定的属性是谁
一般情况下,Binding源是逻辑层的对象,Binding 目标是UI层的控件对象,这样我们就完成了数据驱动UI
的过程。
我将通过下面的一些案例来详解 Binding的那些事 …
【1. Binding 入门基础】
有了这些基本的概念之后,我们尝试写一个简单的小例子,来感受一下 Binding的神奇之处
- 首先,我们创建一个名为 Student的类,这个类的实例作为 数据源 的使用:
class Student
{
private string name; // 姓名
public string Name
{
get {return name;}
set {name = value;}
}
}
- 链接接口:
但是,当我们将这个 name的值改变时,UI元素怎么才能知道我们改变了呢?并且作出相应的变化 ?我们要做的是让作为:
数据源的类实现 System.ComponentModel名称空间中的INotifyPropertyChanged 接口
当为Bidning 设置了数据源后, Binding 就会自动侦听来自这个接口的 PropertyChanged事件。
我们将 Student类升级过后是这样的:
class Student : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged; // 侦听的事件
private string name;
public string Name
{
get{return name;}
set
{
name = value;
// 激发事件
if(this.PropertyChanged != null)
{
// 告诉它名为 Name的属性发生了改变
this.PropertyChanged.Invoke(this, new PropertyChangedEventArgs("Name"));
}
}
}
}
- 我们数据源部分已经写好,接下来我们准备两个 UI元素,TextBox 和 Button:
<StackPanel>
<TextBox x:Name="textBoxName" BorderBrush="Black" Margin="5" />
<Button Content="Add Age" Margin="5" Click="Button_Click" />
</StackPanel>
界面大小自行设置,结果图如下:
- 使用 Binding 把数据源和 UI元素连接起来:
连接之前,我们首先在 MainWindow类里写出:
Student student;
为什么不在构造器里面写呢 ? 为了我们使用Button按钮的点击事件时,方便访问
在 MainWindow 构造器中,写出如下的两行代码:
student = new Student() { Name = "huameng" } // 实例化对象
// 第一个参数是 TextBox的依赖属性绑定到文本
// 第二个实例化 Binding对象,参数为需要绑定的属性(路径), 设置数据源
this.textBoxName.SetBinding(TextBox.TextProperty, new Binding("Name")
{ Source = student });
- 为Button 元素实现点击事件(修改数据源的属性):
private void Button_Click(object sender, RoutedEventArgs e)
{
student.Name += "nb";
}
- 效果动画图如下:
【2. Binding 源与路径】
“源” 亦或是万物的源头, Binding的源也就是数据的源头。Binding对源的要求并不苛刻 ——
只要它是一个对象,并且通过属性(Property)公开自己的数据,它就能作为 Binding的源。
2.1 —— 把控件作为 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>
与之等价的 TextBox的Binding, C#代码是:
this.testBox1.SetBinding(TextBox.TextProperty, new Binding("Value")
{ElementName="slider1"};
Binding类的构造器是可以接收 Path作为参数,所以XAML代码也可以这样写:
<TextBox x:Name="textBox1" Text="{Binding Value, ElementName=slider1}"
BorderBrush="Black" Margin="5" />
实现效果图如下:
绑定效果已经完成,但是我在最后尝试了改变 TextBox的Text,而 Slider的Value 没有立即显示出来,这是为什么呢? 答案在下一小节中揭晓 …
2.2 —— 控制 Binding 方向及数据更新
2.2.1 控制 Binding 方向
很多的 UI元素是只读的,也就是 Binding中的源向目标的单向沟通 ,比如 TextBlock、Label等等
也有些 UI元素是可读可写的,也就是可以支持 Binding中的目标向源的沟通,比如上一小节的 TextBox
控制 Binding 数据流向的属性是 Mode,它的类型是 BindingMode枚举。
BindingMode的取值为:
- TwoWay
- OneWay
- OnTime
- OneWayToSource
- Default (根据目标的实际情况确定,比如:**TextBox 双向、TextBlock 单向**
比如我们将上一小节的TextBox代码增加一个单向模式,这样我们就不能通过修改TextBox的Text来改变 Slider的Value :
2.2.2 数据更新
这里,我们回想上一小节中的最后部分,修改TextBox的Text属性,Slider的Value属性没有立即更改,而是要把 索引光标(焦点) 移到别的地方才会修改属性值,这是为什么呢 ?
这就引出了 Binding的另一个属性 —— UpdateSourceTrigger,它的类型是 UpdateSourceTrigger 枚举,取值为:
- PropertyChanged
- LostFocus
- Explicit
- Default (与LostFocus一致,失去焦点后,改变)
我们只需要将这个属性改为 PropertyChanged,Slider 的Value就会随着 TestBox的 Text改变而改变:
实现效果图 :
2.3 —— “没有 Path” 的 Binding
当 Binding源本身就是数据的时候,是没有属性的,比如:string、int 等。
这时,我们只需要将 Path的值设置为 “ . " ,而在XAML中 “ . "又可以省略不写,但是在C#代码中,是不能省略的,看下面的例子:
<StackPanel>
<StackPanel.Resources>
<sys:String x:Key="str">Hello World!</sys:String>
</StackPanel.Resources>
<TextBlock x:Name="textBlock1" TextWrapping="Wrap" FontSize="20"
Text="{Binding Path=., Source={StaticResource str}}"/>
</StackPanel>
其中,引用资源对象 str 作为 TextBlock的Text 的绑定对象,” Path=. " 是可以省略不写的:
Text="{Binding Source={StaticResource str}}"
与之对应的 C#代码为:
string str = "Hello World!";
this.textBlock1.SetBinding(TextBlock.TextProperty, new Binding(".") { Source = str });
效果如图:
2.4 —— 没有 Source, 使用 DataContext
作为Binding的源
DataContext 属性被定义在 FrameworkElement 类里,这个类是WPF控件的基类,意味着所有控件都有这个属性。
WPF的 UI布局是树形结构,树上的每个结点都是一个控件,所以 ——
在 UI元素树的每个结点都有 DataContext
。
当 Binding只有 Path 而没有 Source时,它会一直向着树的根部,遇到一个控件则查看一下 DataContext。如果一直没有找到,则上下文中不存在该属性。(这种说法并不完全正确,下面会讲到 …)
让我们看一下下面的例子,来感受一下 DataContext:
- 创建一个Student类,具有三个属性:
public class Student
{
public int Id { get; set; }
public string Name { get; set; }
public int Age { get; set; }
}
- 创建程序的 UI:
<StackPanel Background="LightBlue">
<StackPanel.DataContext>
<local:Student Id="1" Age="20" Name="huameng"/>
</StackPanel.DataContext>
<Grid>
<StackPanel>
<TextBox Text="{Binding Id}" Margin="5"/>
<TextBox Text="{Binding Name}" Margin="5"/>
<TextBox Text="{Binding Age}" Margin="5"/>
</StackPanel>
</Grid>
</StackPanel>
其中,我们为StackPanel 的DataContext进行赋值,它是 Student对象,并为三个TextBox进行了 Binding,它们会自动去寻找可用的 DataContext 对象 。
- 效果图:
我们前面说过这样一句话:
我来解释一下这里的问题,其实,这只是WPF给我们的一个错觉,Binding并没有那么智能。
之所以可以有这样的效果是因为DataContext是一个 “依赖属性”
,当你没有为某个依赖属性赋值时,控件会把自己容器的属性值 “借过来” 当作自己的属性值。
例如看下面的这个例子:
<Grid>
<Button x:Name="b" Content="yes" Height="30" Margin="0,-60,0,0"
Click="B_Click"/>
<Grid>
<Grid DataContext="6">
<Grid>
<Button x:Name="btn" Content="OK" Height="30" Margin="0,60,0,0"
Click="Btn_Click"/>
</Grid>
</Grid>
</Grid>
</Grid>
上面的这段代码有两个 Button,放在了不同的层级中,在第一个与第二个 Button之间,有一个 DataContext,我们看看这个例子会发生什么:
我们看到了,点击 OK按钮会显示 DataContext中的数据,但点击 yes按钮却没有任何的反应,
实际上属性值是沿着 UI元素树向下传递的
,所以才有这种现象。
2.5 —— 使用集合对象作为列表控件的 ItemsSource
2.5.1 ListView控件与后台数据绑定
我们来实现一下 ListView控件与后台数据绑定的例子,来学习一下ItemsSource 的使用:
- XAML 代码如下:
<StackPanel Background="LightBlue">
<ListView x:Name="listViewStudents" Height="130" 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>
</StackPanel>
我们为 ListView控件创建了三列,并将每一列的数据绑定到一个属性中,效果图如下:
- 实现后台数据,并为 ItemsSouce绑定数据:
学生类已经之前的例子中写出,此处就不再写了
ObservableCollection<Student> students = new ObservableCollection<Student>()
{
new Student(){Id = 1, Name="huameng1", Age = 20},
new Student(){Id = 2, Name="huameng2", Age = 21},
new Student(){Id = 3, Name="huameng3", Age = 22},
new Student(){Id = 4, Name="huameng4", Age = 23},
new Student(){Id = 5, Name="huameng5", Age = 24},
new Student(){Id = 6, Name="huameng6", Age = 25},
};
this.listViewStudents.ItemsSource = students;
我创建了一个集合对象,并为这个对象初始化了一些东西,并且这个集合对象赋值给了 listViewStudents 的 ItemsSource 属性。
这里代码中,并没有之前看到的 Binding,可以会有一些疑问,其实当我们给 ItemsSouce 赋值时,就等同于创建了 Binding。
细心的朋友可能会发现,我此处用的集合类型不是 List,这是为什么呢?
因为 ObservbleCollection 这个类,实现了两个接口,分别是:
- INotifyCollectionChanged
- INotifyPropertyChanged
他们会把集合的变化立刻通知显示它的列表控件,改变会立刻显现出来。
- 效果图如下:
2.5.2 TreeView 绑定 XmlDataProvider
- 创建一个 XmlDataProvider对象:
<Window.Resources>
<XmlDataProvider x:Key="xdp" XPath="FildSystem/Folder">
<x:XData>
<FildSystem xmlns=""> <!--这句代码必加,不加显示不了结果-->
<Folder Name="Books">
<Folder Name="programming">
<Folder Name="WPF"/>
<Folder Name="MFC"/>
<Folder Name="Unity"/>
</Folder>
</Folder>
<Folder Name="Tools">
<Folder Name="Vs2019"/>
<Folder Name="Win10"/>
<Folder Name="Phone"/>
</Folder>
</FildSystem>
</x:XData>
</XmlDataProvider>
</Window.Resources>
- 为一个 TreeView对象 绑定 XmlDataProvider:
<Grid>
<TreeView ItemsSource="{Binding Source={StaticResource xdp}}">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding XPath=Folder}">
<TextBlock Text="{Binding XPath=@Name}"/>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
</Grid>
其中为什么 XPath后面的值有一个加了 @呢?
很明显,使用@符号加字符串表示的是 XML元素的 Attribute,不加 @表示的是子级元素
其中有一些模板方面的知识,此处就不讲了,以后我的文章应该会更新
- 效果图如下:
2.6 —— 使用 ObjectDataProvider 对象作为 Binding 的Souce
有的时候,很难保存一个类的所有数据都是使用属性暴露出来,比如我们需要的数据可能是方法的返回值,重新设计一个类成本会高的一比,所以这时候我们就用到了 ObjectDataProvider 来包装Binding源的数据对象了.
下面们来实现一个例子,输入两个数,能够算出他们的和,将这两个数和他们的和分别绑定一个对象。
- 设计一个计算器类,实现一个加法的方法:
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!";
}
}
- XAML代码如下:
<StackPanel Background="LightBlue">
<TextBox x:Name="textBoxArg1" Margin="5"/>
<TextBox x:Name="textBoxArg2" Margin="5"/>
<TextBox x:Name="textBoxResult" Margin="5"/>
</StackPanel>
效果图如下:
- MainWindow类构造器中实现 Binding:
ObjectDataProvider odp = new ObjectDataProvider();
odp.ObjectInstance = new Calculator();
odp.MethodName = "Add";
odp.MethodParameters.Add("0");
odp.MethodParameters.Add("0");
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 };
this.textBoxArg1.SetBinding(TextBox.TextProperty, bindingToArg1);
this.textBoxArg2.SetBinding(TextBox.TextProperty, bindingToArg2);
this.textBoxResult.SetBinding(TextBox.TextProperty, bindingToResult);
代码解析:
- 前五行 对Add 方法进行包装(利用ObjectDataProvider)
- 中间三个Binding 分别将方法的两个参数和结果设置源的路径
- 最后三行 对 UI元素进行绑定
可能有朋友会想问 第三个 Binding为什么与前两个Binding有那么大的不同,原因在于:
在把 ObjectDataProvider对象当作 Binding的 Source来使用时,这个对象本身就代表了数据,所以这里的 Path使用的是 “ . " ,而非其 Data属性。
- 动图效果如下:
2.7 —— 使用 Binding的 RelativeSource
有的时候我们不知道 Source的对象叫什么名字,但我们知道作为 Binding目标的对象在 UI布局上有 相对关系,这时候我们就要使用 Binding的 RelativeSource属性。
比如下面的这个 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>
我们如何使 testBox1的 Text特征 绑定到 x:Name 为 g2的呢 ?
我们可以通过g2相对于 testBox1的层级与类型来确认绑定,如下面代码:
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);
我们绑定源为 第一个类型为 Grid的对象的 Name特性
或者在 XAML 中插入等效代码:
Text="{Binding RelativeSource={RelativeSource FindAncestor,
AncestorType={x:Type Grid}, AncestorLevel=1}, Path=Name}"