《15天玩转WPF》—— 数据驱动UI核心之 Binding

用最少的文字来解释清楚每一行代码,加油 !


程序 = 数据 + 算法, 数据一直处于核心地位
但传统的程序设计都是 UI驱动程序 ,这反而使数据成为被动,如何使数据反变之为 主动 呢?
WPF 的核心理念就是 数据驱动UI , 要想让数据成功真正的核心,必须使用 Binding 机制。


下面的那些超链接没什么用,只是好看而已 ^ - ^

【文章目录】


要想进行下面的学习,我们最好理清一个基本的概念
什么是源 ? 什么是目标 ? 什么是路径?
我们把 Binding 比作数据的桥梁,那么它的两端分别是 Binding 的 源(Source)目标(Target)
数据从哪里来哪里就是源,Binding是架在中间的桥梁,Binding目标是数据要往哪儿去,路径是数据源中需要绑定的属性是谁
一般情况下,Binding源是逻辑层的对象,Binding 目标是UI层的控件对象,这样我们就完成了数据驱动UI的过程。

我将通过下面的一些案例来详解 Binding的那些事 …


【1. Binding 入门基础】

有了这些基本的概念之后,我们尝试写一个简单的小例子,来感受一下 Binding的神奇之处 
  1. 首先,我们创建一个名为 Student的类,这个类的实例作为 数据源 的使用:
class Student
{
    private string name;	// 姓名

    public string Name
    {
        get {return name;}
        set {name = value;}
    }	
}
  1. 链接接口:
    但是,当我们将这个 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"));
            }
        }
    }	
}
  1. 我们数据源部分已经写好,接下来我们准备两个 UI元素,TextBox 和 Button:
<StackPanel>
	<TextBox x:Name="textBoxName"  BorderBrush="Black"  Margin="5" />
	<Button Content="Add Age"  Margin="5"  Click="Button_Click" />
</StackPanel>

界面大小自行设置,结果图如下:
在这里插入图片描述

  1. 使用 Binding 把数据源和 UI元素连接起来:

连接之前,我们首先在 MainWindow类里写出:
Student student;
为什么不在构造器里面写呢 ? 为了我们使用Button按钮的点击事件时,方便访问

在 MainWindow 构造器中,写出如下的两行代码:

student = new Student() { Name = "huameng" }  // 实例化对象

// 第一个参数是 TextBox的依赖属性绑定到文本
// 第二个实例化 Binding对象,参数为需要绑定的属性(路径), 设置数据源
this.textBoxName.SetBinding(TextBox.TextProperty, new Binding("Name")
 { Source = student });
  1. 为Button 元素实现点击事件(修改数据源的属性):
private void Button_Click(object sender, RoutedEventArgs e)
{
    student.Name += "nb";
}
  1. 效果动画图如下:
    在这里插入图片描述

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

  1. 创建一个Student类,具有三个属性:
public class Student
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int Age { get; set; }
}
  1. 创建程序的 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 对象

  1. 效果图:

在这里插入图片描述

我们前面说过这样一句话:在这里插入图片描述
我来解释一下这里的问题,其实,这只是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 的使用:

  1. 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控件创建了三列,并将每一列的数据绑定到一个属性中,效果图如下:
在这里插入图片描述

  1. 实现后台数据,并为 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

他们会把集合的变化立刻通知显示它的列表控件,改变会立刻显现出来

  1. 效果图如下:
    在这里插入图片描述

2.5.2 TreeView 绑定 XmlDataProvider

  1. 创建一个 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>
  1. 为一个 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,不加 @表示的是子级元素

其中有一些模板方面的知识,此处就不讲了,以后我的文章应该会更新

  1. 效果图如下:

在这里插入图片描述


2.6 —— 使用 ObjectDataProvider 对象作为 Binding 的Souce

有的时候,很难保存一个类的所有数据都是使用属性暴露出来,比如我们需要的数据可能是方法的返回值,重新设计一个类成本会高的一比,所以这时候我们就用到了 ObjectDataProvider 来包装Binding源的数据对象了.

下面们来实现一个例子,输入两个数,能够算出他们的和,将这两个数和他们的和分别绑定一个对象

  1. 设计一个计算器类,实现一个加法的方法:
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!";
        }
}
  1. XAML代码如下:
<StackPanel Background="LightBlue">
        <TextBox x:Name="textBoxArg1" Margin="5"/>
        <TextBox x:Name="textBoxArg2" Margin="5"/>
        <TextBox x:Name="textBoxResult" Margin="5"/>
</StackPanel>

效果图如下:
在这里插入图片描述

  1. 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属性。

  1. 动图效果如下:
    在这里插入图片描述

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

作者:浪子花梦

猜你喜欢

转载自blog.csdn.net/weixin_42100963/article/details/105002103