WPF MVVM入门系列教程(三、数据绑定)

本文主要介绍WPF的数据绑定(Data Binding)功能,如果你已经熟悉本文的内容,可以跳过并直接阅读后面的文章。

本文介绍的内容里都是在MVVM模式开发过程中必须要了解的内容,还有一些关于绑定的知识点,例如:指定源指定、多路绑定、绑定到元素、数据验证等,并没有完全覆盖到,可以自行查找资料学习。

 

什么是数据绑定

我们先来看一下MSDN上的说明:

数据绑定是在应用 UI 与其显示的数据之间建立连接的过程。 如果绑定具有正确的设置,并且数据提供适当的通知,则在数据更改其值时,绑定到该数据的元素会自动反映更改。 数据绑定还意味着,如果元素中数据的外部表示形式发生更改,则基础数据可以自动进行更新以反映更改。

 

我们先不考虑技术细节 ,通俗点来说,可以理解为:

将控件的某个依赖属性(UI)(BindingTarget)绑定到某个数据(BindingSource)上,当数据进行更改时,绑定的依赖属性值会更新(UI更新)。而当依赖属性的值更改(UI更改)时,绑定的数据也会进行更改。

 

举个简单的例子:

例如TextBox控件有个依赖属性Text,它可以设置TextBox的显示内容。

然后我们有一个对象,对象里有个属性叫DisplayText, 将TextBox.Text属性绑定到这个对象的DisplayText属性上。

当我们在界面上对这个TextBox的文本进行编辑时,DisplayText属性会更新。反之,我们对DisplayText进行操作时,TextBox也会进行刷新 。

 


一些基础概念

通常情况下,每个绑定具有四个组件:

  • 绑定目标对象。
  • 目标属性。
  • 绑定源。
  • 指向绑定源中要使用的值的路径

例如TextBox控件有个依赖属性Text,然后我们有一个对象Object,对象里有个属性叫DisplayText, 将TextBox.Text属性绑定到这个对象的DisplayText属性上。

对应绑定的四个组件如下:

 
设置“值”
目标 TextBox
目标属性 Text
源对象 Object
源对象值路径 DisplayText
说明:
1、目标属性必须为依赖属性
      大多数 UIElement 属性都是依赖属性,而大多数依赖属性(只读属性除外)默认支持数据绑定。 只有从 DependencyObject 派生的类型才能定义依赖项属性。 所有 UIElement 类型从 DependencyObject 派生。

2、绑定源不限于自定义 .NET 对象。

绑定源对象不限于自定义 .NET 对象。 WPF 数据绑定支持 .NET 对象、XML 甚至是 XAML 元素对象形式的数据。

3、在建立绑定时,需要将绑定目标绑定到绑定源。 例如,如果要使用数据绑定在 ListBox 中显示List<T>的数据,则需要将 ListBox 绑定到 List<T>数据。

 

 

为什么要使用数据绑定

在第一篇文章中,介绍MVVM的基础概念时,使用了上面这张图,来介绍MVVM的原理。可以看到ViewModel层和View层的交互方式里,有DataBinding

视图为用户提供两项服务:数据展示和与数据交互。数据展示通过读取数据并以某种格式显示,数据交互指的是编辑现有数据或添加新数据。而实现这一功能的基础就是数据绑定。
 
 

数据上下文(DataContext)

当在 XAML 元素上声明数据绑定时,WPF会通过查看它的 DataContext 属性来解析数据绑定。在MVVM开发中,就是将ViewModel赋值给整个窗口的DataContext属性。

如果未设置元素的 DataContext 属性,则将检查父元素的 DataContext 属性,依此类推,直到 XAML 对象树的根。 简而言之,除非在对象上显式设置,否则用于解析绑定的数据上下文将继承自父级。

当 DataContext 属性发生更改时,所以的绑定值都会进行刷新。

 

我们先通过一个简单的例子演示一下:

首先我们创建一个窗口,放置一个TextBox控件,然后将Text属性绑定到DisplayText

 1 <Window x:Class="BasicDataContextDemo.MainWindow"
 2         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
 3         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
 4         xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
 5         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
 6         xmlns:local="clr-namespace:BasicDataContextDemo"
 7         mc:Ignorable="d"
 8         Title="MainWindow" Height="450" Width="800" Name="window">
 9     <Grid Name="grid">
10         <TextBox Name="textbox" HorizontalAlignment="Center" VerticalAlignment="Center" Width="200" Text="{Binding DisplayText}"></TextBox>
11     </Grid>
12 </Window>

 

然后我们创建一个数据对象,其中包含一个DisplayText属性:

 1   public class MyData
 2   {
 3       private string displayText = "HelloWorld";
 4 
 5       public string DisplayText
 6       {
 7           get => displayText;
 8           set => displayText = value;
 9       }
10   }

 

