总目录


前言

本章主要介绍依赖属性,附加属性以及数据绑定的的内容。


一、依赖属性&附加属性

1.依赖属性

1)概念

  • 什么是依赖对象DependencyObject
    派生自DependencyObject的类型就是依赖对象,而WPF中所有的控件都是派生自DependencyObject。
  • 什么是依赖属性DependencyProperty
    依赖属性就是一种自己可以没有值,但可以通过绑定从其他数据源获取值,这种依赖在其它对象上的属性,就是依赖属性。
  • 依赖对象和依赖属性
    只有 DependencyObject 类型可以定义依赖属性,WPF的控件都是依赖对象,并且大多数的属性都是依赖属性

2)依赖属性提供的属性功能

依赖属性重要性在于,在WPF中的资源引用,数据绑定,样式模板的引用,动画设置,元数据重写,属性值继承等等功能的实现都需要依赖属性的支持。

  • 资源,依赖属性值可以通过引用资源(DynamicResource 或StaticResource)来设置。
    示例代码如下:
    <Window.Resources>
        <SolidColorBrush x:Key="myBtnColor" Color="Red"></SolidColorBrush>
    </Window.Resources>
    <Grid Background="{DynamicResource myBtnColor}"></Grid>

注意:若要是使用动态资源引用,必须设置为依赖属性

  • 数据绑定,依赖属性可以通过数据绑定来引用值。使用数据绑定,最终属性值的确定将延迟到运行时,在运行时将从数据源获取属性值。也只有依赖属性才可以进行数据绑定
    示例代码如下:
<Grid  >
        <TextBox x:Name="tb1" Width="200" Height="40" VerticalContentAlignment="Center"></TextBox>
        <Button Content="{Binding ElementName=tb1,Path=Text}" Width="200" Height="40" ></Button>
    </Grid>
  • 样式,样式和模板是使用依赖属性的两个主要场景,主要是通过Style 和Template 两个依赖属性来设置一个控件的外观

  • 动画,可以对依赖属性进行动画处理,
    示例代码如下:

<Button>I am animated
  <Button.Background>
    <SolidColorBrush x:Name="AnimBrush"/>
  </Button.Background>
  <Button.Triggers>
    <EventTrigger RoutedEvent="Button.Loaded">
      <BeginStoryboard>
        <Storyboard>
          <ColorAnimation
            Storyboard.TargetName="AnimBrush" 
            Storyboard.TargetProperty="(SolidColorBrush.Color)"
            From="Red" To="Green" Duration="0:0:5" 
            AutoReverse="True" RepeatBehavior="Forever" />
        </Storyboard>
      </BeginStoryboard>
    </EventTrigger>
  </Button.Triggers>
</Button>
  • 元数据重写,通过依赖属性的OverrideMetadata来重写父类的属性默认行为

  • 属性值继承,元素可以从其在对象树中的父级继承依赖属性的值。
    示例代码如下:

    public class UserInfo
    {
        public string Id { get; set; }
        public string Name { get; set; }
    }
    <Window.Resources>
        <local:UserInfo x:Key="info" Id="1" Name="测试"></local:UserInfo>
    </Window.Resources>
    <StackPanel>
        <StackPanel  DataContext="{Binding Source={StaticResource info}}">
            <!--由于父类控件,指定了数据源,因此子类会继承DataContext属性,无需再次指定-->
            <TextBlock Text="{Binding Path=Id}"></TextBlock>
            <!--当父类已经指定数据源,并且Binding中只有一个Path的时候,可以省略-->
            <TextBlock Text="{Binding Name}"></TextBlock>
        </StackPanel>

		    <!--无父类指定数据源,则需自己指定,与前一种对比-->
        <TextBlock Text="{Binding Source={StaticResource info}, Path=Name}"></TextBlock>
    </StackPanel>
  • WPF 设计器集成,使用WPF设计器时,可以在属性窗口编辑属性值

3)如何自定义一个依赖属性

  • 快捷键propdp,双击Tab键,改下属性名称,就有了如下代码
    public class UserInfo:DependencyObject
    {
        public int Id
        {
            get { return (int)GetValue(IdProperty); }
            set { SetValue(IdProperty, value); }
        }

        // Using a DependencyProperty as the backing store for Id.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty IdProperty =
            DependencyProperty.Register("Id", typeof(int), typeof(UserInfo), new PropertyMetadata(0));
    }

定义一个依赖属性很简单,下面来解析一下依赖属性这一长串的代码:

  1. 首先依赖属性所在类必须继承DependencyObject,如public class UserInfo:DependencyObject
  2. 再者申明一个DependencyProperty的变量,并且必须以Property作为后缀,该变量才是真正的依赖属性,如IdProperty
  3. 再者通过调用DependencyProperty.Register静态方法向WPF属性系统中注册依赖属性
  4. 最后用get和set 包装依赖属性,设置器中通过GetValue()和SetValue()方法来操作依赖属性值的,这两个方法由DependencyObject提供

4)详解DependencyProperty.Register

为使属性成为依赖属性,必须在属性系统注册该属性,并为属性指定一个唯一标识符。
(1)用于注册依赖属性的静态方法,有以下三个重载方法

public static DependencyProperty Register(string name, Type propertyType, Type ownerType);

public static DependencyProperty Register(string name, Type propertyType, Type ownerType, PropertyMetadata typeMetadata);

public static DependencyProperty Register(string name, Type propertyType, Type ownerType, PropertyMetadata typeMetadata, ValidateValueCallback validateValueCallback);

