文章

WPF基础——Binding

开始

Binding对象是实现数据和界面双向绑定的基础

在数据部分,数据源需要实现INotifyPropertyChanged接口,其中包含一个PropertyChangedEventHandler类型的PropertyChanged属性,该属性是一个事件,当数据源内的属性变化时,需要调用PropertyChanged来触发属性变化事件,从而能够通知到UI改变数据

数据源的基本实现如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Student : INotifyPropertyChanged {

    public event PropertyChangedEventHandler? PropertyChanged;

    private string name = "";
    public string Name {
        get => name;
        set {
            name = value;
            // 当Name被修改时,触发事件
            // 注意这里传入Name属性而不是name字段,因为外部通过Name属性来访问name字段
            PropertyChanged?.Invoke(this, new("Name"));
        }
    }
}

编写一个简单布局,在后台类中通过C#代码的方式将Student类的Name属性绑定到文本框中(也可使用标签扩展),xaml布局如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<StackPanel 
    Orientation="Vertical"
    VerticalAlignment="Center">
    <TextBlock
    	Width="250"
        Height="50"
        Margin="30, 0"
        x:Name="MyText"/>
    <Button
        Width="250"
        Height="50"
        Margin="30, 0"
        x:Name="Button"
        Content="Click Me"/>
</StackPanel>

后台类如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public partial class MainWindow : Window {
    public MainWindow() {
        InitializeComponent();

        var student = new Student();
        // 数据源就是Student对象,路径Path就是属性的访问器,即Name属性
        var binding = new Binding() {
            Source = student,
            Path = new PropertyPath("Name")
        };

        // 将Student的Name属性绑定到TextBlock的Text属性上
        BindingOperations.SetBinding(MyText, TextBlock.TextProperty, binding);
        // 基于FrameworkElement的元素也就是基本元素对SetBinding进行了封装,也拥有SetBinding方法
        // MyText.SetBinding(TextBlock.TextProperty, binding);

        // 设置Click事件
        Button.Click += (_, _) => student.Name = "Hello";
    }
}

上述Binding对象的Source属性可以接收任何对象,若对象没有实现INotifyPropertyChanged接口,则无法向Binding通知自身的状态变化,INotifyPropertyChanged提供了对象向Binding通知自身变化的能力

Binding属性

Binding对象的常用属性如下

属性名描述
AsyncState获取或设置传递给异步数据调度程序的不透明数据。
BindingGroupName获取或设置此绑定所属的BindingGroup的名称
Converter获取或设置要使用的转换器
ConverterParameter获取或设置要传递给Converter的参数
Delay获取或设置更新位于目标更改上的值之后的绑定源前要等待的时间(毫秒)
ElementName获取或设置要用作绑定源对象的控件元素的名称
FallbackValue获取或设置当绑定无法返回值时要使用的值
IsAsync获取或设置一个值,该值表示Binding是否应异步获取和设置值
Mode获取或设置一个值,该值指示绑定的数据流方向
Path获取或设置绑定源属性的路径
RelativeSource通过指定绑定源相对于绑定目标位置的位置,获取或设置此绑定源
Source获取或设置要用作绑定源的对象
StringFormat获取或设置一个字符串,该字符串指定如果绑定值显示为字符串时如何设置该绑定的格式
TargetNullValue获取或设置当源的值为 null 时在目标中使用的值

数据流向

通过设置Binding对象的Mode属性可以改变Binding数据的流向,Mode属性是BindingMode类型,拥有四个枚举值

  • OneWay:单向流动
  • TwoWay:双向流动,默认值
  • OnTime
  • OneWayToSource
  • Default:根据控件的读写属性确定单向或双向

Path路径

