WPF教程三:学习Data Binding把思想由事件驱动转变为数据驱动

之前大家写代码都喜欢用事件驱动,比如说鼠标输入的click事件、初始化的内容全部放在窗体加载完毕的load事件,等等,里面包含了大量的由事件触发后的业务处理代码。导致了UI和业务逻辑高度耦合在一个地方。代码难于维护、也难以优化。

  我们这个章要讲的内容是忘记我们的事件驱动、尝试理解数据驱动。客户端开发分层的话理论上就是数据层、业务逻辑层、UI层,相对于三层的话一般我们的代码可以分为:

  A:数据的持久化存储;

  B:数据的读取和写入;

  C:业务逻辑处理;

  D:界面业务逻辑处理后数据的展示。

  E:界面与业务逻辑的交互。

  在这样的开发过程中A、B一般都是设计最满意的地方。持久化过程做的既通用、又能清晰,持久化数据和实体类之间的定义、转换,都是变动性最小、最稳定的。而C与客户端的关系最紧密、变动也最大。大多数代码都是集中在这里。D、E两部分是负责显示UI、和处理UI的交互逻辑。也有不少的代码量。

  显然C部分是一个程序中,代码量最多,随着版本迭代最容易混乱的地方,所以我们应该重点把精力放在C部分,但是D、E两个部分切因为和业务层紧密相连,C部分的频繁改动很可能导致我们把本来属于C部分的代码写入D、E部分里。比如窗体或控件的Click、构造函数、load里面。因为这2部分以消息或者事件来与逻辑层沟通,所以一旦出现同一个数据需要在多处展示、修改时,用于同步逻辑得代码就会变得复杂,代码也会到处乱写。因为在解决业务问题时,我们的重点在C部分。但是在解决UI交互问题的时候,D、E的UI展示又编程了我们的重点,思维来回的切换,导致我们写出很多难以维护的代码。

  WPF中引入了Data Binding的概念。使用Data Binding配合属性通知和数据模板,我们就可以把关注的D、E的展示层和C的业务逻辑层更好的分割开来。使我们把重点放在业务逻辑层。UI上的元素通过Data Binding可以和数据关联上、一处数据可以和多处UI元素绑定。也可以双向绑定,如果能很好的使用这个思路,我们就可以很好的实现了逻辑层和UI层的解耦。而且所有与业务逻辑相关的代码都会处于业务逻辑层、用户界面不包含任何代码。

  开头讲了这么说,就是想让大家忘记之前的事件驱动写代码的方式。然后尝试开始学习数据驱动写代码的方式。Data Binding就是第一步。

什么是Binding

  我们先来看一个最简单的例子,我们使用Binding来把一个元素的值绑定到另外一个元素的值上。使用ElementName来指向对应的元素Name,Path来指向我们想绑定的元素对应的属性,该例子不包含任何后台代码:

 

<Window x:Class="BindingExample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:BindingExample"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <StackPanel>
            <TextBlock Text="{Binding ElementName=slider,Path=Value}"/> 
            <Slider x:Name="slider" Maximum="100" Minimum="1"/>
        </StackPanel> 
    </Grid>
</Window>

我们把代码跑起来效果图如下,我们的TextBlock显示的Text和Slider的滑动值绑定在了一起:

在WPF中Binding可以通过调用类的INotifyPropertyChanged的实现自动通知功能使多个绑定了属性的UI元素自动更新UI。在WPF中依赖项属性是个很重要的知识点,但是我觉得应该先讲解bangding,在建立了数据驱动的思维,先去使用数据驱动,再去搞明白数据驱动的原理。而这个例子中我们使用Binding绑定了Slider的属性Value。再Slider 上按F12.进入到类的说明界面。我们看到了又一个Value属性。还有一个属性名为ValueProperty,类型为DependencyProperty的对象。他就是我们所说的依赖项属性。这一章我们不讲他。只讲如何使用。当作普通属性就好。

所以这个例子就是我们把一个Textlock的Text显示内容通过Binding绑定到了Slider的Value属性上。而通过在属性的Set方法中调用INotifyPropertyChanged的实现。所以TextBlock的Text能随着Slider的Value变化跟着一起显示对应的值就行。这里能理解到这样就可以了。继续往下。

 既然2个元素可以绑定一个属性。随着DataContext下对应属性的值的变化而变化,就达到了我们要的目的,解耦业务层和UI层。我们通过业务层修改对应的属性,达到更新UI得目的。UI通过更新对应的属性。达到修改业务层得目的。这样我们就可以把重点放在业务层。

 通过这个原理,我们尝试创建一个业务层和UI层交互的属性,并绑定它,通过属性更新UI显示结果。通过UI得交互修改属性得值来达到更新业务层。这样我们只需要关注业务层当前值的变化。

