之前大家写代码都喜欢用事件驱动,比如说鼠标输入的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. 使用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。可以尝试修改一下并且看一下效果。