(2)各参数解析

  • 第一个参数name:表示要注册的依赖属性的名称。 名称必须在所有者类型的注册命名空间中是唯一的。这个名称非常重要,在定义控件Style和Template的时候,Setter的Property属性填入的值就是注册依赖属性时使用的名称
  • 第二个参数propertyType: 表示属性的类型。
  • 第三个参数ownerType:表示正在注册依赖属性的所有者类型
  • 第四个参数typeMetadata:依赖属性的属性元数据
  • 第五个参数validateValueCallback:表示一个回调(委托),验证数据的有效性,除典型的类型验证之外,该引用还应执行依赖属性值的任何自定义验证

前三个参数好理解,那么第四个和第五个参数呢?

(3)【PropertyMetadata typeMetadata】解析

这个参数是表示依赖属性的元数据,需要实例化new 才可以传入
主要用来指定依赖属性的功能例如:默认值、属性值变更后的操作(回调函数)、继承、双向绑定、是否可以绑定数据等。
PropertyMetadata 有以下构造函数

//默认无参数的构造函数
public PropertyMetadata()//设置默认值的构造函数,通常用于为依赖属性设置默认值
public PropertyMetadata(object defaultValue);

//提供属性变更后触发操作(回调)的构造函数
public PropertyMetadata(PropertyChangedCallback propertyChangedCallback);

//设置默认值,并提供属性变更后触发操作(回调)的构造函数
public PropertyMetadata(object defaultValue, PropertyChangedCallback propertyChangedCallback);

//设置默认值,提供属性变更后触发操作(回调)并且的构造函数
public PropertyMetadata(object defaultValue, PropertyChangedCallback propertyChangedCallback, CoerceValueCallback coerceValueCallback);
//表示在依赖属性的有效属性值更改时调用的回调。
//d 表示属性值已更改的 System.Windows.DependencyObject对象
//e 属性更改事件的数据,可以提供属性更新前的OldValue 和 更新后的 NewValue,OldValue和NewValue是e这个参数常用的两个属性值
public delegate void PropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e);

//每当重新计算的依赖项属性值,或强制专门请求时调用的方法提供一个模板
==未完==//
public delegate object CoerceValueCallback(DependencyObject d, object baseValue);

【ValidateValueCallback validateValueCallback】解析
主要验证数据的有效性,当Register定义了ValidateValueCallback,值传入或者更改的时候就会先进行验证

//value 表示需要验证的值,返回bool类型,表示值是否通过验证
public delegate bool ValidateValueCallback(object value);

2.附加属性

1)概念

附加属性是指可在任何依赖项对象上设置的全局属性。
附加属性是个 XAML 概念,而依赖属性则是个 WPF 概念。但是在WPF中,WPF 附加属性是依赖属性
因此,WPF中附加属性和依赖属性基本使用和功能都是一样的,准确来讲,在WPF中应该叫做:附加的依赖属性。区别在于,依赖属性定义在宿主内部,而附加属性不定义在宿主内部,一般定义在公共的类中,提供给全局使用。

示例代码如下:

 <DockPanel>
        <Button Width="200" Height="35" DockPanel.Dock="Left"></Button>
 </DockPanel>

附加属性就是自己没有这个属性,在某些上下文中需要就被附加上去。如 示例中的Button中设置的DockPanel.Dock属性,就是附加属性,该属性是在DockPanel中定义的附加属性,本不属于Button,但是如果控件属于DockPanel的子元素就可以使用。

2)如何自定义一个附加属性

  • propa 双击Tab,自动生成如下代码
        public static int GetMyProperty(DependencyObject obj)
        {
            return (int)obj.GetValue(MyPropertyProperty);
        }

        public static void SetMyProperty(DependencyObject obj, int value)
        {
            obj.SetValue(MyPropertyProperty, value);
        }

        // Using a DependencyProperty as the backing store for MyProperty.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty MyPropertyProperty =
            DependencyProperty.RegisterAttached("MyProperty", typeof(int), typeof(ownerclass), new PropertyMetadata(0));

我们修改属性类型和名称,如下:

    public class UserInfo
    {
        public static string GetName(DependencyObject obj)
        {
            return (string)obj.GetValue(NameProperty);
        }

        public static void SetName(DependencyObject obj, string value)
        {
            obj.SetValue(NameProperty, value);
        }

        public static readonly DependencyProperty NameProperty =
            DependencyProperty.RegisterAttached("Name", typeof(string), typeof(UserInfo), new PropertyMetadata(null));

    }

相较于依赖属性的定义,附加属性使用DependencyProperty.RegisterAttached 静态方法注册附加属性。
RegisterAttached 和Register的重载方法都是一样的,这里就不再讲解。另外定义附加属性无需继承自DependencyObject,因为他在获取和设置附加属性值得时候,都需要将DependencyObject作为参数传入。

3.重写依赖属性的元数据

这个需要了解,当前学习阶段用的不多,当我们在看别人的代码的时候,至少知道这个是在重写依赖属性元数据。
用法很简单,使用OverrideMetadata方法就可重写。

public class MyStateControl : ButtonBase
{
  public MyStateControl() : base() { }
  public Boolean State
  {
    get { return (Boolean)this.GetValue(StateProperty); }
    set { this.SetValue(StateProperty, value); }
  }
  public static readonly DependencyProperty StateProperty = DependencyProperty.Register(
    "State", typeof(Boolean), typeof(MyStateControl),new PropertyMetadata(false));
}

使用OverrideMetadata重写

public class MyAdvancedStateControl : MyStateControl
{
  public MyAdvancedStateControl() : base() { }
  static MyAdvancedStateControl()
  {
    MyStateControl.StateProperty.OverrideMetadata(typeof(MyAdvancedStateControl), new PropertyMetadata(true));
  }
}

该案例就是通过重写将State 的默认值false ,重写为默认值 true

4.自定义依赖属性和附加属性应用场景

