依赖属性

依赖属性

当你开始用WPF开发应用程序时,你很快会被依赖属性所牵绊。它看起来与普通的.NET属性一样,但是这个概念背后是更加复杂与强大的。
这主要的不同是,普通的.NET属性的值是从你的类的私有成员中直接读取的。反之,当你调用从DependencyObject继承的GetValue方法时,依赖属性的值是动态地解析。
当你给一个依赖属性赋值时,它不是存储在你的对象的字段中,而是存储在基类DependencyObject提供的键值对的字典中。实体的键就是这个属性的名字,值就是你想设置的值。
依赖属性的优势:

  • 减少内存占用
    当你认为超过90%的UI控件通常保持它的默认值,存储每个属性的值这是一个巨大的浪费。依赖属性解决了这个问题,通过仅仅存储实例中已更改的属性。默认值被一次性存储在依赖属性中。
  • 值继承
    当你访问依赖属性时,属性的值通过值解析策略解析。当你在根元素上设置FontSize属性时,他将会应用在下面的所有的文本块,除非你覆盖这个值。
  • 更改通知
    依赖属性有内置的更改通知机制。通过这个属性的元数据注册一个回调,当属性值被改变后你会收到一个通知。这也被数据绑定使用。

值解析策略

每次你访问一个依赖属性,它内部遵循从高到低的优先解析值。它检查本地值是否可用,如果不是则检查自定义样式触发器是否可用,...,持续查找直到找到值。最后,默认值总是可用。
解析值的顺序:
GetValue

  • 动画
  • 绑定表达式
  • 本地值
  • 自定义样式触发器
  • 自定义模板触发器
  • 默认样式触发器
  • 默认样式Setter
  • 继承值
  • 默认值

SetValue

  • 本地值

它背后的魔力

每个WPF控件都注册了一组DependencyProperty到静态类DepencyProperty。它们的每一个都由一个键(每个类型必须唯一)和包含回调和默认值的元数据组成。
所有想要使用依赖属性的类必须继承自DependencyObject类。这个基类定义了一个键值对字典,它包含了依赖属性的本地值。实体的键是已定义的依赖属性的名字。
当你通过.NET属性包装器访问依赖属性,它内部调用依赖属性的GetValue方法来访问值。这个方法通过使用下面详述的值解析策略解析值。如果本地值可用,它将会直接地从字典中读取值。如果没有值,则将从逻辑树向上移动查找,将其作为继承值。如果没有值,那么将会从已定义的属性元数据中得到默认值。

  • 从依赖对象的字典中查找本地值
  • 向上访问逻辑树,查找继承值
  • 在依赖属性中查找默认值

创建依赖属性

为了创建依赖属性,需要向你的类型中添加一个DepencyProperty类型的静态字段,然后调用DependencyProperty.Register()方法创建依赖属性的实例。依赖属性的名字总是以Property结尾,这是WPF的命名约定。
为了创建一个正常的可访问的.NET属性,你需要添加一个属性包装器。这个包装器内部只能通过从DependencyObject继承的SetValue和GetValue方法来设置和获取值,并且需要传递依赖属性实例作为键传递。
重要:不要添加任何逻辑到属性包装器中,因为当你从代码中设置值时它才会被调用。如果你从XAML中设置这个属性,依赖对象的SetValue将会直接地被调用。
如果你使用VS,你可以输入propdp然后点击两下tab键来快速地创建依赖属性。

public static readonly DependencyProperty CurrentTimeProperty = DependencyProperty.Register("CurrentTime",typeof(DateTime),typeof(MyClockControl),new FrameworkPropertyMetadata(DataTime.Now));

public DateTime CurrentTime
{
    get { return (DateTime)GetValue(CurrentTimeProperty); }
    set { SetValue(CurrentTimeProperty); }
}

每个依赖属性提供回调实现更改通知、值强制和验证。这些回调在依赖属性中被注册。

new FrameworkPropertyMetadata(DateTime.Now, OnCurrentTimePropertyChange,OnValidateCurrentTimeProperty);

值更改回调
更改通知回调是一个静态方法,每次当TimeProperty的值被改变时调用。新值通过事件参数传递,值发生改变的对象将作为源传递。

private static void OnCurrentTimePropertyChanged(DependencyObject source, 
        DependencyPropertyChangedEventArgs e)
{
    MyClockControl control = source as MyClockControl;
    DateTime time = (DateTime)e.NewValue;
    // Put some update logic here...
}