创建一个MyData对象,并绑定到DataContext

 1  public partial class MainWindow : Window
 2  {
 3      public MainWindow()
 4      {
 5          InitializeComponent();
 6 
 7          var myData = new MyData();
 8          this.textbox.DataContext = myData;
 9      }
10  }

 

运行程序可以看到文本框已经被赋值"HelloWorld"

 

数据流的方向

在进行绑定时,可以通过Mode属性来设置数据流的方向。WPF中支持以下几种模式。

 

OneWay模式 (属性 -> UI)

  • 通过 OneWay 绑定,对源属性的更改会自动更新目标属性,但对目标属性的更改不会传播回源属性。 如果绑定的控件为隐式只读,则此类型的绑定适用。 

          如果无需监视目标属性的更改,则使用 OneWay 绑定模式可避免 TwoWay 绑定模式的系统开销。

          用前面的例子来说:

          我们将MyDataDisplayText属性绑定到TextBox.Text属性上,

          当修改MyData.DisplayText属性时,界面会进行更新。

          但是在TextBox进行编辑时,MyData.DisplayText属性不会被更新。

 

 

TwoWay模式(默认)(UI <=> 属性)

  • 通过 TwoWay 绑定,更改源属性或目标属性时会自动更新另一方。 此类型的绑定适用于可编辑窗体或其他完全交互式 UI 方案。 大多数属性默认为 OneWay 绑定,但某些依赖属性(通常为用户可编辑控件的属性,例如 TextBox.Text 和 CheckBox.IsChecked)默认为 TwoWay 绑定。

          用前面的例子来说:

          我们将MyDataDisplayText属性绑定到TextBox.Text属性上,

          当修改MyData.DisplayText属性时,界面会进行更新。

          当在TextBox进行编辑时,MyData.DisplayText属性也会被更新。

 

OneWayToSource模式
  • OneWayToSource 绑定与 OneWay 绑定相反;当目标属性更改时,它会更新源属性。(UI -> 属性)

         用前面的例子来说:

         我们将MyDataDisplayText属性绑定到TextBox.Text属性上,

          当修改MyData.DisplayText属性时,界面不会进行更新。

          当在TextBox进行编辑时,MyData.DisplayText属性会被更新。

 

OntTime模式

  • OneTime 绑定未在图中显示,该绑定会使源属性初始化目标属性,但不传播后续更改。 如果数据上下文发生更改,或者数据上下文中的对象发生更改,则更改不会在目标属性中反映。 如果适合使用当前状态的快照或数据实际为静态数据,则此类型的绑定适合。 如果你想使用源属性中的某个值来初始化目标属性,且提前不知道数据上下文,则此类型的绑定也有用。 此模式实质上是 OneWay 绑定的一种简化形式,它在源值不更改的情况下提供更好的性能。

 

使用示例如下:

1     <TextBox Text="{Binding DisplayText,Mode=TwoWay}"></TextBox>

 

 

属性更改通知

在使用MVVM模式进行开发时,属性更改通知是一个很关键的知识点。

让我们再次看到前面的示例,将TextBox.Text绑定到MyData.DisplayName

1 var myData = new MyData();
2 this.textbox.DataContext = myData;

 

此时我们会发现一个问题,即使我们使用了TwoWay模式绑定,当我们修改MyData.DisplayName值时,界面并不会更新。

1  var myData = this.textbox.DataContext as MyData;
2  myData.DisplayText = "tragedy";  //ui not update

 

如何实现属性更改通知呢?我们需要对MyData进行一些改造。

需要修改的地方如下

1、将MyData实现 INotifyPropertyChanged 接口。

2、当属性更改时,调用PropertyChanged事件进行通知。

 

完整的示例如下:

 1     public class MyData2 : INotifyPropertyChanged
 2     {
 3         private string displayText2 = "HelloWorld";
 4 
 5         public string DisplayText2
 6         {
 7             get => displayText2;
 8             set
 9             {
10                 displayText2 = value;
11 
12                 PropertyChanged?.Invoke(this,new PropertyChangedEventArgs("DisplayText2"));
13 
14                 //or
15                 //OnPropertyChanged();
16             }
17         }
18 
19         public event PropertyChangedEventHandler? PropertyChanged;
20 
21 
22         protected void OnPropertyChanged([CallerMemberName] string name = null)
23         {
24             PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
25         }
26     }

 

此时我们再修改属性的值,界面也会同步刷新。

 

触发源更新的因素

在进行绑定时,可以通过UpdateSourceTrigger属性来设置如何触发源更新。

通俗点来说,就是当界面上的值更改了,例如我编辑了TextBox,何时去更改绑定的属性值呢?这种情况就可以通过UpdateSourceTrigger属性来设置。