大多数的需要自定义依赖属性和附加属性的场景都是自定义控件或者用户控件的时候,详细使用会在自定义控件或者用户控件那一章进行详细介绍。这里简单先介绍一下,自定义依赖属性和附加属性在自定义控件中的使用

示例如下:

        <Button Height="40" Width="200" Background="Red" Content="测试按钮"  Foreground="White" FontWeight="Bold"
                VerticalContentAlignment="Center" HorizontalContentAlignment="Center">
            <Button.Template>
                <ControlTemplate TargetType="Button">
                    <Border CornerRadius="5" Background="{TemplateBinding Background}">
                        <ContentPresenter VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
                                          HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                                          ></ContentPresenter>
                    </Border>
                </ControlTemplate>
            </Button.Template>
        </Button> 

效果如下:
在这里插入图片描述

上面示例通过ControlTemplate写了个圆角的按钮,但是呢,目前圆角的大小是硬编码,如果该模板是公用资源,别的地方需要复用的话,就需要圆角大小可以调节了,因此我需要将圆角这个属性绑定到Button上,让我们可以通过设置button的某个属性,就可以直接调节圆角。可惜button,本身又没有圆角这个属性,此时我们就需要自定义一个Button,然后给他定义个圆角的依赖属性,如下所示:

新建一个类,继承自Button,然后定义一个依赖属性BtnCornerRadius

    public class MyButton:Button
    {
        public CornerRadius BtnCornerRadius
        {
            get { return (CornerRadius)GetValue(BtnCornerRadiusProperty); }
            set { SetValue(BtnCornerRadiusProperty, value); }
        }

        public static readonly DependencyProperty BtnCornerRadiusProperty =
            DependencyProperty.Register("BtnCornerRadius", typeof(CornerRadius), typeof(MyButton), new PropertyMetadata(null));
    }

在xaml代码中引用这个自定义的按钮,然后将Border的圆角属性绑定到BtnCornerRadius属性上,就实现了一个可以调节圆角的按钮

        <local:MyButton Height="40" Width="200" Background="Green" 
                 Content="测试按钮"  Foreground="White" FontWeight="Bold"
                VerticalContentAlignment="Center" HorizontalContentAlignment="Center"
                BtnCornerRadius="10">
            <local:MyButton.Template>
                <ControlTemplate TargetType="{x:Type local:MyButton}">
                    <Border CornerRadius="{TemplateBinding BtnCornerRadius}" Background="{TemplateBinding Background}">
                        <ContentPresenter VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
                                          HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                                          ></ContentPresenter>
                    </Border>
                </ControlTemplate>
            </local:MyButton.Template>
        </local:MyButton>

效果如下:
在这里插入图片描述
以上就是依赖属性在自定义控件中的简单应用。

附加属性如何使用呢?再看下面场景:
我们平时项目中,除了按钮,肯定还有一些输入框或者其他控件也会需要边框,此时我们就可以将圆角定义成附加属性,提供给所有需要的控件使用,操作如下:

先新建一个类,类中定义一个圆角附加属性CommonCornerRadius

    public class Common
    {
        public static CornerRadius GetCommonCornerRadius(DependencyObject obj)
        {
            return (CornerRadius)obj.GetValue(CommonCornerRadiusProperty);
        }

        public static void SetCommonCornerRadius(DependencyObject obj, CornerRadius value)
        {
            obj.SetValue(CommonCornerRadiusProperty, value);
        }

        public static readonly DependencyProperty CommonCornerRadiusProperty =
            DependencyProperty.RegisterAttached("CommonCornerRadius", typeof(CornerRadius), typeof(Common), new PropertyMetadata(null));

    }

再者在xaml 中将模板中Border 的圆角属性绑定到 CommonCornerRadius 上即可。

    <UniformGrid>
        <local:MyButton Height="40" Width="200" Background="Green" Content="测试按钮"  Foreground="White" FontWeight="Bold"
                VerticalContentAlignment="Center" HorizontalContentAlignment="Center" local:Common.CommonCornerRadius="20">
            <local:MyButton.Template>
                <ControlTemplate TargetType="{x:Type local:MyButton}">
                    <Border CornerRadius="{TemplateBinding local:Common.CommonCornerRadius}" Background="{TemplateBinding Background}">
                        <ContentPresenter VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
                                          HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                                          ></ContentPresenter>
                    </Border>
                </ControlTemplate>
            </local:MyButton.Template>
        </local:MyButton>
        <TextBox Height="40" Width="200" BorderThickness="3" BorderBrush="Red"  
                 Text="圆角输入框" Foreground="White" FontWeight="Bold" local:Common.CommonCornerRadius="20"
                 HorizontalContentAlignment="Left" VerticalContentAlignment="Center">
            <TextBox.Template>
                <ControlTemplate>
                    <Border Background="Red" CornerRadius="{TemplateBinding local:Common.CommonCornerRadius}" BorderThickness="1">
                        <ScrollViewer Margin="20,0,0,0" x:Name="PART_ContentHost"  
                                      VerticalAlignment="{TemplateBinding VerticalContentAlignment}" 
                                      HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"/>
                    </Border>
                </ControlTemplate>
            </TextBox.Template>
        </TextBox>
    </UniformGrid>

以上案例,通过TemplateBinding local:Common.CommonCornerRadius 将附加属性分别绑定到 自定义local:MyButton 和 TextBox 上,实现了圆角的按钮和圆角的输入框。
相信通过以上两个案例,对于依赖属性和附加属性的使用,就有了一定的认识。
在这里插入图片描述

官方资料-Control 样式和模板,感兴趣的可以查看这个文档提前了解一下针对不同的控件,样式模板如何编写。

二、数据绑定

1.数据绑定基本概念

数据绑定是在应用 UI 与其显示的数据之间建立连接的过程。

1)WPF 数据绑定示意图

在这里插入图片描述