Path属性指定绑定的数据源属性

  • 直接路径:直接指定属性名

    1
    2
    3
    
    <!--通过标签扩展引用其他元素的属性-->
    <TextBlock Text="Hello World" x:Name="MyText"></TextBlock>
    <TextBlock Text="{Binding ElementName=MyText, Path=Text}"></TextBlock>
    
  • 多级路径:可以获取属性的属性

    1
    2
    3
    4
    5
    6
    
    <!--通过标签扩展引用其他元素的属性-->
    <TextBlock Text="Hello World" x:Name="MyText"></TextBlock>
    <!--Path指向Text的Length属性-->
    <TextBlock Text="{Binding ElementName=MyText, Path=Text.Length}"></TextBlock>
    <!--使用Text的索引器,点.可以省略-->
    <TextBlock Text="{Binding ElementName=MyText, Path=Text.[0]}"></TextBlock>
    
  • 默认路径:当绑定的数据源自身就是数据值时,使用默认路径

    当数据源是一个集合时,使用/表示第一个元素的默认路径

    1
    2
    3
    4
    5
    6
    
    <Window.Resources>
        <sys:String x:Key="MyValue">Hello</sys:String>
    </Window.Resources>
    <TextBlock Text="{Binding ., Source={StaticResource MyValue}}"/>
    <!--等价于-->
    <TextBlock Text="{Binding Path=., Source={StaticResource ResourceKey=MyValue}}"/>
    

数据源

Binding指定数据源主要通过Source属性,下面介绍通过不同的途径设置Binding数据源

CLR属性和依赖属性

CLR属性就是普通对象的普通属性,与依赖对象的依赖属性区分

依赖对象继承了DependencyObject类,依赖属性为DependencyProterty类型,命名后缀通常是Property,在xaml中引用时会省略该后缀。依赖属性是依赖其他属性的属性,如控件的可绑定属性,通常作为Binding的目标

同时依赖对象也可作为Binding的源,当作为Binding源时,该依赖对象可能是其他Binding的目标,从而形成依赖链

DataContext

Binding将DataContext的值作为数据源,Path属性指定数据源中的属性

1
2
3
4
5
6
<!--可以省略点.-->
<Label Content="{Binding Path=.}">
    <Label.DataContext>
        Hello
    </Label.DataContext>
</Label>

DataContextFrameworkElement的属性,是object类型,每个控件都拥有自己的DataContextDataContext是一个依赖属性,当控件的DataContext没有显式赋值时,会依赖父控件的DataContext,因此当Binding指定路径时,可以自动获取到父控件或祖先控件的DataContext中的属性

1
2
3
4
5
6
 <StackPanel>
    <StackPanel.DataContext>
        <sys:Int32>32</sys:Int32>
    </StackPanel.DataContext>
    <Label Content="{Binding Path=.}"></Label>
</StackPanel>

集合类型

ItemsControl控件拥有ItemsSource属性,接收一个实现IEnumerable接口的对象作为数据源

DataTemplate类定义了单个子项数据通过哪些控件展示,DataTemplateSelector类的子类重写了SelectTemplate方法,实现了子项数据与控件的绑定(创建Binding对象),再使用DataTemplate将添加Binding的控件对象包装起来返回

1
2
3
public virtual DataTemplate SelectTemplate(object item, DependencyObject container) {
    // ...
}

默认的DataTemplateSelectorDisplayMemberTemplateSelector,它的DisplayMemberPath属性指定了数据源成员的路径,简单地将数据源成员通过TextBlock控件展示为一个字符串,其核心代码如下

1
2
3
4
5
6
7
8
9
10
11
// 初始化DataTemplate和控件对象
_clrNodeContentTemplate = new DataTemplate();
FrameworkElementFactory text = ContentPresenter.CreateTextBlockFactory();
// 创建Binding并设置
Binding binding = new Binding();
binding.Path = new PropertyPath(_displayMemberPath);
binding.StringFormat = _stringFormat;
text.SetBinding(TextBlock.TextProperty, binding);
// 由DataTemplate包装后返回
_clrNodeContentTemplate.VisualTree = text;
_clrNodeContentTemplate.Seal();

在xaml中可以给ItemsControl自定义DataTemplate,赋值给ItemTemplate属性

1
2
3
4
5
6
7
8
9
10
11
<ListBox
	x:Name="StudentList"
	Height="Auto">
	<ListBox.ItemTemplate>
		<DataTemplate>
			<Label Content="{Binding Path=Name}"/>
		</DataTemplate>
	</ListBox.ItemTemplate>
</ListBox>
<!--在C#中指定ItemsSource-->
<!--StudentList.ItemsSource = list;-->

ElementName

