WPF进阶技巧和实战08-依赖属性与绑定01

依赖项属性

定义依赖项属性

注意:只能为依赖对象(继承自DependencyObject的类)添加依赖项属性。WPF中的元素基本上都继承自DependencyObject类。

  • 静态字段
  • 名称约定(属性末尾加上Property)
  • Readonly(只能在静态构造函数中进行设置)
public static readonly DependencyProperty ScalingRatioProperty = 
DependencyProperty.Register("ScalingRatio", 
typeof(double), 
typeof(FundusCanvas), 
new FrameworkPropertyMetadata(1.0));

注册依赖项属性

为了确保DependencyProperty对象不被直接实例化,使能使用静态的DependencyProperty.Register方法创建依赖项属性实例,而且创建后不能被改变,所以使用只读属性。创建过程中需要提供的几个要素:

  • 属性名(ScalingRatio)
  • 属性使用的数据类型(double)
  • 拥有该属性的类型(FundusCanvas)
  • 一个具有附加属性设置的FrameworkPropertyMetadata对象(可选)
  • 一个用于验证属性的回调函数(可选)

使用FrameworkPropertyMetadata对象和属性验证回调可以更加丰富依赖项属性,创建依赖项属性的附加功能。FrameworkPropertyMetadata类的所有属性:

名称 说明
AffectsArrange
AffectsMeasure
AffectsParentArrange
AffectsParentMeasure
如果为true,依赖项属性会影响在布局操作的测量过程和排列过程中如何放置相邻的元素或者父元素。如果属性值发生变化,那么布局容器需要重新执行测量步骤以确定新的布局
AffectsRender 如果为true,依赖项属性会对元素的绘制方式造成一定的影响,要求重新绘制元素
BindsTwoWayByDefault 如果为true,默认情况下,依赖项属性将使用双向数据绑定而不是单向数据绑定。不过,当创建数据绑定时,可以明确指定所需的绑定行为
Inherits 如果为true,就通过元素树传播该依赖项属性值,并且可以被嵌套的元素继承。例如:Font是可继承的依赖项属性,如果在更高层次的元素中为Font属性设置了值,那么该属性值就会被嵌套的元素继承,除非手动设置来覆盖继承来的值
IsAnimationProhibited 如果为true,就不能将依赖项属性用于动画
IaNoDataBindable 如果为true,就不能使用绑定表达式设置依赖项属性
Journal 如果为true,基于页面的应用程序中,依赖项属性将被保存到日志中(浏览过的页面历史记录)
SubPropertiesDoNotAffectRender 如果为true,并且对象的某个子属性(属性的属性)发生了变化,WPF将不会重新渲染该对象
DefaultUpdateSourceTrigger 当该属性用于绑定表达式时,该属性用于为Binding.UpdateSourceTrigger属性的默认值。UpdateSourceTrigger属性决定了数据绑定值在何时应用自身的变化。当创建绑定时,可手动设置UpdateSourceTrigger属性
DefaultValue 该属性用于为依赖项属性设置默认值
CoerceValueCallback 该属性提供一个回调函数,用于验证依赖项属性之前尝试“纠正”属性值
PropertyChangedCallback 该属性提供一个回调函数,当依赖项属性的值发生变化时调用该函数

添加属性包装器

public FundusEditMode EditMode 
{ 
get => (FundusEditMode)GetValue(EditModeProperty); 
set => SetValue(EditModeProperty, value); 
}

当创建属性封装器时,应当值包含对SetValue和GetValue方法的调用,不应当添加任何验证属性的额外代码,引发事件的代码等。WPF提供了用于进行这些工作的地方-使用依赖项属性回调函数。应该通过前面介绍的ValidateValueCallback回调函数进行验证操作。
依赖项属性遵循严格的优先规则来确定他们的当前值。即使没有直接设置依赖项属性,也可能已经有了值(可能是由数据绑定、样式、或者动画提供的,也可能是继承来的),不过,主要直接设置了属性值,设置的属性值就会覆盖所有其他的影响。

可能希望删除本地值设置,并像从来没有设置过那样确定属性值。显然不能够通过设置新值来实现,反而需要使用方法ClearValue()。

使用依赖项属性

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

  • 更改通知

当属性值发生变化时,依赖项属性不会自动引发事件以通知属性值发生了变化。相反,他们会触发受保护的名为OnPropertyChangedCallback()的方法。该方法通过两个WPF服务(数据绑定和触发器)传递信息,并调用PropertyChangedCallback回调函数。也就是说,当属性变化时,如果希望进行相应,有两种选择–可使用属性创建绑定,也可以编写能够自动改变其他属性或者开始动画的触发器。但是依赖项属性没有提供一种通用的方法触发一些代码,进而对属性的变化进行相应。

  • 动态值识别

本质上,依赖项属性依赖于多个属性提供者,每个提供者都有各自的优先级。当从属性检索值事,WPF属性系统会通过一系列步骤获取最终值。首先通过考虑以下因素(优先级从低到高)来决定基本值。

  1. 默认值(有FrameworkPropertyMetadata对象设置的值)
  2. 继承而来的值(假设设置了FrameworkPropertyMetadata.Inherits标志,并为包含层次中的某个元素提供了值)
  3. 来自主题样式的值
  4. 来自项目样式的值
  5. 本地值(使用代码或者XAML直接为对象设置的值)