如图所示,数据绑定实质上是绑定目标与绑定源之间的桥梁,通常情况下,每个绑定具有四个组件:

【绑定目标对象】、【目标属性】、【绑定源】、【指向绑定源中要使用的值的路径】

2) 数据绑定初识

示例代码如下:

    <StackPanel>
        <TextBox x:Name="tb1" Text="{Binding ElementName=slider1,Path=Value,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}" Height="50" Width="300" VerticalContentAlignment="Center" Background="AliceBlue" Margin="10"></TextBox>
        <Slider x:Name="slider1" Minimum="0" Maximum="100" TickFrequency="5" TickPlacement="Both" Value="50"></Slider>
    </StackPanel>

在这里插入图片描述

上面案例就是将输入框的文本将滑块的值进行绑定。下面就以该案例展开:

  • 绑定目标对象以及目标属性,绑定目标的对象必须是依赖对象,绑定目标的属性必须是依赖属性,如案例中的TextBox就是绑定目标,由上面的依赖属性的讲解中我们知道,TextBox元素和Text属性都是符合要求的

  • 绑定源,指获取数据的源,如上面案例就是将Slider作为绑定源。

  • 路径Path ,就是指定绑定源控件的那个具体的属性值作为数据源,如上案例就是Slider的Value属性

  • 数据流的方向Mode,如上面案例中的Mode 则是指定数据流的方向,有OneWay,OneWayTwoSource,OneTime,TwoWay四种方式。

  • 触发源更新的因素UpdateSourceTrigger 控制绑定源更新的执行时间

3)绑定源

以绑定源来区分,有三种绑定方式:
(1) 指定ElementName,绑定指定名称的控件
通过使用就是指定具体控件名称,然后使用该控件的某个属性值。

        <TextBox x:Name="tb" Height="40" VerticalContentAlignment="Center" Text="123456"></TextBox>
        <!--绑定 TextBox 的Text-->
        <Button  Height="40" Content="{Binding ElementName=tb,Path=Text}"></Button>
        <TextBlock  Height="40" Background="LightBlue" Text="{Binding ElementName=tb,Path=Height}"></TextBlock>
        <!--绑定 自身的背景颜色值 ,讲道理可以绑定一切有名字的控件-->
        <TextBlock x:Name="tbk"  Height="40" Background="Azure" Text="{Binding ElementName=tbk,Path= Background.Color}"></TextBlock>

(2)通过RelativeSource绑定,RelativeSource通常在 ControlTemplate 或 Style 中指定绑定
Mode 描述相对于绑定目标的位置的绑定源的位置
Self 绑定自身,FindAncestor 绑定父类元素,TemplatedParent 通常用在模板中,具体的应用如下面示例代码所示:

  <StackPanel x:Name="mainStackPanel">

        <!-- Self  绑定自身-->
        <TextBlock x:Name="tbk"  Height="40" Background="Azure" Text="{Binding RelativeSource={RelativeSource Mode=Self},Path=Name}"></TextBlock>

        <!--FindAncestor 绑定父类元素-->
        <Grid>
            <StackPanel x:Name="stackPanel">
                <Button  Height="40" Content="测试按钮"></Button>
                <!--使用AncestorType 指定父类元素的类型,使用AncestorLevel 指定向上找几层-->
                <TextBlock Height="40" Background="Azure" Text="{Binding RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=StackPanel,AncestorLevel=1},Path=Name}"/>
                <TextBlock Height="40" Background="Aqua" Text="{Binding RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=StackPanel,AncestorLevel=2},Path=Name}"/>
            </StackPanel>
        </Grid>

        <!--TemplatedParent 通常用在模板中-->
        <!-- Background="{Binding RelativeSource={RelativeSource Mode=TemplatedParent},Path=Background}"  与  Background="{TemplateBinding Background}" 效果一样-->
        <Button Height="40" Background="Green">
            <Button.Template>
                <ControlTemplate>
                    <Border Background="{Binding RelativeSource={RelativeSource Mode=TemplatedParent},Path=Background}" 
                           
                            CornerRadius="10" Margin="10,0">
                        <!--其他代码-->
                    </Border>
                </ControlTemplate>
            </Button.Template>
        </Button>
    </StackPanel>

在这里插入图片描述

(3) 使用Source 指定数据源
使用场景如下:

    public class UserInfo
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string PhoneNumber { get; set; }
    }
        <StackPanel 
            xmlns:ss="clr-namespace:System;assembly=mscorlib">
            <StackPanel.Resources>
                <local:UserInfo x:Key="info" Id="1" Name="张三" PhoneNumber="15123556233"></local:UserInfo>
                <ss:String x:Key="str">测试一下</ss:String>
            </StackPanel.Resources>
            <!--使用suorce 引用指定的系统资源-->
            <TextBlock Text="{Binding Source={x:Static SystemColors.ActiveBorderBrush }}"></TextBlock>
            <!--使用suorce 引用指定资源-->
            <TextBlock Text="{Binding Source={StaticResource info},Path=Name}"></TextBlock>
            <TextBlock Text="{Binding Source={StaticResource str}}"></TextBlock>
        </StackPanel>

4)DataContext

(1)获取或设置元素参与数据绑定时的数据上下文。
public object DataContext { get; set; } 因为是object才可以承载各种复杂的值

  • 如果要将几个属性绑定到一个通用源,则需要使用 DataContext属性,
  • 它能让你方便地建立一个范围,所有数据绑定的属性都在该范围中继承通用源。
  • DataContext是依赖属性,因此子元素继承父元素依赖属性的值。