强制值回调
强制值回调允许你调整超出边界的值在不抛出异常的情况下。一个好的例子是带有上限或者下限值进度条。在这个例子中我们可以强制值在允许的范围内。在下面这个例子中,我们限制时间在过去

private static object OnCoerceTimeProperty( DependencyObject sender, object data )
{
    if ((DateTime)data > DateTime.Now )
    {
        data = DateTime.Now;
    }
    return data;
}

验证回调
在验证回调你检查设置的值是否有效。如果返回false,一个参数异常将会被抛出。在我们的例子需求中,数据是DateTime的实例。

private static bool OnValidateTimeProperty(object data)
{
    return data is DateTime;
}

只读依赖属性

WPF中许多控件的依赖属性是只读的。它们通常被用作报告控件的状态,像是IsmouseOver属性。为该值提供Setter是没有意义的。
也许你问你自己,为什么我们不使用一个普通的.NET属性?一个重要的原因是你无法在普通的.NET属性上设置触发器。
创建一个只读的属性与创建一个正常的依赖属性是相似的。替代DependencyProperty.Register()的是DependencyProperty.RegisterReadonly()。它返回一个DependencyPopertyKey。这个Key应该被存储在你的类中的一个私有或者受保护的静态只读字段。这个Key允许你在你的类中设置它的值,就像普通的依赖属性一样。(允许在类中设置,但是不允许在XAML中设置。这样做是因为XAML会直接调用SetValue方法,不走.NET属性包装器)
第二件事情是注册一个公共依赖属性,被指派给DependencyPropertyKey。这个属性从外部访问时是只读的。

//注册依赖属性Key
private static readonly DependencyPropertyKey IsMouseOverPropertyKey = DependencyProperty.RegisterReadOnly("IsMouseOver",typeof(bool),typeof(MyClass),new FrameworkPropertyMetadata(false));

//注册依赖属性
private static readonly DependencyPoperty IsMouseOverProperty = IsMouseOverPropertyKey.DependencyProperty;

//.NET属性包装器
public int IsMouseOver
{
    get { return (bool)GetValue(IsMouseoverProperty); }
    set { SetValue(IsMouseOverPropertyKey, value) }
}

附加属性

附加属性是特别的依赖属性。它允许你附加一个值到一个对象,而这个对象对该值一无所知(也就是不用修改源代码?)。
对于这个概念一个非常好的例子是布局面板。每个布局面板需要不同的数据来对齐它们的孩子元素。Canvas需要Top和Left,DockPanel需要Dock,诸如此类。当你写你自己的布局面板,这个列表是无限的。所以你看,在WPF控件中包含布局面板所有的所需的属性是不可能的。
解决方案是附加属性。控件所需的数据由另一个来自特定上下文的控件所提供。举个例子,被父布局面板对齐的元素。
为了设置附加属性的值,在XAML中添加一个带有提供附加属性元素前缀的属性。若要设置在Canvas面板内对齐的按钮的Canvas.Top和Canvas.Left属性,请像下面这样写:

<Canvas>
    <Button Canvas.Top="20" Canvas.Left="20" Content="Click me!"/>
</Canvas>
public static readonly DependcyProperty TopProperty = DependencyProperty.RegisterAttached("Top",typeof(double),typeof(Canvas),new FrameworkPopertyMetadata(0d,FrameworkPropertyMetadataOptions.Inherits));

public static void SetTop(UIElement element, double value)
{
    element.SetValue(TopPorperty, value)
}

public static double GetTop(UIElement element)
{
    return (double)element.GetValue(TopProperty);
}

监听依赖属性变化

如果你想要监听依赖属性的变化,你可以继承定义这个属性的类,然后重写属性元数据并传递PropertyChangedCallback回调。但是一种更轻松的方式是获取DependencyPropertyDescriptor并通过调用AddValueChanged方法连接一个回调。

DependencyPropertyDescriptor textDescr = DependencyPopertyDescriptor.FromProperty(TextBox.TextPoperty, typeof(TextBox));
if(textDescr != null)
{
    textDescr.AddValueChanged(myTextBox, delegate
    {
        //在这添加你的属性变化逻辑
    });
}

如何清空本地值

因为空也是一个有效的本地值,有一个常量DependencyProperty.UnsetValue描述一个未设置的值。

DependencyObject基类提供的ClearValue方法,参数为要清空的属性
button1.ClearValue(Button.ContentProperty);
posted @ 2023-07-15 13:48  Juston007  阅读(28)  评论(0)    收藏  举报