需要注意的是只有TwoWay 或 OneWayToSource 模式的绑定会生效,因为只有这两种模式的数据流是从UI到属性。

 

支持以下几种模式

Default

大多数依赖属性的默认值是 PropertyChanged,但是 Text 属性的默认值是 LostFocus

 

PropertyChanged

每当绑定目标属性发生变化时,立即更新绑定源。

 

LostFocus

当绑定目标元素失去焦点时更新绑定源。

 

Explicit

仅在调用 UpdateSource() 方法时更新绑定源。

 

示例如下:

1   <TextBox Text="{Binding Text,UpdateSourceTrigger=PropertyChanged}"></TextBox>

 

数据转换

考虑这样一个场景,假设我们有一个状态值需要绑定到Label并进行显示。

然后我们希望状态是完成时,Label显示绿色,未完成时,显示为红色。 如果我们再增加一个属性绑定到Label.Foreground,这样就显示有点多余。

 

数据如下:

 1     public class MyData : INotifyPropertyChanged
 2     {
 3         public event PropertyChangedEventHandler? PropertyChanged;
 4 
 5         private string status;
 6 
 7         public string Status 
 8         {
 9             get => status;
10             set
11             {
12                 status = value;
13                 PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Status"));
14             }
15         }
16     }

 

界面如下:

1  <Grid>
2      <Label Content="{Binding Status}" HorizontalAlignment="Center" VerticalAlignment="Center"></Label>
3  </Grid>

 

我们可以借用WPF的Converter转换器)功能来实现。

转换器的作用是在目标显示数据之前转换源数据。WPF内置了一些转换器,这里不做详细介绍。大多数情况下,我们需要实现自己的转换器。

需要通过实现 IValueConverter 接口来创建一个自定义转换器LabelForegroundConverter

 

IValueConverter提供了两个函数ConverterConverterBack,当在目标显示数据之前,系统会调用Converter函数。

 

Converter函数参数说明如下:

value绑定源生成的值。
targetType绑定目标属性的类型。
parameter要使用的转换器参数。
culture要用在转换器中的区域性。
完整的示例如下:
 1     public class LabelForegroundConverter : IValueConverter
 2     {
 3         public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
 4         {
 5             if((string)value == "未完成")
 6             {
 7                 return Brushes.Red;
 8             }
 9             else
10             {
11                 return Brushes.Green;
12             }
13         }
14 
15         public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
16         {
17             return null;
18         }
19     }

 

使用方法如下:

1、在XAML中创建转换器实例

1     <Window.Resources>
2         <local:LabelForegroundConverter x:Key="LabelForegroundConverter"></local:LabelForegroundConverter>
3     </Window.Resources>

2、在数据绑定时使用转换器

1  <Label Content="{Binding Status}" Foreground="{Binding Status,Converter={StaticResource LabelForegroundConverter}}" HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="20"></Label>

 

这样当我们设置Label为未完成时,就会显示为红色,当设置为完成时,就会显示为绿色。完整的代码可以查看文末的示例代码

 

下面提供了一些典型方案,在这些方案中,实现数据转换器非常有意义:

  • 数据应根据区域性以不同方式显示。 例如,可能需要根据在特定区域性中使用的约定,实现货币转换器或日历日期/时间转换器。

  • 使用的数据不一定会更改属性的文本值,但会更改其他某个值(如图像的源,或显示文本的颜色或样式)。 在这种情况下,可以通过转换可能不合适的属性绑定(如将文本字段绑定到表单元格的 Background 属性)来使用转换器。

  • 多个控件或控件的多个属性会绑定到相同数据。 在这种情况下,主绑定可能仅显示文本,而其他绑定则处理特定的显示问题,但仍使用同一绑定作为源信息。上面的示例代码就是对应于这种情况

  • 目标属性具有绑定集合,称为 MultiBinding。 对于 MultiBinding,使用自定义 IMultiValueConverter 从绑定的值中生成最终值。 例如,可以从红色、蓝色和绿色的值来计算颜色,这些值可能来自相同绑定源对象,也可能来自不同绑定源对象。 有关示例和信息,请参阅 MultiBinding

 

示例代码

https://github.com/zhaotianff/WPF-MVVM-Beginner/tree/main/3_DataBinding/DataBinding

 

参考资料:

https://learn.microsoft.com/zh-cn/dotnet/desktop/wpf/data/?view=netdesktop-8.0

https://learn.microsoft.com/zh-cn/dotnet/desktop/wpf/data/how-to-implement-property-change-notification?view=netframeworkdesktop-4.8&viewFallbackFrom=netdesktop-8.0

posted @ 2024-10-22 10:22  zhaotianff  阅读(180)  评论(0编辑  收藏  举报