示例代码如下:

        <StackPanel>
            <StackPanel.Resources>
                <local:UserInfo x:Key="userInfo" Id="1" Name="张三" PhoneNumber="888888888"></local:UserInfo>
            </StackPanel.Resources>
            <!--父元素指定数据上下文-->
            <StackPanel.DataContext>
                <Binding Source="{StaticResource userInfo}"></Binding>
            </StackPanel.DataContext>
            <!--如果父类定义DataContext了那么子类将不需要定义,直接使用Path指定即可-->
            <TextBlock Text="{Binding Path=Id}"></TextBlock>
            <TextBlock Text="{Binding Path=Name}"></TextBlock>
        </StackPanel>

        <Grid>
            <!--父元素不指定数据上下文-->
            <Grid.Resources>
                <local:UserInfo x:Key="userInfo" Id="1" Name="李四" PhoneNumber="888888888"></local:UserInfo>
            </Grid.Resources>
            <!-- 父元素未指定,则需自己指定-->
            <TextBlock Text="{Binding Source={StaticResource userInfo},Path=Name}"></TextBlock>
        </Grid>

以上说明了DataContext 的基本使用,DataContext在xaml中是要配合Binding使用才可以的。

(2)将DataContext 定义在整个xaml页面根元素上
一般情况下在MVVM模式中使用DataContext都是定义在整个xaml页面根元素上,将整个页面的数据都一次性绑定上
方式一:在根元素上指定数据绑定

    <UserControl.DataContext>
        <local:UserViewModel></local:UserViewModel>
    </UserControl.DataContext>

方式二:在cs文件中指定数据绑定

        public UserView()
        {
            InitializeComponent();
            this.DataContext = new UserViewModel();
        }

5)数据路径Path

  • 在最简单的情况下,Path 属性值是要用于绑定的源对象的属性名称,如 Path=PropertyName。

  • 属性的子属性可以通过类似于 C# 的语法来指定。 例如,子句 Path=ShoppingCart.Order 设置与对象或属性 ShoppingCart 的子属性 Order 的绑定。

  • 若要绑定到附加属性,请将附加属性置于括号中。 例如,若要绑定到附加属性 DockPanel.Dock,则语法为 Path=(DockPanel.Dock)。

  • 可以在已应用索引器的属性名后面的方括号内指定属性的索引器。 例如,子句 Path=ShoppingCart[0] 将绑定设置为与属性的内部索引处理文本字符串“0”的方式对应的索引。 还支持多个索引器。

  • 可以在 Path 子句中混用索引器和子属性,例如 Path=ShoppingCart.ShippingInfo[MailingAddress,Street].或RenderTransform.Children[1].Angle

  • 可以在索引器内使用多个由逗号 (,) 分隔的索引器参数。 可以使用括号指定每个参数的类型。 例如,可以使用 Path=“[(sys:Int32)42,(sys:Int32)24]”,其中 sys 将映射到 System 命名空间。

  • 如果源是集合视图,则可以使用斜杠 (/) 指定当前项。 例如,子句 Path=/ 设置与视图中当前项的绑定。 如果源是集合,则此语法指定默认集合视图的当前项。

  • 可以组合使用属性名和斜杠,以遍历作为集合的属性。 例如,Path=/Offices/ManagerName 指定源集合的当前项,源集合中包含的 Offices 属性也是一个集合。 它的当前项是一个包含 ManagerName 属性的对象。

  • 句点 (.) 路径也可以用于绑定到当前源。 例如,Text=“{Binding}” 等效于 Text=“{Binding Path=.}”。

6)数据方向

数据流方向说明
OneTime该绑定会使源属性初始化目标属性,但不传播后续更改
即只在项目初始化是绑定一次,赋予初始值,后续不再绑定
OneWay对源属性的更改会自动更新目标属性,但对目标属性的更改不会传播回源属性
OneWayToSource绑定与 OneWay 绑定相反,当目标属性更改时,它会更新源属性
TwoWay更改源属性或目标属性时会自动更新另一方

仍然以滑块和文本框作为示例:

<StackPanel>
            <TextBox x:Name="tb1" Text="{Binding ElementName=slider1,Path=Value,Mode=OneTime}" Height="50" Width="300" VerticalContentAlignment="Center" Background="AliceBlue" Margin="10"></TextBox>
            <Slider x:Name="slider1" Minimum="0" Maximum="100" TickFrequency="5" TickPlacement="Both" Value="50"></Slider>
</StackPanel>

如果是OneTime,那么程序运行后,无论任何滑动滑块改变value,都不会影响到文本框的显示。
如果是OneWay,那么移动滑块(数据源变化),文本框的文本(目标属性)就会变化,反之,则不行
在这里插入图片描述
如果是OneWayToSource ,那么你会发现,当我们更新目标属性的时候,源压根就没更新
Text="{Binding ElementName=slider1,Path=Value,Mode=OneWayToSource,FallbackValue=20}" 这里使用FallbackValue 属性给文本一个默认值,否则会显示错误边框。
在这里插入图片描述
如果是TwoWay,那么你会发现,明明是TwoWay,但是实际上还是只能实现,源数据(滑块滑动)变化,导致文本框文本(目标属性)显示变化。于是我另外加一个Button,修改完文本框文本后,然后点击Button,这时你会发现 两个可以联动了。
在这里插入图片描述
这是为什么呢?

7)触发 更新源 的因素

在上面介绍数据流方向的时候我们发现,当我们将滑块和文本框绑定起来,使用Mode为OneWayToSource或者TwoWay的时候似乎还存在点问题,那么是什么导致的呢?

现在我们将以上示例代码,再做一点修改,绑定中增加一项UpdateSourceTrigger=PropertyChanged

我们发现现在使用OneWayToSource或者TwoWay 联动都没有任何问题了。
在这里插入图片描述
原因就在于:

UpdateSourceTrigger 属性的作用是获取或设置,绑定源更新的方式。

