65.9K
CodeProject 正在变化。 阅读更多。
Home

WPF 中的依赖属性是如何存储的

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.90/5 (20投票s)

2016 年 12 月 30 日

CPOL

7分钟阅读

viewsIcon

33666

downloadIcon

294

本文深入概述了 WPF 属性系统中依赖属性的存储方式。

引言

在工作中,有人问我一个问题:我们知道 WPF 中的依赖属性通常是 public staticreadonly 的,但每个拥有者对象的实例可以同时拥有各自的值。这与我们对 static CLR 属性的理解不符。在本文中,我们将首先研究 WPF 的几个类来回答这个问题。在讨论过程中,它引发了更多的问题,我们将在下一篇文章中进行探讨。

注意:为方便书写和阅读,下文将使用缩写“DP”代替“Dependency Property”。对 WPF 框架有基本了解是本文的前提。

Outline

  • 常规 DP 创建代码
  • Register 方法内部发生了什么
  • DP 的 getter 是如何工作的
  • DP 的 setter 是如何工作的
  • WPF 属性系统如何减少内存占用

常规 DP 创建代码

本文中,在附带的演示代码中,我们使用了两个 TextBox,并首先关注 TextBox 类的“MinLines”DP。在 MainWindow.xaml 中,我们创建了两个 TextBox 实例,命名为 TextBoxOne 的对象将“MinLines”DP 的本地值设置为 2,而命名为 TextBoxTwo 的对象将其设置为 4。当我们运行应用程序时,这两个 TextBox 根据其本地值,对于 MinLines DP 拥有不同的值。

为了理解 DP 的工作原理,我们将以内置的“MinLines”DP 为例,看看本地值是如何存储的。在深入研究 DP 及其本地值如何存储之前,让我们先看看 TextBox 类是如何定义“MinLines”DP 的。以下代码摘自 TextBox 的参考代码,可在 此处 获取。

/// <summary>
/// Dependency ID for the MinLines property
/// Default value: 1
/// </summary>
public static readonly DependencyProperty MinLinesProperty =
                DependencyProperty.Register(
                    "MinLines", // Property name
                    typeof(int), // Property type
                    typeof(TextBox), // Property owner
                    new FrameworkPropertyMetadata(
                        1,
                        FrameworkPropertyMetadataOptions.AffectsMeasure,
                        new PropertyChangedCallback(OnMinMaxChanged)),
                    new ValidateValueCallback(MinLinesValidateValue));
 
/// <summary>
/// Minimum number of lines to size to.
/// </summary>
[DefaultValue(1)]
public int MinLines
{
    get { return (int) GetValue(MinLinesProperty); }
    set { SetValue(MinLinesProperty, value); }
}

仅仅通过查看上面的代码,我们就可以注意到与普通的 CLR 属性有很多不同之处

  • 在创建 DP 时,我们只需调用 DependencyProperty 类的 static 方法 Register
  • 变量 MinLinesPropertypublic static readonly 的,它被称为 DP 的标识符。
  • DP 的 getter 调用了 GetValue 方法,并进行了 int 类型转换。
  • DP 的 setter 也调用了 SetValue 方法。
  • 与我们为 CLR 属性所习惯的,这里没有后备的 private 字段。

并且我们在创建 DP 时调用方法进行注册,而在获取或设置 DP 值时调用方法。DP 如何工作和 *存储其值* 的机制隐藏在 WPF 属性系统中。接下来,我们将仔细研究调用这些方法时会发生什么。

Register 方法内部发生了什么

