数据绑定

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 减少频繁更新
  • 避免在集合绑定中使用复杂的转换器
  • 考虑使用虚拟化处理大量数据