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属性上。
对应绑定的四个组件如下:
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 绑定模式的系统开销。
用前面的例子来说:
我们将MyData的DisplayText属性绑定到TextBox.Text属性上,
当修改MyData.DisplayText属性时,界面会进行更新。
但是在TextBox进行编辑时,MyData.DisplayText属性不会被更新。
TwoWay模式(默认)(UI <=> 属性)
-
通过 TwoWay 绑定,更改源属性或目标属性时会自动更新另一方。 此类型的绑定适用于可编辑窗体或其他完全交互式 UI 方案。 大多数属性默认为 OneWay 绑定,但某些依赖属性(通常为用户可编辑控件的属性,例如 TextBox.Text 和 CheckBox.IsChecked)默认为 TwoWay 绑定。
用前面的例子来说:
我们将MyData的DisplayText属性绑定到TextBox.Text属性上,
当修改MyData.DisplayText属性时,界面会进行更新。
当在TextBox进行编辑时,MyData.DisplayText属性也会被更新。
-
OneWayToSource 绑定与 OneWay 绑定相反;当目标属性更改时,它会更新源属性。(UI -> 属性)
用前面的例子来说:
我们将MyData的DisplayText属性绑定到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提供了两个函数Converter和ConverterBack,当在目标显示数据之前,系统会调用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