下面是 DependencyPropertyRegister 方法 的简要工作流程

  • 在创建 DP 时,您调用 DependencyProperty 类的 static 方法 Register 并传入参数。
  • Register 方法验证元数据(如果提供),然后调用以下方法:

    DependencyProperty property = RegisterCommon(name, propertyType, ownerType, defaultMetadata, validateValueCallback);

  • RegisterCommon 方法内部,首先,使用依赖属性的 Name 和其拥有者类型创建一个 DP 的键。
  • 一个名为 FromNameKey 的类将被用作 KeyKey 维护 DP 的整数 Hashcode。它重写了 GetHashCode 方法,并基于 DP 名称及其 OwnerType 生成 HashCode
  • DependencyProperty 类有一个名为 PropertyFromNameprivate HashTable。然后 RegisterCommon 方法检查名为 PropertyFromName 的哈希表是否不包含该 Key
  • 然后,如果未提供元数据,则会创建默认元数据。
  • 然后,使用 new 关键字创建 DP 的实例
    // Create property
    DependencyProperty dp = new DependencyProperty(name, propertyType, ownerType, 
    defaultMetadata, validateValueCallback);
  • 创建了新的 DP 实例并将其存储在名为 PropertyFromNameHashTable 中,如下所示:
    PropertyFromName[key] = dp;
  • 新创建的 DP 从 RegisterCommon 方法 返回给 DependencyProperty 类的 Register 方法。
  • 最后,Register 方法将 DP 实例返回给调用 Register 方法的类,以将其也存储在标识符中供其使用。在我们的例子中,变量/标识符 MinLinesProperty 将接收 Register 方法的返回值。

以上仅是对此过程的简化总结,您可以随时深入代码研究 - 参考代码在此处DependencyProperty 类在名为 PropertyFromName 的哈希表中维护所有 DependencyPropertystatic 引用。每个 dependencyProperty 对象都注册在该 HashTable 中。

DependencyProperty 类中有总共 5 种重载的 public static Register 方法用于注册 DP。同样,还有另外 5 种重载的 RegisterAttach 方法用于注册附加属性。有关更多信息,请参阅 MSDN 上的 DependencyProperty 类详解

DP 的 getter 是如何工作的

DP 的 getter 工作原理如下:

  • Getter 的工作方式与 CLR 属性的 getter 不同。它不从 private 字段返回一个值,而是调用 DependencyObject 类的方法 GetValue(DependencyProperty)
  • DependencyProperty 本身不提供存储 DP 本地值的机制。DP 仅包含其默认值和一个名为 GetDefaultValue 的方法,该方法将返回 DP 的默认值,仅此而已。
  • DP 只能 定义在继承自 DependencyObject 的类中的原因之一是,DP 的 Getter 需要 GetValue 方法,而该方法来自基类,即 DependencyObject 类。
  • 当您调用 GetValue 时,您传入了本地的 public static 只读变量(标识符),它包含与 DP 相关的一些信息和元数据(但不是值)。
  • GetValue 方法调用以下方法:
    GetValueEntry( LookupEntry(dp.GlobalIndex), dp, null, RequestFlags.FullyResolved) 

    该方法返回一个名为 EffectiveValueEntrystruct

  • GetValueEntry 方法内部,为了简单起见,以下代码最为相关:
    entry = _effectiveValues[entryIndex.Index];
    它查看一个名为 _effectiveValuesEffectiveValueEntry structprivate 数组,该数组存储了该类的 DP 的所有实例值。此数组在 DepencentObject 类中作为 private 实例成员定义,因此所有继承自 DepencentObject 的类都拥有并使用它。_effectiveValues 作为 DependencyObjects 的有效值集合。
  • struct EffectiveValueEntry 有一个名为 Value 的字段,其类型为 object,因此它可以根据任何数据类型存储 DP 的实例值。
  • 方法 EntryIndex LookupEntry (int targetIndex) 接受 DP 的 GlobalIndex(属性的零基全局唯一索引),并查找与给定 DP 匹配的条目。
  • 最后,在 DP 的 getter 中发生类型转换,从 object 转换为特定的数据类型(因为 GetValue 方法的返回类型是 object)。

以下是 _effectiveValuesDependencyObject 类中定义的代码:

// The cache of effective values for this DependencyObject
// This is an array sorted by DP.GlobalIndex.  This ordering is
// maintained via an insertion sort algorithm.
   private EffectiveValueEntry[] _effectiveValues;

同样,该 private 数组通过 DependencyObject 类的 internal 属性 EffectiveValues 公开。