ElementName设置为其他控件的x:Name,将被引用的控件对象作为Binding源

1
2
<TextBlock Text="Hello" x:Name="Hello"/>
<Label Content="{Binding ElementName=Hello, Path=Text}"></Label>

RelativeResource

相对于当前元素,查找其他元素的属性

构造RelativeSource对象赋值给Binding的RelativeSource属性,将RelativeSource查找的元素作为源

  • Mode:查找模式,FindAncestor查找祖先,Self查找自身
  • AncestorLevel:确定祖先相对层级
  • AncestorType:确定祖先控件类型,通过x:Type获取Type类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<StackPanel 
    Orientation="Vertical"
    VerticalAlignment="Center"
    Background="Red">
    <TextBlock
        Text="{
            Binding 
                RelativeSource={
                RelativeSource 
                Mode=FindAncestor,
                    AncestorLevel=1, 
                    AncestorType={x:Type StackPanel}
                }, 
            Path=Background
        }"/>
</StackPanel>

其他数据源

  • ADO.NET类型对象

    DataTable、DataView等

  • XML数据

    通过XmlDataProvider将xml中的数据传递给集合控件数据源属性

  • ObjectDataProvider

    当数据通过方法获取时,使用ObjectDataProvider包装数据源,再赋给Source

数据校验

基本使用

Binding对象包含一个ValidationRules属性,类型为Collection<ValidationRule>,表示可以对一个Binding设置多个校验条件

ValidationRule是一个抽象类,包含一个Validate抽象方法,返回ValidationResult对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class RangeValidation : ValidationRule {

    public override ValidationResult Validate(object? value, CultureInfo cultureInfo) {
        var num = (int)(value ?? throw new ArgumentNullException(nameof(value)));
        if (num > 0) {
            // 第二个参数为错误信息
            return new ValidationResult(true, null);
        } else {
            return new ValidationResult(false, "num < 10");
        }
    }
}

// 添加到ValidationRules中
binding.ValidationRules.Add(new RangeValidation());

Binding校验默认只校验通过外部方法改变Target导致Source改变,不会校验Source改变导致Target改变,设置ValidatesOnTargetUpdated属性为true,校验Source导致的改变

校验错误事件

若需要Binding在校验错误时发出一个事件,需要设置NotifyOnValidationError属性为true

校验错误事件会沿着元素树传播,当遇到一个元素设置了校验错误事件处理器,则该元素处理该事件,处理后可继续传播,也可立即停止

1
2
3
4
5
6
7
8
9
10
11
// 定义事件处理器
void ValidateError(object sender, RoutedEventArgs e) {
    // 判断是否有校验错误
    if (Validation.GetHasError(MyText)) {
        // 获取校验错误信息
        var message = Validation.GetErrors(MyText)[0].ErrorContent.ToString(); 
    }
}

// 设置事件处理器
MyTextBlock.AddHandler(Validation.ErrorEvent, new RoutedEventHandler(ValidateError));

数据转换

数据转换通过IValueConverter实现类实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class MyConverter : IValueConverter {

    public object Convert(object? value, 
                          Type targetType, 
                          object? parameter, 
                          CultureInfo culture) {
        // Source转换为Target
    }

    public object ConvertBack(object? value, 
                              Type targetType, 
                              object? parameter, 
                              CultureInfo culture) {
        // Target转换为Source
    }
}

MultiBinding

MultiBinding组合多个Binding的Source,绑定到一个Target

MultiBinding支持Binding的基本属性,如StringFormatModeConverter

image-20240413213742394

组合字符串的基本使用,xaml标签扩展

1
2
3
4
5
6
7
8
<TextBlock x:Name="MyText">
    <TextBlock.Text>
        <MultiBinding StringFormat="Binding1:{},Binding2:{}">
            <Binding />
            <!--other bindings-->
        </MultiBinding>
    </TextBlock.Text>
</TextBlock>

多值转换:实现IMultiValueConverter转换多个Source

1
2
3
4
5
6
7
8
9
10
11
public class MultiConverter : IMultiValueConverter {

    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) {
        // Sources转换为Target
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) {
        // Target转换为Sources
    }
}

本文由作者按照 CC BY 4.0 进行授权