UpdateSourceTrigger 默认值为 Default,即返回目标依赖属性的默认 UpdateSourceTrigger值。 但是,大多数依赖项属性的默认值为 PropertyChanged,而 TextBox.Text属性的默认值为 LostFocus(失去焦点时)

这就解释了,为什么上面我使用OneWayToSource或者TwoWay的时候,修改文本框的文本(修改目标属性)后无法传导到滑块上(绑定源),然后更新源。但当点击另一个按钮,让文本框失去焦点就又可以了。另外我们设置UpdateSourceTrigger=PropertyChanged后,表示不再是默认的失去焦点后更新数据源,而是每当绑定目标属性发生更改时,都会更新源。

        <StackPanel>
            <TextBox x:Name="tb1" Text="{Binding ElementName=slider1,Path=Value,
            Mode=TwoWay,UpdateSourceTrigger=PropertyChanged, 
            FallbackValue=50}" Height="50" Width="300" 
            VerticalContentAlignment="Center" Background="AliceBlue" 
            Margin="10"></TextBox>
            <Slider x:Name="slider1" Minimum="0" Maximum="100" 
            TickFrequency="5" TickPlacement="Both" Value="20"></Slider>
        </StackPanel>
枚举值说明
Default 0绑定目标属性的默认 UpdateSourceTrigger 值。 大多数依赖属性的默认值为 PropertyChanged,
而 Text 属性的默认值为 LostFocus。
PropertyChanged 1每当绑定目标属性发生更改时,都会更新绑定源。
LostFocus 2每当绑定目标元素失去焦点时,都会更新绑定源。
Explicit 3仅在调用 UpdateSource() 方法时更新绑定源。

对于控件之间的相互绑定是依赖属性之间的操作可以使用UpdateSourceTrigger去通知更新。
但是对于自己定义的类如果想绑定到界面上会出现什么情况呢?

2.数据更新 INotifyPropertyChanged

如果创建的 CLR 对象未实现 INotifyPropertyChanged,必须安排自己的通知系统,才能确保绑定中所用的数据保持最新状态。可以通过支持要更改通知的每个属性的 PropertyChanged 模式来提供更改通知。 若要支持此模式,请为每个属性定义一个PropertyNameChanged 事件,其中 PropertyName 是属性的名称。 每次更改属性时都会引发该事件。

简单说:就是说我们需要将自己写的类绑定到xmal页面,并且还要能够实时更新,必须实现INotifyPropertyChanged接口。

下面就以案例来说明:

 		<StackPanel>
            <TextBox Text="{Binding PhoneNumber, Mode=TwoWay}" Height="50" VerticalContentAlignment="Center" Background="AliceBlue" Margin="10" FontSize="30"></TextBox>
            <TextBlock  Text="{Binding Name, Mode=TwoWay}" Height="50"  Background="AliceBlue" Margin="10" FontSize="30"></TextBlock>
            <Button Height="50" Margin="10" Click="Button_Click_1" Content="测试按钮-目标更新是否传导到源"></Button>
            <Button Height="50" Margin="10" Click="Button_Click" Content="测试按钮- 源更新是否传导到目标"></Button>
        </StackPanel>
    public class PersonInfo
    {
        public int Id { get; set; }

        public string Name { get; set; }

        public string PhoneNumber { get; set; }
    }
    public partial class MainWindow : Window
    {
        public PersonInfo Person { get; set; } = new PersonInfo() { Id = 1, Name = "测试章", PhoneNumber = "8888888888" };

        public MainWindow()
        {
            InitializeComponent();
            this.DataContext = Person;
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            Person.PhoneNumber = "7777777777";
            Person.Name = "测试李";
        }

        private void Button_Click_1(object sender, RoutedEventArgs e)
        {
            MessageBox.Show($"Name:{Person.Name}---------PhoneNumber:{Person.PhoneNumber}");
        }
    }

在这里插入图片描述
当我们仅仅是定义个普通的类,然后去绑定的xaml页面,我们发现除了初始化的时候,值绑定上去了,目标到源的更新没问题,但是点击按钮修改源时候,页面上并非做更新。

下面我们来认识一下INotifyPropertyChanged 接口。
INotifyPropertyChanged接口的作用是:通知客户端属性值已更改
该接口中只有一个事件需要子类实现:

//通知客户端属性值已更改
public interface INotifyPropertyChanged
{
    //     在属性值更改时发生。
    event PropertyChangedEventHandler PropertyChanged;
}

//表示将处理的方法PropertyChanged 在组件上更改属性时引发的事件。
//sender 事件源,e 包含事件数据的 System.ComponentModel.PropertyChangedEventArgs。
public delegate void PropertyChangedEventHandler(object sender, PropertyChangedEventArgs e);


// 为 System.ComponentModel.INotifyPropertyChanged.PropertyChanged 事件提供数据。
public class PropertyChangedEventArgs : EventArgs
{   
        //propertyName:已更改属性的名称。
        public PropertyChangedEventArgs(string propertyName);

        //获取已更改属性的名称。
        public virtual string PropertyName { get; }
}

由上分析可知:当我们在更新了某个属性的值的时候,需要需要通知客户端(这里指xaml页面)做出更新,我们需要调用PropertyChanged这个事件,这个事件本身是一个需要有两个入参的委托,第一参数是事件源本身,第二个参数就是更改了属性值的属性名称,当我们调用并传入参数后,该事件就负责通知xaml页面上的绑定目标的属性值做出更新。