// The cache of effective (aka "computed" aka "resolved") property
// values for this DO.  If a DP does not have an entry in this array
// it means one of two things:
//  1) if it's an inheritable property, then its value may come from
//     this DO's InheritanceParent
//  2) if it's not an inheritable property (or this DO's InheritanceParent
//     doesn't have an entry for this DP either), then the value for
//     that DP on this DO is the default value.
// Otherwise, the DP will have an entry in this array describing the
// current value of the DP, where this value came from, and how it
// has been modified
internal EffectiveValueEntry[] EffectiveValues
{
    [FriendAccessAllowed] // Built into Base, also used by Framework.
    get { return _effectiveValues; }
}

DP 的 setter 是如何工作的

DP 的 getter 工作原理如下:

  • 与 Getter 类似,Setter 的工作方式也与 CLR 属性的 setter 不同。它不将值存储在 private 字段中,而是调用 DependencyObject 类的方法 SetValue
  • 当您调用 SetValue 时,您会传入 local public static 只读标识符 和给定的值(会发生类型转换为 object)。
  • SetValue 方法调用以下方法,该方法返回 void
    SetValueCommon(dp, value, metadata, false, false, OperationType.Unknown, false)
  • SetValueCommon 方法内部,它首先会在 _effecticeValues 数组中查找条目索引。
    EntryIndex entryIndex = LookupEntry(dp.GlobalIndex);
  • 然后为给定的 DP 创建 struct EffectiveValueEntry,最后 SetValueCommon 会调用以下方法来将值 插入/更新_effectiveVaues 数组中:
    SetEffectiveValue(entryIndex, dp, dp.GlobalIndex, metadata, 
    newExpr, BaseValueSourceInternal.Local);
    .................
    .................
    UpdateEffectiveValue(entryIndex, dp, metadata, oldEntry, 
    ref newEntry, coerceWithDeferredReference, coerceWithCurrentValue, operationType);
  • SetValue SetValueCommon 方法都是 void,所以不返回任何内容。

顺便说一句,DependencyObject 类中存在以下方法,它们在不同操作期间被调用,用于更新 _effectiveVaues 数组中的值:

private EffectiveValueEntry GetEffectiveValue(
	            EntryIndex entryIndex, DependencyProperty dp,
	            RequestFlags requests)

private void InsertEntry(
	            EffectiveValueEntry entry, uint entryIndex)

internal void SetEffectiveValue(
	            EntryIndex entryIndex, DependencyProperty dp, 
	            PropertyMetadata metadata, EffectiveValueEntry newEntry, 
	            EffectiveValueEntry oldEntry)

internal void SetEffectiveValue(
	            EntryIndex entryIndex, DependencyProperty dp, 
	            int targetIndex, PropertyMetadata metadata, 
	            object value, BaseValueSourceInternal valueSource)
        
private void SetExpressionValue(
	            EntryIndex entryIndex, object value, object baseValue)

WPF 属性系统如何减少内存占用

现在让我们总结一下我们对 DP 在内部如何存储的结论,如下图所示:

此外,让我们看看 CLR 属性是如何存储的。例如,如果一个名为 Customer 的类有 100 个 CLR 属性,那么当您创建 Customer 的实例时,所有 100 个 CLR 属性的内存都会在 Heap 中分配。

现在让我们来看一个 WPF 中的 TextBox 类,它有超过 150 个依赖属性,但在我们设置本地值之前,为创建的拥有者实例存储本地值不会消耗任何 DP 的内存。但是,您仍然可以使用 getter,它将返回一个值,要么是其默认值,要么是根据依赖属性值解析机制确定的值。正如我们所见,DP 的默认值不是特定于拥有者实例的,例如不同的 TextBoxes。DP 的默认值存储在 PropertyFromName HashTable 中的 global DP static 对象中(在我们的例子中是 DependencyProperty 类的 MinLines 实例)。

因此,对于 DP 而言,实例的本地值仅在设置时才占用内存。这就是 WPF 属性系统帮助减少应用程序内存占用的方式。

结论

在本文中,我们研究了依赖属性的创建及其 getter 和 setter 的工作方式。我们理解了依赖属性的存储机制如何帮助减少应用程序的内存占用。感谢阅读。欢迎您提出评论和改进建议。

参考文献

© . All rights reserved.