数据绑定
WPF 数据绑定系统的完整指南
绑定基础
数据绑定是 WPF 的核心特性,它建立了 UI 元素与数据源之间的自动连接,实现了数据与视图的同步更新。
基本绑定语法
<!-- 简单绑定 -->
<TextBox Text="{Binding UserName}" />
<!-- 带路径的绑定 -->
<TextBlock Text="{Binding User.Name}" />
<!-- 带格式化的绑定 -->
<TextBlock Text="{Binding Price, StringFormat='¥{0:N2}'}" />
<!-- 带更新触发器的绑定 -->
<TextBox Text="{Binding UserName, UpdateSourceTrigger=PropertyChanged}" />
INotifyPropertyChanged 实现
public class Person : INotifyPropertyChanged
{
private string _name;
public string Name
{
get => _name;
set
{
if (_name != value)
{
_name = value;
OnPropertyChanged(nameof(Name));
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
使用 CommunityToolkit.Mvvm
using CommunityToolkit.Mvvm.ComponentModel;
public partial class Person : ObservableObject
{
[ObservableProperty]
private string _name;
[ObservableProperty]
private int _age;
// 自动生成 Name 属性和 OnPropertyChanged 调用
}
💡 绑定工作原理
数据绑定系统包含四个主要组件:
- 绑定目标:通常是 UI 元素的依赖属性
- 绑定源:包含数据的对象(CLR 对象、ADO.NET 对象、XML 数据等)
- 绑定模式:定义数据流动的方向
- 值转换器:在源和目标之间转换数据
绑定模式
绑定模式决定了数据在源和目标之间的流动方向。
OneWay 模式
数据从源流向目标,适用于只读数据展示。
<TextBlock Text="{Binding UserName, Mode=OneWay}" />
TwoWay 模式
数据双向流动,源和目标相互同步。
<TextBox Text="{Binding UserName, Mode=TwoWay,
UpdateSourceTrigger=PropertyChanged}" />
OneWayToSource 模式
数据从目标流向源,适用于需要反向更新的场景。
<TextBox Text="{Binding UserName, Mode=OneWayToSource}" />
OneTime 模式
仅在初始化时绑定一次,之后不再更新。
<TextBlock Text="{Binding UserName, Mode=OneTime}" />
Default 模式
根据目标属性的默认行为决定绑定模式。
<!-- TextBox 默认是 TwoWay -->
<TextBox Text="{Binding UserName}" />
<!-- TextBlock 默认是 OneWay -->
<TextBlock Text="{Binding UserName}" />
💡 模式选择建议
- 只读数据使用 OneWay 或 OneTime
- 可编辑数据使用 TwoWay
- 需要反向更新时使用 OneWayToSource
- 性能敏感场景使用 OneTime
绑定源
绑定源可以是多种类型的数据对象。
ElementName 绑定
<StackPanel>
<Slider x:Name="slider" Minimum="0" Maximum="100" Value="50" />
<TextBlock Text="{Binding ElementName=slider, Path=Value}" />
</StackPanel>
RelativeSource 绑定
<!-- 绑定到自身 -->
<TextBlock Text="{Binding RelativeSource={RelativeSource Self}, Path=FontSize}" />
<!-- 绑定到祖先元素 -->
<TextBlock Text="{Binding RelativeSource={RelativeSource AncestorType=Window},
Path=Title}" />
<!-- 绑定到模板父元素 -->
<ControlTemplate TargetType="Button">
<TextBlock Text="{Binding RelativeSource={RelativeSource TemplatedParent},
Path=Content}" />
</ControlTemplate>
<!-- 绑定到上一级数据上下文 -->
<TextBlock Text="{Binding RelativeSource={RelativeSource PreviousData},
Path=Name}" />
StaticResource 绑定
<Window.Resources>
<SolidColorBrush x:Key="PrimaryBrush" Color="#2196F3" />
</Window.Resources>
<Button Background="{Binding Source={StaticResource PrimaryBrush}}" />
DataContext 绑定
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
// 设置 DataContext
this.DataContext = new MainViewModel();
}
}
集合绑定
public class MainViewModel
{
public ObservableCollection<Person> People { get; }
public MainViewModel()
{
People = new ObservableCollection<Person>
{
new Person { Name = "张三", Age = 25 },
new Person { Name = "李四", Age = 30 }
};
}
}
<ListBox ItemsSource="{Binding People}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Name}" Margin="0,0,10,0" />
<TextBlock Text="{Binding Age}" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
值转换器
值转换器允许在绑定过程中转换数据类型或格式。
IValueConverter 实现
public class BoolToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType,
object parameter, CultureInfo culture)
{
if (value is bool boolValue)
{
return boolValue ? Visibility.Visible : Visibility.Collapsed;
}
return Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType,
object parameter, CultureInfo culture)
{
if (value is Visibility visibility)
{
return visibility == Visibility.Visible;
}
return false;
}
}
使用转换器
<Window.Resources>
<local:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
</Window.Resources>
<TextBlock Text="这是可见内容"
Visibility="{Binding IsVisible,
Converter={StaticResource BoolToVisibilityConverter}}" />
带参数的转换器
public class StringFormatConverter : IValueConverter
{
public object Convert(object value, Type targetType,
object parameter, CultureInfo culture)
{
if (value == null || parameter == null)
return value;
return string.Format(parameter.ToString(), value);
}
public object ConvertBack(object value, Type targetType,
object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
<TextBlock Text="{Binding Price,
Converter={StaticResource StringFormatConverter},
ConverterParameter='¥{0:N2}'}" />
MultiConverter
public class MultiValueConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType,
object parameter, CultureInfo culture)
{
if (values.All(v => v != null))
{
return $"{values[0]} - {values[1]}";
}
return string.Empty;
}
public object[] ConvertBack(object value, Type[] targetTypes,
object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
<TextBlock>
<TextBlock.Text>
<MultiBinding Converter="{StaticResource MultiValueConverter}">
<Binding Path="FirstName" />
<Binding Path="LastName" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
💡 转换器最佳实践
- 转换器应该是无状态的,避免依赖实例状态
- 使用参数传递转换配置,而不是创建多个转换器
- 考虑使用 CommunityToolkit.Mvvm 的转换器基类
- 为转换器添加适当的错误处理
数据验证
WPF 提供了多种数据验证机制,确保数据的完整性和正确性。
ExceptionValidationRule
public class Person : INotifyPropertyChanged
{
private int _age;
public int Age
{
get => _age;
set
{
if (value < 0 || value > 150)
{
throw new ArgumentException("年龄必须在 0-150 之间");
}
_age = value;
OnPropertyChanged(nameof(Age));
}
}
}
<TextBox Text="{Binding Age, UpdateSourceTrigger=PropertyChanged,
ValidatesOnExceptions=True}" />
ValidationRule
public class AgeValidationRule : ValidationRule
{
public int MinAge { get; set; } = 0;
public int MaxAge { get; set; } = 150;
public override ValidationResult Validate(object value,
CultureInfo cultureInfo)
{
if (int.TryParse(value?.ToString(), out int age))
{
if (age >= MinAge && age <= MaxAge)
{
return ValidationResult.ValidResult;
}
}
return new ValidationResult(false,
$"年龄必须在 {MinAge}-{MaxAge} 之间");
}
}
<TextBox Text="{Binding Age, UpdateSourceTrigger=PropertyChanged}">
<TextBox.Text>
<Binding Path="Age" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<local:AgeValidationRule MinAge="0" MaxAge="150" />
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
INotifyDataErrorInfo
public class Person : INotifyPropertyChanged, INotifyDataErrorInfo
{
private readonly Dictionary<string, List<string>> _errors =
new Dictionary<string, List<string>>();
private int _age;
public int Age
{
get => _age;
set
{
_age = value;
ValidateAge();
OnPropertyChanged(nameof(Age));
}
}
private void ValidateAge()
{
_errors.Remove(nameof(Age));
if (_age < 0 || _age > 150)
{
_errors[nameof(Age)] = new List<string>
{
"年龄必须在 0-150 之间"
};
}
ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(nameof(Age)));
}
public bool HasErrors => _errors.Any();
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
public IEnumerable GetErrors(string propertyName)
{
return _errors.TryGetValue(propertyName, out var errors)
? errors
: Enumerable.Empty<string>();
}
}
显示验证错误
<Style TargetType="TextBox">
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="True">
<Setter Property="ToolTip"
Value="{Binding RelativeSource={RelativeSource Self},
Path=(Validation.Errors)[0].ErrorContent}" />
<Setter Property="BorderBrush" Value="Red" />
</Trigger>
</Style.Triggers>
</Style>
高级绑定
PriorityBinding
<TextBlock.Text>
<PriorityBinding>
<Binding Path="HighPriority" IsAsync="True" />
<Binding Path="MediumPriority" IsAsync="True" />
<Binding Path="LowPriority" />
</PriorityBinding>
</TextBlock.Text>
StringFormat
<TextBlock Text="{Binding Price, StringFormat='¥{0:N2}'}" />
<TextBlock Text="{Binding Date, StringFormat='yyyy-MM-dd'}" />
<TextBlock Text="{Binding Name, StringFormat='Hello, {0}!'}" />
FallbackValue
<TextBlock Text="{Binding UserName, FallbackValue='未登录'}" />
TargetNullValue
<TextBlock Text="{Binding MiddleName, TargetNullValue='无'}" />
Delay
<!-- 延迟 500ms 更新源 -->
<TextBox Text="{Binding SearchText,
UpdateSourceTrigger=PropertyChanged,
Delay=500}" />
绑定调试
<!-- 启用绑定调试 -->
<TextBox Text="{Binding UserName,
PresentationTraceSources.TraceLevel=High}" />
💡 绑定性能优化
- 使用 OneTime 或 OneWay 模式代替 TwoWay
- 合理设置 UpdateSourceTrigger
- 使用 Delay 减少频繁更新
- 避免在集合绑定中使用复杂的转换器
- 考虑使用虚拟化处理大量数据