下面我们实现一下INotifyPropertyChanged接口

    public class PersonInfo:INotifyPropertyChanged
    {
        private int id;

        public int Id
        {
            get { return id; }
            set
            {
                id = value;
                OnPropertyChanged(nameof(Id));
            }
        }

        private string name;

        public string Name
        {
            get { return name; }
            set
            {
                name = value;
                OnPropertyChanged(nameof(Name));
            }
        }

        private string phoneNumber;

        public string PhoneNumber
        {
            get { return phoneNumber; }
            set
            {
                phoneNumber = value;
                OnPropertyChanged(nameof(PhoneNumber));
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;

        public void OnPropertyChanged(string propertyName)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }

set中调用OnPropertyChanged这个方法,就是表示每次属性的值更新的时候,都会调用这个方法,通知xaml做出对应更新。
实现完接口,我们在此运行示例项目:
在这里插入图片描述
此时就已经完成了 数据源更新通知的功能。

但是还有点问题,那就是平常我们很多对象都需要实现INotifyPropertyChanged接口,如果每个对象中都写一遍不太“ok”,我们就要定义一个基类,实现掉INotifyPropertyChanged接口,后续其他的类直接继承基类即可,另外还有一个问题,那就是OnPropertyChanged这个方法每次传入的都是固定格式的属性名称,这个也可以封装到基类中,直接调用而不用每次都传入属性名称。

编写基类如下:

    public class PropertyChangedBase : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        public void OnPropertyChanged([CallerMemberName]string propertyName=null)
        {
            PropertyChanged?.Invoke(this,new PropertyChangedEventArgs(propertyName));
        }
    }

其中CallerMemberName的作用是:允许获取方法调用方的方法或属性名称,比如Id 属性的set 设置器中
调用了OnPropertyChanged,那么就会获取得到Id这个属性名称,作为propertyName的参数值传入,也就避免我们每次调用都需要传入属性名称的操作了。
那么对象中就可以精简如下:

    public class PersonInfo:PropertyChangedBase
    {
        private int id;
        private string name;
        private string phoneNumber;

        public int Id
        {
            get { return id; }
            set
            {
                id = value;
                OnPropertyChanged();
            }
        }
        
        public string Name
        {
            get { return name; }
            set
            {
                name = value;
                OnPropertyChanged();
            }
        }
        
        public string PhoneNumber
        {
            get { return phoneNumber; }
            set
            {
                phoneNumber = value;
                OnPropertyChanged();
            }
        }
    }

到这里已经很简洁了,那还能不能更简洁一点呢,答案是:当然可以!

	public class PropertyChangedBase : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        public void Set<T>(ref T field, object value, [CallerMemberName] string name="")
        {
            field= (T?)value!;
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));            
        }
    }
        

我们直接将属性set设置器内给字段设置值的逻辑也集成进来,使用如下图所示:

		//在这里对比一下:会发现第二种方式更为简洁
        //[第一种方式]
        private string phone;
        public string Phone
        {
            get { return phone; }
            set { phone = value; OnPropertyChanged(); }
        }
		//[第二种方式]
        private string phoneNumber;
        public string PhoneNumber
        {
            get { return phoneNumber; }
            set { Set<string>(ref phoneNumber, value); }
        }

以上两种方式,都是可行的,具体看个人偏好。并且以上方法,都可以通过自定义代码片段的方法,快速生成,例如:敲一个propp 就自动写出整个属性代码片段,感兴趣的可查看我的另一篇博文:VS代码片段(CodeSnippet)的制作以及常用代码片段记录

至此,数据绑定基本介绍完成了大部分。

3.数据转换 IValueConverter

若要在绑定期间转换数据,必须创建一个实现 IValueConverter 接口的类,此操作涉及使用到 Convert 和 ConvertBack 方法。

日常开发中,数据转换可能会经常碰到,我们通过以下案例理解一下如何使用:
将DataTime转化为短时间字符串,将0,1,2性别int标记转化为保密,男,女字符串类型

    <UserControl.DataContext>
        <local:UserViewModel></local:UserViewModel>
    </UserControl.DataContext>
    <UserControl.Resources>
        <Style TargetType="TextBox">
            <Setter Property="Height" Value="50"></Setter>
            <Setter Property="VerticalContentAlignment" Value="Center"></Setter>
            <Setter Property="FontSize" Value="30"></Setter>
        </Style>
    </UserControl.Resources>
    <StackPanel Background="LightSteelBlue">
        <TextBox Background="LawnGreen" Text="{Binding CrateTime}" ></TextBox>
        <TextBox Background="LightBlue" Text="{Binding Sex}"></TextBox>
    </StackPanel>
    class UserViewModel:PropertyChangedBase
    {
        private string userId;
        private int sex;
        private DateTime crateTime;

        public string UserId
        {
            get { return userId; }
            set 
            { 
            	userId = value; 
				OnPropertyChanged();
			}
        }

        //Sex 0 表示保密,1 表示男,2 表示女
        //页面上要显示中文
        public int Sex
        {
            get { return sex; }
            set 
            { 
            	sex = value; 
                OnPropertyChanged();
            }
        }

        //页面上要显示短时间
        public DateTime CrateTime
        {
            get { return crateTime; }
            set
            {
                crateTime = value;
                OnPropertyChanged();
            }
        }

        public UserViewModel()
        {
            userId = Guid.NewGuid().ToString();
            sex = 1;
            crateTime = DateTime.Now;
        }
    }

绑定后我们在页面中看到的是如下数据,与我们想要的不符,因此我们为此定义两个转换器。
在这里插入图片描述

    //ValueConversion表示指定该类的作用是由何种指定类型转至何种指定类型
    //(typeof(DateTime),typeof(string) ,表示该类是将DateTime转换成string 类型的转换器
    [ValueConversion(typeof(DateTime),typeof(string))]
    class DateConverter : IValueConverter
    {

        //表示将源数据的类型转化成目标属性的类型
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            DateTime date = (DateTime)value;
            return date.ToShortDateString();
        }

        //表示将目标属性的类型转化成 源数据的类型
        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            string strValue = value as string;
            DateTime resultDateTime;
            if (DateTime.TryParse(strValue, out resultDateTime))
            {
                return resultDateTime;
            }
            return DependencyProperty.UnsetValue;
        }
    }

    [ValueConversion(typeof(int), typeof(string))]
    class SexConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (value is int sex)
            {
                if (sex==0) return "保密";
                else if(sex==1) return "男";
                else return "女";
            }
            return DependencyProperty.UnsetValue;
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            string strValue = value as string;
            if (value is string sexStr)
            {
                if (sexStr.Equals("保密")) return 0;
                else if (sexStr.Equals("男")) return 1;
                else return 2;
            }
            return DependencyProperty.UnsetValue;
        }
    }