我们通过cs文件设置一个简单的属性通知。我们把UI的显示值和设置的Person下的Intelligence属性绑定在一起。如果UI变化了。Intelligence也变化。如果Intelligence变化了。UI也跟着变化。就实现了我们刚才计划的一个业务逻辑层的值变化。直接影响绑定的UI部分。UI的绑定的值变化,可以直接再逻辑层处理。因为只是演示功能。所以我们没有VM层。这里只演示值得变化。后面MVVM会讲解DataContext。和MVVM分层。这里主要理解Binding得值可以双向通知就可以了。

修改XAML代码和CS为以下内容:

 

<Window x:Class="BindingExample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:BindingExample "
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <StackPanel>
            <TextBlock Text="{Binding Intelligence}"/>
            <Slider x:Name="slider" Maximum="100" Minimum="1" Value="{Binding Intelligence}"/>
        </StackPanel>   
    </Grid>
</Window>

 

 

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace BindingExample
{
    /// <summary>
    /// MainWindow.xaml 的交互逻辑
    /// </summary>
    public partial class MainWindow : Window
    {
        Person duwenlong;
        public MainWindow()
        {
            InitializeComponent();
            duwenlong = new Person();
            this.DataContext = duwenlong;
        }
    }
    public class Person : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        private double _intelligence;
        public double Intelligence
        {
            get { return _intelligence; }
            set
            {
                _intelligence = value;
                Debug.WriteLine($"Intelligence as {Intelligence}");
                if (this.PropertyChanged != null)
                {
                    this.PropertyChanged.Invoke(this, new PropertyChangedEventArgs("Intelligence"));
                }
            }
        }
    }
}

我们再每次Set值得时候。向控制台打印了一个消息。Debug.WriteLine($"Intelligence as {Intelligence}");用来观察是否实现了绑定。我们观察到,值再变化的时候,业务层和UI层是一起再变化的。到此刻我们得目的就达到了。因为基于这个功能,我们结合其他知识我们可以完成很多很多得功能。但是目前要理解和养成数据驱动得思维习惯。

 到现在为止,我们也没有在后台写业务代码。因为我们的模板是解耦业务逻辑层和UI层。我们要学习的是通过Binding来实现业务逻辑的值变更、直接更新到UI层。

我们讲解一下以上代码:

在XAML文件中我们创建了一个TextBlock 和一个Slider。2个控件。我们把TextBlock的Text属性(用于显示文本的属性)设置为{Binding Intelligence}。把Slider的Value属性(滑块的当前值)设置为{Binding Intelligence}。

如果想使用绑定。

1、XAML中就必须使用{Binding }这样的写法。后面跟的是属性,而这个属性是来自于当前类的DataContext中。this.DataContext对象是我们自己在cs代码中赋值的。XAML元素通过Binding绑定DataContext下的某个元素的值。来实现更改对应的属性。

而后台代码中必须设置需要绑定的对象到this.DataContext。这个对象(我们当前的Person)必须继承自INotifyPropertyChanged。并且使用PropertyChanged来触发通知。如果这个属性需要通知UI层,在属性的Set里就需要发送通知消息。写法就类似于

PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Name"));

如果执行绑定失败可以在对照一下代码。看看哪里有问题。这是简单的绑定。。

接下来我们尝试双向绑定和通过代码设置绑定。修改代码如下:

 

<Window x:Class="BindingExample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:BindingExample"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
    <StackPanel>
        <TextBlock Text="{Binding Intelligence}"/>
        <TextBox Text="{Binding Intelligence,Mode=TwoWay}"/>
        <Slider Minimum="1" Maximum="100" Value="{Binding Intelligence}"/>
        <StackPanel Orientation="Horizontal">
            <TextBlock Text="名称:"/>
            <TextBlock Text="{Binding Name}" MinWidth="120"/>
            <TextBlock Text="请输入需要修改的名称:"/>
            <TextBox MinWidth="120" x:Name="tb_inputName"/>
        </StackPanel>
        <Button Content="通过代码修改绑定值得属性。修改Name为杜文龙" Click="AlertText_Click"/>
    </StackPanel>
    </Grid>
