数据绑定
数据绑定是WPF的核心,也是MVVM开发模式的根基。WPF中,数据可以绑定到对象、控件、资源甚至其他绑定,数据绑定完整支持双向同步、转换、校验等功能,配合依赖属性和变更通知机制,我们的图形界面可以真正做到数据驱动。这篇笔记我们对WPF中的数据绑定用法进行介绍。
Binding 标记扩展
Binding是一个XAML中的标记扩展,它写在XAML的属性值位置,并指定要绑定的内容路径。
<TextBlock Text="{Binding Name}"/>
上面XAML的含义是TextBlock的Text属性绑定到数据源的Name属性上。
绑定数据源
绑定到DataContext内的属性
DataContext是FrameworkElement上的依赖属性,它表示元素的数据上下文,它也是最常用的Binding数据源。当绑定没有显式指定源时,WPF会自动沿元素树向上找最近的DataContext,由于DataContext是可继承的依赖属性,我们通常把它设置在窗口级别,窗口里所有绑定都以它为源。
<Window x:Class="Gacfox.Demo.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:Gacfox.Demo"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<StackPanel>
<TextBlock Text="{Binding Name}"/>
<TextBlock Text="{Binding Age}"/>
</StackPanel>
</Window>
using Gacfox.Demo.Model;
using System.Windows;
namespace Gacfox.Demo;
public partial class MainWindow : Window
{
private readonly Student _student = new() { Name = "张三", Age = 20 };
public MainWindow()
{
InitializeComponent();
DataContext = _student;
}
}
DataContext其实是object类型,因此我们可以将自定义的Student变量赋予它,XAML中,我们使用Binding绑定的字段其实就是赋予给DataContext的对象内的属性。
绑定到另一个元素的属性
WPF中,我们也可以使用另一个元素的属性作为绑定数据源。下面例子中,我们没写任何C#代码,直接基于该绑定机制实现了滑块的值实时显示在文本里的功能。
<Window x:Class="Gacfox.Demo.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:Gacfox.Demo"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<StackPanel>
<Slider x:Name="volumeSlider" Minimum="0" Maximum="100" Value="50"/>
<TextBlock Text="{Binding ElementName=volumeSlider, Path=Value, StringFormat='音量:{0:0}'}"/>
</StackPanel>
</Window>
Source 指定绑定源对象
WPF中,绑定允许使用Source直接指定源对象,这种用法常配合StaticResource绑定XAML资源里声明的对象。
<Window x:Class="Gacfox.Demo.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:Gacfox.Demo"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.Resources>
<local:Config x:Key="config" Theme="Dark"/>
</Window.Resources>
<TextBlock Text="{Binding Source={StaticResource config}, Path=Theme}"/>
</Window>
RelativeSource 相对自身定位源对象
RelativeSource用于相对自身定位源对象,常用于控件模板内部或绑定到父级元素。
下面例子我们将TextBlock的Text属性绑定到了自身的ActualWidth属性,用于实时获取并显示UI控件自身的实际渲染宽度。
<TextBlock Text="{Binding RelativeSource={RelativeSource Self}, Path=ActualWidth}"/>
下面例子我们向上查找最近的Window,绑定它的标题。
<TextBlock Text="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=Title}"/>
Path 绑定路径
Path绑定路径指定从源对象上取哪个值,它的语法其实非常灵活,例如:
{Binding Path=Name}:绑定Name属性,最基础的写法{Binding Name}:Path是默认的隐式属性,Path=可省略{Binding Address.City}:Path支持多级属性,一路向下取值{Binding Students[0].Name}:Path支持索引器,取集合第1项的Name{Binding}:整个Path都省略时,绑定源对象本身
一个常见坑是XAML中,绑定路径是个字符串,因此写错了也不会编译报错,只会在输出窗口打印绑定错误消息,运行界面显示为空。遇到类似问题时,需要看看输出窗口中有没有错误信息。
Mode 绑定模式
绑定的同步方向由Mode属性控制:
| 模式 | 方向 | 典型场景 |
|---|---|---|
OneWay |
从源到目标 | 用于只读展示,如TextBlock显示 |
TwoWay |
源和目标之间双向同步 | 用于编辑场景,如TextBox输入 |
OneTime |
仅绑定建立时同步一次 | 用于初始化后不再变的数据 |
OneWayToSource |
从目标到源 | 用于目标属性驱动源更新的特殊场景 |
不显式指定Mode时,每个依赖属性有自己的默认模式,例如TextBox.Text默认TwoWay,TextBlock.Text默认OneWay。当你不确定时建议将Mode显式写出来,这样代码可读性更好。
数据变更通知 INotifyPropertyChanged
WPF数据绑定中,从源到目标的绑定能生效的前提是源能通知自身发生了变化,普通的属性是没有这个功能的,数据源类需要实现INotifyPropertyChanged才能获得该能力,这和Winform中是类似的。
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace Gacfox.Demo.Model;
public class Student : INotifyPropertyChanged
{
private string? _name;
public string? Name
{
get => _name;
set
{
_name = value;
OnPropertyChanged();
}
}
private int _age;
public int Age
{
get => _age;
set
{
_age = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler? PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string? name = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
代码中,我们用到了[CallerMemberName],这是使用INotifyPropertyChanged时的一种最佳实践,它能让编译器自动把调用处的属性名填进参数,这样每个属性的setter只需写OnPropertyChanged(),不用手写字符串了。实现通知后,界面上所有绑定到Name的控件都会在属性变化的同时实时刷新。
绑定集合 ObservableCollection
对于集合,为了实现集合增删条目时自动通知界面刷新,.NET中提供了INotifyCollectionChanged接口,不过它用起来要实现大量集合操作的变更通知,这非常繁琐,除非极特殊情况我们不必直接实现它,在.NET中还有一个实现好的ObservableCollection<T>类。下面例子中,我们把学生列表绑定到了ListBox,并支持动态增删。
<Window x:Class="Gacfox.Demo.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:Gacfox.Demo"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid Margin="20">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<ListBox x:Name="studentList" DisplayMemberPath="Name"/>
<StackPanel Grid.Row="1" Orientation="Horizontal">
<Button Content="添加" Click="AddButton_Click" Width="80" Margin="0,10,10,0"/>
<Button Content="删除" Click="RemoveButton_Click" Width="80" Margin="0,10,10,0"/>
</StackPanel>
</Grid>
</Window>
using Gacfox.Demo.Model;
using System.Collections.ObjectModel;
using System.Windows;
namespace Gacfox.Demo;
public partial class MainWindow : Window
{
private readonly ObservableCollection<Student> _students = new()
{
new Student { Name = "汤姆", Age = 18 },
new Student { Name = "杰瑞", Age = 17 },
};
public MainWindow()
{
InitializeComponent();
studentList.ItemsSource = _students;
}
private void AddButton_Click(object sender, RoutedEventArgs e)
{
_students.Add(new Student { Name = "新同学", Age = 18 });
}
private void RemoveButton_Click(object sender, RoutedEventArgs e)
{
if (studentList.SelectedItem is Student s)
{
_students.Remove(s);
}
}
}
值转换器 IValueConverter
IValueConverter是绑定管道中的转换器,它能在源和目标之间做值的变换,下面例子中,我们实现了布尔转颜色和布尔转可见性。
using System.Globalization;
using System.Windows;
using System.Windows.Data;
using System.Windows.Media;
namespace Gacfox.Demo;
public class BoolToBrushConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
=> value is true ? Brushes.ForestGreen : Brushes.IndianRed;
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
=> throw new NotSupportedException();
}
public class BoolToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
=> value is true ? Visibility.Visible : Visibility.Collapsed;
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
=> value is Visibility.Visible;
}
代码中,Convert方法是源到目标方向的转换,ConvertBack方法是反向的,单向绑定中后者不会被调用。另外parameter参数对应XAML中的ConverterParameter,我们可以在这里传参复用同一个转换器。
转换器是无状态的,它们通常声明为XAML资源,通过Converter属性引用。
<Window x:Class="Gacfox.Demo.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:Gacfox.Demo"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.Resources>
<local:BoolToBrushConverter x:Key="boolToBrush"/>
<local:BoolToVisibilityConverter x:Key="boolToVis"/>
</Window.Resources>
<StackPanel Margin="20">
<CheckBox x:Name="onlineCheck" Content="在线"/>
<Ellipse Width="20" Height="20" Margin="0,10"
Fill="{Binding ElementName=onlineCheck, Path=IsChecked, Converter={StaticResource boolToBrush}}"/>
<TextBlock Text="详细信息面板"
Visibility="{Binding ElementName=onlineCheck, Path=IsChecked, Converter={StaticResource boolToVis}}"/>
</StackPanel>
</Window>
运行后我们可以看到,勾选复选框,指示灯变绿且信息面板显示,取消勾选则变红隐藏,这些逻辑完全是基于强大的数据绑定机制实现的,没有一行C#代码。
多绑定 MultiBinding
有时一个目标属性依赖多个源,例如“姓名 = 姓 + 名”,MultiBinding可以把多个绑定聚合起来,配合IMultiValueConverter或StringFormat输出。
<Window x:Class="Gacfox.Demo.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:Gacfox.Demo"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<TextBlock>
<TextBlock.Text>
<MultiBinding StringFormat="{}{0} {1}">
<Binding Path="LastName"/>
<Binding Path="FirstName"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</Window>
StringFormat通常足以应付拼接类需求,只有当需要真正的计算(如取最大值)时才可能用到IMultiValueConverter。
数据校验 INotifyDataErrorInfo
TwoWay绑定中用户输入可能不合法,WPF提供了校验机制,我们可以为数据类实现INotifyDataErrorInfo接口并编写校验逻辑。
using System.Collections;
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace Gacfox.Demo.Model;
public class Student : INotifyPropertyChanged, INotifyDataErrorInfo
{
private string? _name;
public string? Name
{
get => _name;
set
{
_name = value;
ValidateName();
OnPropertyChanged();
}
}
private void ValidateName()
{
_errors.Remove(nameof(Name));
if (string.IsNullOrWhiteSpace(_name))
_errors[nameof(Name)] = new List<string> { "姓名不能为空" };
else if (_name.Length > 10)
_errors[nameof(Name)] = new List<string> { "姓名不能超过10个字符" };
ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(nameof(Name)));
}
private readonly Dictionary<string, List<string>> _errors = new();
public bool HasErrors => _errors.Count > 0;
public event EventHandler<DataErrorsChangedEventArgs>? ErrorsChanged;
public IEnumerable GetErrors(string? propertyName)
=> propertyName != null && _errors.TryGetValue(propertyName, out var errors)
? errors
: Enumerable.Empty<string>();
public event PropertyChangedEventHandler? PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string? name = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
绑定侧只需在Binding上添加ValidatesOnNotifyDataErrors=True(默认开启),输入框校验失败时WPF会自动给它画上红色边框。
<Window x:Class="Gacfox.Demo.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:Gacfox.Demo"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<StackPanel Orientation="Vertical">
<TextBox Text="{Binding Name, UpdateSourceTrigger=PropertyChanged, ValidatesOnNotifyDataErrors=True}"/>
</StackPanel>
</Window>
如果想显示具体错误文字,可以绑定Validation.Errors附加属性,配合控件模板定制错误提示样式,这些内容我们将在后续章节介绍。