前言

在我尝试写一部分 WPF 元素绑定的相关内容的时候,写到开头发现我不得不先解释说明一下依赖项属性的相关内容,遂停止元素绑定内容的记录,补写依赖项属性的相关内容。

理解依赖项属性

属性和事件是.NET抽象模型的核心部分,如果你懂得 VB 或者 WinForm 相关开发的话会十分了解这点。但是对于 WPF 来说,它在传统的属性基础上做了一层封装,将它变成了更加高级的依赖项属性。同时也不会和传统的属性发生冲突。

这种更高级的依赖项属性使用了效率更高的保存机制,并支持附加功能,例如:更高通知以及属性值的继承。

依赖项属性是.NET属性的新实现,在 WPF 技术特性中,是及其依赖于依赖项属性的实现,虽然在使用过程中,感觉和普通的属性一致。

定义依赖项属性

绝大部分的微软提供的属性都是依赖项属性,但是在某些情况下,我们需要自定义我们自己的依赖项属性,比如我们自定义的控件。

定义依赖项属性

需要注意的是:**自定义的依赖项属性的类,必须继承于DependencyObject**,因为后续需要使用该类的方法来修改依赖项属性的值。代码示例:

1
2
3
4
5
6
// 自定义了 Test 类,该类继承于 DependencyObject
class Test :DependencyObject
{
//自定义依赖项属性 InfoProperty
public static readonly DependencyProperty InfoProperty;
}

创建依赖项属性的命名规则是最后加上Property来表示其为依赖项属性

注册依赖项属性

定义好依赖项属性后需要注册依赖项属性来使用该依赖项,注册依赖项需要两个步骤,第一个步骤是创建FrameworkPropertyMetadata对象来决定依赖项的一些基础属性,然后创建DependencyProperty对象来实现依赖项属性的初始化。

因为我们需要使用依赖项属性,所以依赖项属性的注册必须在使用代码之前注册,所以需要在静态构造函数中定义。又因为为了确保DependencyProperty,即依赖项属性不会在后续的代码中被人更改,所以使用的是readonly关键字。

同样的,WPF 确保DependencyProperty不会被直接实例化,所以必须通过静态的DependencyProperty.Register来生成实例对象;代码示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Test :DependencyObject
{
//定义依赖项属性
public static readonly DependencyProperty InfoProperty;

//初始化注册依赖项属性
static Test()
{
//初始化依赖项属性的相关参数和功能
FrameworkPropertyMetadata metadata = new FrameworkPropertyMetadata("这是默认值哦");

//定义依赖项属性对象
InfoProperty = DependencyProperty.Register("Anfo", typeof(string), typeof(Info),metadata);
}
}

FrameworkPropertyMetadata 对象

该对象规定了所注册依赖项的默认初始值,以及所支持的相关服务(布局影响,数据绑定等),它的构造函数有多个重载,如下所示:

1
2
public FrameworkPropertyMetadata(object defaultValue);
public FrameworkPropertyMetadata(object defaultValue, FrameworkPropertyMetadataOptions flags, PropertyChangedCallback propertyChangedCallback, CoerceValueCallback coerceValueCallback);
  • defaultValue:该依赖项属性的默认初始值

  • FrameworkPropertyMetadataOptions:该依赖项属性的所支持的服务功能,参数如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    // 摘要:
    // 未指定任何选项;依赖属性使用 Windows Presentation Foundation (WPF) 属性系统的默认行为。
    None = 0,
    //
    // 摘要:
    // 更改此依赖属性的值会影响布局组合的测量过程。
    AffectsMeasure = 1,
    //
    // 摘要:
    // 更改此依赖属性的值会影响布局组合的排列过程。
    AffectsArrange = 2,
    //
    // 摘要:
    // 更改此依赖属性的值会影响父元素上的测量过程。
    AffectsParentMeasure = 4,
    //
    // 摘要:
    // 更改此依赖属性的值会影响父元素上的排列过程。
    AffectsParentArrange = 8,
    //
    // 摘要:
    // 更改此依赖属性的值会影响呈现或布局组合的某一方面(不是测量或排列过程)。
    AffectsRender = 16,
    //
    // 摘要:
    // 此依赖属性的值将由子元素继承。
    Inherits = 32,
    //
    // 摘要:
    // 此依赖属性的值跨越分隔的树以实现属性值继承。
    OverridesInheritanceBehavior = 64,
    //
    // 摘要:
    // 不允许将数据绑定到此依赖属性。
    NotDataBindable = 128,
    //
    // 摘要:
    // 此依赖属性上的数据绑定的 System.Windows.Data.BindingMode 默认为 System.Windows.Data.BindingMode.TwoWay。
    BindsTwoWayByDefault = 256,
    //
    // 摘要:
    // 此依赖属性的值应由日记记录进程或在由 Uniform resource identifiers (URIs) 导航时进行保存或存储。
    Journal = 1024,
    //
    // 摘要:
    // 此依赖属性值上的子属性不会影响呈现的任何方面。
    SubPropertiesDoNotAffectRender = 2048

    可以传入参数名称,也可以传入参数对应的数值

  • CoerceValueCallback:该方法在validateValueCallback后执行,可以对依赖项属性进行验证,详细

    可以查看下面的属性验证部分内容。

    validateValueCallbackDependencyProperty类型的方法

  • propertyChangedCallback:当属性值发生改变时会调用