</Window>

 

 

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace BindingExample
{
    /// <summary>
    /// MainWindow.xaml 的交互逻辑
    /// </summary>
    public partial class MainWindow : Window
    {
        Person duwenlong;
        public MainWindow()
        {

            InitializeComponent();
            duwenlong = new Person();
            Binding binding = new Binding();
            binding.Source = duwenlong;
          binding.Mode = BindingMode.TwoWay;
            binding.Path = new PropertyPath("Name");
            BindingOperations.SetBinding(tb_inputName, TextBox.TextProperty, binding);
            this.DataContext = duwenlong;
        }

        private void AlertText_Click(object sender, RoutedEventArgs e)
        {
            duwenlong.Name = "杜文龙";
        }
    }

    public class Person : INotifyPropertyChanged
    {
        private double _intelligence;
        public double Intelligence
        {
            get { return _intelligence; }
            set
            {
                _intelligence = value;
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Intelligence"));

            }
        }
        private string _name;
        public string Name
        {
            get { return _name; }
            set
            {
                _name = value;
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Name"));
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;
    }
}

在XAML中我们添加了一个TextBox 它的TextBox为 {Binding Intelligence,Mode=TwoWay} ,注意这个Mode=TwoWay。这是一个双向绑定的意思。通过它可以实现UI的内容更新了会返回到后台的绑定属性上。(因为没有绑定TextChanged事件,所以输入完成后需要丢失焦点才会有反应我按下了Tab键。这里需要使用Binding的一个属性UpdateSourceTrigger.他的类型是一个枚举。目前先不延申去讲)。我们看到了没有针对性的写后台的的代码通过一个属性,就完成了多处的使用和更新。而这个属性是业务层的。所以可以通过这个值来干很多的事情。

接下来我们继续看上面其他的代码:

<TextBlock Text="{Binding Name,Mode=TwoWay}" MinWidth="120"/>
<TextBox MinWidth="120" x:Name="tb_inputName"/>
<Button Content="通过代码修改绑定值得属性。修改Name为杜文龙" Click="AlertText_Click"/>
 在这里TextBlock 的Text绑定了后台代码的Person实例下的Name属性,
Name为tb_inputName的TextBox通过后台代码也实现了绑定Name。还是双向绑定。在cs文件下和XAML文件下使用{Binding }效果是一样的。

 我们把TextBlock和TextBox都绑定了Person的Name属性。我们又在Button下创建了Click事件。用来模拟修改Name属性(我们目前没有分层。也没有学习Command 所以假设cs文件是业务层)。

在Click事件中我们修改了person对象的Name属性为杜文龙。Name属性通过绑定关联了textblock和textbox。所以我们没有直接操作UI层。

当Name属性变化时。对应绑定的UI控件的值也发生了变化。因为双向绑定当TextBox的值变化时,Name也发生了变化。这样就可以在业务层处理了。。

我们尝试把双向绑定修改为单向绑定:

XAML下写法:

 cs下写法:

 在尝试修改TextBox并把焦点切换走。会发现其他绑定Name值得控件的值并没有变化。这章就讲这么多拉。主要是尝试培养数据驱动得思维。

Binding还支持多级路径、省略Path等写法。作为新手目前不推荐延申这些知识。因为主要先搞明白什么是数据驱动。如何使用数据驱动。在去考虑如何使用更高级的功能。

经过大佬提醒前面有些内容写错了,属性通知我们使用的是普通属性,不是依赖项属性,已经修改了。漏掉了一个在Binding中比较重要的知识点。RelativeSource.

在实际使用Binding的过程中大部分时间Binding都放在了数据模板和控件模板中,(数据模板是控件模板用于定义控件的UI)。

在模板中编写Binding时有时候无法直接拿到我们需要绑定的数据对象,我们不能确定我们需要的Source对象叫什么,但是我们直到了我们需要使用的对象在UI布局上的相对关系。比如控件自己关联了某个数据,关键自己某个层级的容器数据。这个时候我们的RelativeSource就派上了用场。我们使用RelativeSource首先要3个关键参数。

AncestorType=我们需要查找的类型。比如Grid

AncestorLevel= 我们需要向上查找几级

Path=我们找到的元素需要绑定的属性。

这三个关键的参数配置完。我们就可以完成对RelativeSource的使用。

 

<Grid x:Name="G0" Margin="12" Background="Red">
                <TextBlock Text="In this Grid0 container"/>
                <Grid x:Name="G1" Margin="12" Background="Blue">
                    <TextBlock Text="In this Grid1 container"/>
                    <Grid x:Name="G2" Margin="12" Background="Yellow">
                        <TextBlock Text="In this Grid2 container"/>
                        <Grid x:Name="G3" Margin="12" Background="Beige">
                            <StackPanel>
                                <TextBlock Text="In this Grid3 container"/>
                                <TextBlock Name="ces" Text="{Binding RelativeSource={RelativeSource AncestorType=Grid,AncestorLevel=1},Path=Name}"/>
                            </StackPanel>                         
                        </Grid>
                    </Grid>
                </Grid>
            </Grid>

我们嵌套几个Grid,并在每个嵌套的Grid中都放入了一行文本用来显示自己所在的位置。设置了Margin使他有部分的重叠,可以更好的看到相互之间的层级关系。最内层使用一个TextBlock.在TextBlock的Text属性上使用RelativeSource。通过修改AncestorLevel 来设置向上查找Grid的等级。我们设置为1.向外层查找第一个找到的Grid对象。并绑定对应的Name。可以尝试修改一下并且看一下效果。

 

 

 

我创建了一个C#相关的交流群。用于分享学习资料和讨论问题。欢迎有兴趣的小伙伴:QQ群:542633085

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值