定义完转换器后,然后再窗口文件中将其添加为资源,然后绑定中引用该转换器即可。

    <UserControl.DataContext>
        <local:UserViewModel></local:UserViewModel>
    </UserControl.DataContext>
    <UserControl.Resources>
        <Style TargetType="TextBox">
            <Setter Property="Height" Value="50"></Setter>
            <Setter Property="VerticalContentAlignment" Value="Center"></Setter>
            <Setter Property="FontSize" Value="30"></Setter>
        </Style>

        <local:DateConverter x:Key="dataConverter"></local:DateConverter>
        <local:SexConverter x:Key="sexConverter"></local:SexConverter>
    </UserControl.Resources>
    <StackPanel Background="LightSteelBlue">
        <TextBox Background="LawnGreen" Text="{Binding CrateTime,Converter={StaticResource dataConverter}}" ></TextBox>
        <TextBox Background="LightBlue" Text="{Binding Sex,Converter={StaticResource sexConverter}}"></TextBox>
    </StackPanel>

在这里插入图片描述

另外我们可以在绑定的时候,通过ConverterParameter属性传入参数值,以供转换操作中使用
如: <TextBox Background="LawnGreen" Text="{Binding CrateTime,Converter={StaticResource dataConverter},ConverterParameter=测试}" ></TextBox>

特别是在我们绑定单选框RadioButton的IsChecked属性,使用这些属性作为性别,或者其他单选项的时候,由于两个单选框RadioButton的IsChecked属性 会绑定同一个bool 属性,这个场景下ConverterParameter就排上用场了,可以利用ConverterParameter对两个单选框做区分,如下所示:

<RadioButton GroupName="sex" 
IsChecked="{Binding IsMan,Converter={StaticResource sexConverter},ConverterParameter=1}"
Content="" Margin="10"></RadioButton>
<RadioButton GroupName="sex" 
IsChecked="{Binding IsMan,Converter={StaticResource sexConverter},ConverterParameter=0}"
Content="" Margin="10"></RadioButton>

    public class SexConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            //首先判断源数据,然后判断参数
            bool isMan = (bool)value;
            int flag =System.Convert.ToInt32(parameter);
            if (isMan) return flag == 1;
            else return flag == 0;
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            //首先判断目标数据,然后判断参数
            bool isMan = (bool)value;
            int flag = System.Convert.ToInt32(parameter);
            if (isMan) return flag == 1;
            else return flag == 0;
        }
    }

4 绑定的辅助属性

  • Delay
<TextBox Height="50" Text="{Binding Age,UpdateSourceTrigger=PropertyChanged,Delay=3000}"></TextBox>

Delay 属性用于指定在目标属性值更改后,绑定引擎应该等待多长时间才将更改传递回源属性。在给定的代码示例中,Delay 属性设置为 3000,这意味着当用户更改 TextBox 控件的文本时,绑定引擎将等待 3 秒钟,然后才将更改传递回 Age 属性。

这个延迟可以用于优化应用程序的性能,特别是当绑定的源属性在更改时会触发费时的操作(例如数据库查询或网络请求)时。通过延迟绑定引擎的更新,可以避免在短时间内进行多次操作,从而提高应用程序的响应速度。但是,需要注意的是,如果 Delay 值设置得太大,可能会导致用户界面的响应速度变慢或出现延迟,因此需要根据具体情况进行调整。

  • FallbackValue
<TextBox Height="50" Text="{Binding Age,UpdateSourceTrigger=PropertyChanged,FallbackValue=10}"></TextBox>

FallbackValue 属性用于指定绑定目标属性在无法从源属性中获取值时应采用的备用值。如果源属性的值为 null 或无效,则绑定目标属性将使用 FallbackValue 属性中指定的备用值。

在给定的代码示例中,如果 Age 属性的值无效或不存在,则 TextBox 控件的 Text 属性将设置为 10。这可以确保在出现绑定问题时,用户界面仍将显示一个默认值,而不是空白或空字符串。

  • TargetNullValue
<TextBox Height="50" Text="{Binding Age,UpdateSourceTrigger=PropertyChanged,TargetNullValue=10}"></TextBox>

TargetNullValue 属性用于指定绑定目标属性在源属性值为 null 时应采用的备用值。如果源属性的值为 null,则绑定目标属性将使用 TargetNullValue 属性中指定的备用值。

在给定的代码示例中,如果 Age 属性的值为 null,则 TextBox 控件的 Text 属性将设置为 10。这可以确保在出现绑定问题时,用户界面仍将显示一个默认值,而不是空白或空字符串。

需要注意的是,TargetNullValue 属性只有在源属性的值为 null 时才会生效。如果源属性的值为其他类型的空值(例如空字符串或空集合),则 TargetNullValue 属性不会生效。如果需要处理这些情况,可以考虑使用其他属性,例如 FallbackValue 或 StringFormat。


总结

以上就是本文的内容,本文介绍了依赖属性,附加属性和数据绑定,数据转换的使用,希望以上内容可以帮助到你,如文中有不对之处,还请批评指正。


参考:
官方文档-数据
依赖属性,附加属性

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