DependencyProperty 对象

DependencyProperty对象的实例化需要通过静态方法DependencyProperty.Register来实现,该方法同样有多种重载,原型示例:

1
public static DependencyProperty Register(string name, Type propertyType, Type ownerType, PropertyMetadata typeMetadata, ValidateValueCallback validateValueCallback);
  • name:你所定义的依赖项属性名称,该名称只会影响该对象的Name属性,不会对代码产生实际影响
  • propertyType:该依赖项属性的类型,即该依赖项属性的类型
  • ownerType:使用该依赖项属性的类型,即判断该类型对象是否支持该属性
  • validateValueCallback详细查看下面的属性验证部分

添加属性包装器

在完成定义和注册依赖项属性后,需要将其包装成普通的属性,这样可以做到和普通属性一样的调用,即将其包装成传统的.NET属性,这样就完成依赖项属性的最后一步。代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Test :DependencyObject
{
public static readonly DependencyProperty InfoProperty;

static Test()
{
FrameworkPropertyMetadata metadata = new FrameworkPropertyMetadata("这是默认值哦");

InfoProperty = DependencyProperty.Register("Anfo", typeof(string), typeof(Info),metadata);
}

public String Info
{
set { SetValue(InfoProperty, value); }
get { return (String)GetValue(InfoProperty); }
}
}

现在就以及拥有了一个功能完备的依赖项属性了,可以像使用其他依赖项属性一样使用它。

另外,需要注意的是,依赖项属性值的确定是根据优先规则来确定的,即使你并没有直接设置它的值,它也可能通过数值绑定,样式等提供获取,也可能是元素树中继承而来。如果你希望删除本地值设置,像从来没有设置过一样,需要使用另一个继承自DependencyProperty的方法ClearValue()来实现

WPF 使用依赖项属性的方式

WPF 的许多行为功能都需要使用依赖项属性,所有的功能都是通过每个依赖项属性都支持的两个关键行为进行工作的——更改通知和动态值识别

更改通知

即当属性值发生变化时所进行的相关操作

当属性值发生变化时,依赖项属性会触发受保护的名为OnPropertyChangedCallback()的方法,该方法通过了数据绑定和触发器来传递信息,并调用PropertyChangedCallback()回调方法。

换句话所,当属性发生变化时,如果希望进行响应,有两种选择:

  • 使用属性值创建绑定
  • 使用触发器

但是对于通用方法触发一些代码,WPF 处于性能考虑并没有给出,相反的可以将代码定义在回调函数PropertyChangedCallback()来替代实现。

动态值识别

即判定依赖项属性值的赋值问题。

本质上依赖项属性,依赖于多个对象来获取属性值,每个提供者的优先级不同。WPF 会通过一系列检索,获取最终的属性值,相关检索因素如下(优先级从低到高):

  1. 默认值(即注册依赖项属性的时候FrameworkPropertyMetadata对象定义的初始值)
  2. 继承了原来的值
  3. 来自主题样式的值
  4. 来自项目样式的值
  5. 本地值(通过 XAML 或者 CS代码直接设置的值)

如上所示,通过设置优先级高的依赖项提供者,来改变依赖项属性的值。

这样做的好处是可以节省内存,;例如:对于一个窗口的多个Button控件,如果每个Button都可以使用主题或者其父级的样式,就可以少存储一份单独的样式值

当然,在进行上述的值确定后,还需要考虑其他的可改变属性值的提供者,总结来说,WPF 确定属性值的步骤如下:

  1. 确定基本值(即上面的步骤)

  2. 如果属性使用的表达式设置,则对表达式进行求值。

    WPF 支持两种表达式:数据绑定和资源

  3. 如果属性是动画的目标,则应用动画

  4. 运行CoerceValueCallBack回调函数来修正属性值

共享依赖项属性

尽管一些类具有不同的继承层次,但是可以通过DependencyProperty.AddOwner()来共享同一个依赖项属性。

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class Apple :DependencyObject
{
public static readonly DependencyProperty SizeProperty;

static Apple()
{
FrameworkPropertyMetadata metadata = new FrameworkPropertyMetadata("这是默认值哦");

SizeProperty = DependencyProperty.Register("Size", typeof(string), typeof(Apple),metadata);
TextBlock textBlock = new TextBlock();
Control control = new Control();
}

public String Info
{
set { SetValue(SizeProperty, value); }
get { return (String)GetValue(SizeProperty); }
}
}
class Car : DependencyObject
{
public readonly static DependencyProperty SizeProperty;
static Car()
{
//共享Apple的依赖项属性
SizeProperty = Apple.SizeProperty.AddOwner(typeof(Car));
}
public String Size
{
set { SetValue(SizeProperty, value); }
get { return (String)GetValue(SizeProperty); }
}
}

附加的依赖项属性

附加属性被应用到的类并非定义附加属性的类,例如,Grid类定义的RowColumn属性是在其嵌套的元素中应用的。