可通过直接应用一个值来覆盖整个层次,否则属性值可通过上面5中可用项来确定。

WPF按照上面的5种情况去确定依赖项属性的基本值。但是基本值未必就是最后从属性中检索到的值。这是因为WPF还需要考虑其他几个可能改变属性值的提供者:

  1. 确定基本值(按照上述步骤)
  2. 如果属性是使用表达式设置的,就对表达式进行求值(数据绑定或者资源)
  3. 如果属性是动画,就应用动画
  4. 运行CoerceValueCallback回调函数来修正属性值

共享依赖项属性

尽管一些类具有不同的继承层次,但他们会共享同一依赖项属性。

public static readonly DependencyProperty FrontContentProperty = 
FlipPanel.FrontContentProperty.AddOwner(typeof(FlipPanel2));

可以使用相同的技术创建自己的自定义类(假定继承的父类没有提供属性,否则直接重写即可),还可以使用重载的AddOwner方法来提供验证回调函数以及应用依赖项属性的新方法的FrameworkPropertyMetadata对象。

附加的依赖项属性

public static readonly DependencyProperty FrontContent1Property = 
DependencyProperty.RegisterAttached("FrontContent", typeof(object), typeof(FlipPanel2), null);

附加属性是一种依赖项属性,由WPF属性管理器管理,不同之处在于附加属性被应用到的类并非定义附加属性的那个类。例如Grid.Row属性是应用在Grid中定义,但是应用于其他控件。

属性验证

WPF提供了两种方法类阻止非法值:

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

应用程序试图设置依赖项属性时,所有这些内容的作用过程:

  1. 首先,CoerceValueCallback方法有机会修改提供的值(通常,使提供的值和其他属性相容),或者返回DependencyProperty.UnsetValue,这会拒绝修改
  2. 接下来激活ValidateValueCallback方法,该方法返回true以接受一个值作为合法值,或者返回false拒绝值。与CoerceValueCallback不同,本方法不能访问设置属性的实际对象,这意味着不能检查其他属性值。
  3. 最后,如果前两个阶段获得成功,就会触发PropertyChangedCallback方法

验证回调(ValidateValueCallback)

可使用回调函数加强验证,验证通常应被添加到属性过程的设置部分。提供的回调函数必须指向一个接受对象参数并返回bool值的方法,如果返回true来接受对象是合法的,否则返回false拒绝对象。

对于验证回调函数有一个限制,他们必须是静态方法而且无法访问正在被验证的对象,所有能够获得信息就是刚刚应用的数值。

public static readonly DependencyProperty CornerRadiusProperty =
    DependencyProperty.Register("CornerRadius", typeof(CornerRadius),
        typeof(FlipPanel), null, new ValidateValueCallback(CornerRadiusValidate));

private static bool CornerRadiusValidate(object value)
{
    CornerRadius cornerRadius = (CornerRadius)value;
    if (cornerRadius.TopLeft == double.NaN ||
        cornerRadius.TopRight == double.NaN ||
        cornerRadius.BottomLeft == double.NaN ||
        cornerRadius.BottomRight == double.NaN)
        return false;
    return true;
}

强制回调(CoerceValueCallback)

通过设置CoerceValueCallback回调函数处理相互关联的属性。设置本属性的时候,需要处理好本属性和其他属性之间的关系,根据处理结果返回响应的值。

public CornerRadius CornerRadius
{
    get => (CornerRadius)GetValue(CornerRadiusProperty);
    set => SetValue(CornerRadiusProperty, value);
}
public static readonly DependencyProperty CornerRadiusProperty =
    DependencyProperty.Register("CornerRadius", typeof(CornerRadius),
        typeof(FlipPanel), new FrameworkPropertyMetadata(new CornerRadius(5, 5, 5, 5),
            FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
            propertyChangedCallback: CornerRadiusChangedCallback,
            coerceValueCallback: CornerRadiusCoerceValueCallback),
        new ValidateValueCallback(CornerRadiusValidate));

private static bool CornerRadiusValidate(object value)
{
    CornerRadius cornerRadius = (CornerRadius)value;
    if (cornerRadius.TopLeft == double.NaN ||
        cornerRadius.TopRight == double.NaN ||
        cornerRadius.BottomLeft == double.NaN ||
        cornerRadius.BottomRight == double.NaN)
        return false;
    return true;
}

private static object CornerRadiusCoerceValueCallback(DependencyObject d, object value)
{
    CornerRadius cornerRadius = (CornerRadius)value;
    if (cornerRadius.TopLeft == double.NaN ||
        cornerRadius.TopRight == double.NaN ||
        cornerRadius.BottomLeft == double.NaN ||
        cornerRadius.BottomRight == double.NaN)
        return new CornerRadius(5, 5, 5, 5);
    return cornerRadius;
}

private static void CornerRadiusChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{

}