定义附加属性,需要使用RegisterAttached()方法,而不是Register()方法。代码示例:

1
2
3
4
5
6
7
static Apple()
{
FrameworkPropertyMetadata metadata = new FrameworkPropertyMetadata("这是默认值哦");

//注册依赖项属性
SizeProperty = DependencyProperty.RegisterAttached("Size", typeof(string), typeof(Apple),metadata);
}

与普通依赖属性一样,可以设置其回调函数。

当创建附加属性时,不需要定义.NET属性封装器,这是因为附加属性可以被用于任何依赖对象。例如:Grid.Row属性可以被用在任何对象上。

不使用.NET属性封装器,反而附加属性因为需要满足被所有对象调用,所以需要两个静态方法来设置和获取属性值,这样来替代属性封装器。代码示例:

1
2
3
4
5
6
7
8
9
10
//获取值
public static string GetValue(Car car)
{
return (string)car.GetValue(Apple.SizeProperty);
}
//设置值
public static void SetValue(Car car,string value)
{
car.SetValue(Apple.SizeProperty,value);
}

当然你也可以通过直接使用GetValueSetValue方法来绕过这两个静态方法直接获取和设置值,代码示例:

1
car2.SetValue(Apple.SizeProperty, "这是car2的属性值");

以上示例完整CS代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
class Apple :DependencyObject
{
//声明附加属性
public static readonly DependencyProperty SizeProperty;
//初始化对象
static Apple()
{
//设置附加属性的相关服务
FrameworkPropertyMetadata metadata = new FrameworkPropertyMetadata("这是默认值哦");
//注册附加属性
SizeProperty = DependencyProperty.RegisterAttached("Size", typeof(string), typeof(Apple),metadata);
}
//附加属性获取方法
public static string GetValue(Car car)
{
return (string)car.GetValue(Apple.SizeProperty);
}
//附加属性设置方法
public static void SetValue(Car car,string value)
{
car.SetValue(Apple.SizeProperty,value);
}
}
class Car : DependencyObject
{
//空内容,只是单独表示一个要使用附加属性但自己没定义该附加属性的类
}





//这里是调用验证附加属性的过程,我采用的是WPF的项目,写在了窗口初始化的事件里面
private void Window_Initialized(object sender, EventArgs e)
{
//定义两个Car对象
Car car1 = new Car();
Car car2 = new Car();
//查看两个对象附加属性的初始值
Console.WriteLine(Apple.GetValue(car1)+ Apple.GetValue(car2));
//通过定义的附加属性静态方法设置 car1 对象的附加属性值
Apple.SetValue(car1, "这是car1的属性值");
//查看两个对象现在的附加属性初始值
Console.WriteLine(Apple.GetValue(car1)+ Apple.GetValue(car2));
//采用SetValue直接绕过定义的静态方法修改附加属性的值
car2.SetValue(Apple.SizeProperty, "这是car2的属性值");
//查看两个对象的附加属性初始值
Console.WriteLine(Apple.GetValue(car1) + Apple.GetValue(car2));
}

输出结果:

image-20220621123310797

属性验证

在定义任何类型的属性的时候,都需要面对错误设置属性的可能性。对于传统的.NET可以直接在属性设置器中捕获验证这些问题。但是对于依赖项属性来说,它是通过静态方法SetValue来设置的,所以并不适用。

不过,WPF 对此提供了两种替代方法来阻止非法值

  • ValidateValueCallback:该回调函数可接受或拒绝新值。通常,该回调函数用于捕获违反属性约束的明显错误。作为DependencyProperty.Register()方法的一个参数。
  • CoerceValueCallback:该回调函数可将新值修改为更能被接受的值。该回调函数通常用于处理为相同对象设置的依赖项属性值相互冲突的问题。这些值本身可能是合法的,但是同时应用时它们时是相互冲突的。作为FrameworkPropertyMetadata对象的参数。

当应用程序试图更改设置依赖项属性时,如下时这些内容的作用过程:

  1. 首先,CoerceValueCallback方法有机会修改提供的值(通常使提供的值和其他属性值相容,即做一层映射),或者返回DependencyProperty.UnsetValue,来拒绝修改。【强制回调】
  2. 接下来激活ValidateValueCallback方法,该方法返回true接受一个值作为合法值,或者返回false来拒绝值。与上面方法不同的是,该方法不能访问设置属性的实际对象,这意味着不能检查其他属性。【验证回调】
  3. 最后,**如果上面两个阶段都成功,则触发PropertyChangedCallback**。

通俗来所,对于需要映射来兼容其他方法参数或者需要对参数进行约束,例如:对于ScrollBar有最大值和最小值,则需要通过CoerceValueCallback方法来判断最大值是否小于最小值做一次冲突验证。而对于Margin属性来说,如果传入参数为-1,则可以通过ValidateValueCallback来验证传入参数是否合法。最后两个都通过了,则触发PropertyChangedCallback

End

如果你只是想使用依赖项属性,则只需要查看如何定义注册使用,比葫芦画瓢即可,如果你想要了解它的工作原理,则需要理解去了解。