当您开始使用 WPF 开发应用程序时,您很快就会遇到 DependencyProperties。它们看起来与普通的 .NET 属性非常相似,但背后的概念要复杂和强大得多。
主要区别在于,普通 .NET 属性的值是直接从类中的私有成员读取的,而 DependencyProperty 的值是在调用从 DependencyObject 继承的 GetValue() 方法时动态解析的。
当您设置依赖属性的值时,它不会存储在对象的字段中,而是存储在基类 DependencyObject 提供的键和值的字典中。条目的键是属性的名称,值是您要设置的值。
依赖属性的优点是
减少内存占用
当您认为 UI 控件的 90% 以上的属性通常保持其初始值时,为每个属性存储一个字段是一种巨大的消耗。依赖属性通过仅在实例中存储修改的属性来解决这些问题。默认值在依赖属性中存储一次。
值继承
访问依赖项属性时,将使用值解析策略解析该值。如果未设置本地值,则依赖项属性将向上导航逻辑树,直到找到值。当您在根元素上设置FontSize时,它将应用于下面的所有文本块,除非您覆盖该值。
更改通知
依赖属性具有内置的更改通知机制。通过在属性元数据中注册回调,您会在属性值更改时收到通知。这也被数据绑定使用。
每次访问依赖属性时,它都会按照从高到低的优先级在内部解析值。它检查本地值是否可用,如果不可用,如果自定义样式触发器处于活动状态,…并继续直到找到一个值。最后,默认值始终可用。
每个 WPF 控件都将一组 DependencyProperties 注册到静态 DependencyProperty 类。它们中的每一个都包含一个键 - 每个类型必须是唯一的 - 和一个包含回调和默认值的元数据。
所有想要使用 DependencyProperties 的类型都必须从 DependencyObject 派生。这个基类定义了一个键值字典,其中包含依赖属性的本地值。条目的键是使用依赖属性定义的键。
当您通过其 .NET 属性包装器访问依赖属性时,它会在内部调用 GetValue(DependencyProperty) 来访问该值。此方法通过使用下面详细说明的值解析策略来解析值。如果本地值可用,它会直接从字典中读取它。如果没有设置值,则向上查找逻辑树并搜索继承的值。如果未找到值,则采用属性元数据中定义的默认值。这个序列有点简化,但它 显示主要概念。
要创建 DependencyProperty,请将类型为 DepdencyProperty 的静态字段添加到您的类型并调用 DependencyProperty.Register() 以创建依赖项属性的实例。 DependendyProperty 的名称必须始终以 …Property 结尾。这是 WPF 中的命名约定。
要使其可作为普通 .NET 属性访问,您需要添加一个属性包装器。这个包装器除了通过使用从 DependencyObject 继承的 GetValue() 和 SetValue() 方法并将 DependencyProperty 作为键传递来在内部获取和设置值外,什么都不做。
重要提示:不要向这些属性添加任何逻辑,因为只有在您从代码设置属性时才会调用它们。如果从 XAML 设置属性,则直接调用 SetValue() 方法。
如果您使用的是 Visual Studio,则可以键入 propdp 并点击 2x 选项卡来创建依赖项属性。
// Dependency Property
public static readonly DependencyProperty CurrentTimeProperty =
DependencyProperty.Register( "CurrentTime", typeof(DateTime),
typeof(MyClockControl), new FrameworkPropertyMetadata(DateTime.Now));
// .NET Property wrapper
public DateTime CurrentTime
{
get { return (DateTime)GetValue(CurrentTimeProperty); }
set { SetValue(CurrentTimeProperty, value); }
}
每个 DependencyProperty 都提供用于更改通知、值强制和验证的回调。这些回调在依赖属性上注册。
new FrameworkPropertyMetadata( DateTime.Now,
OnCurrentTimePropertyChanged,
OnCoerceCurrentTimeProperty ),
OnValidateCurrentTimeProperty );
更改通知回调是一个静态方法,每次 TimeProperty 的值更改时都会调用该方法。新值在 EventArgs 中传递,更改值的对象作为源传递。
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,则会抛出 ArgumentException。在我们的示例需求中,数据是 DateTime 的实例。
private static bool OnValidateTimeProperty(object data)
{
return data is DateTime;
}
WPF 控件的某些依赖属性是只读的。它们通常用于报告控件的状态,例如 IsMouseOver 属性。为这个值提供一个 setter 是没有意义的。
也许您会问自己,为什么不使用普通的 .NET 属性?一个重要的原因是您不能在普通的 .NET 属性上设置触发器。
创建只读属性类似于创建常规 DependencyProperty。不是调用 DependencyProperty.Register(),而是调用 DependencyProperty.RegisterReadonly()。这会返回一个 DependencyPropertyKey。此密钥应存储在您的类的私有或受保护的静态只读字段中。该键使您可以访问从类中设置值并将其用作普通依赖属性。
第二件事是注册分配给 DependencyPropertyKey.DependencyProperty 的公共依赖属性。此属性是可以从外部访问的只读属性。
// Register the private key to set the value
private static readonly DependencyPropertyKey IsMouseOverPropertyKey =
DependencyProperty.RegisterReadOnly("IsMouseOver",
typeof(bool), typeof(MyClass),
new FrameworkPropertyMetadata(false));
// Register the public property to get the value
public static readonly DependencyProperty IsMouseoverProperty =
IsMouseOverPropertyKey.DependencyProperty;
// .NET Property wrapper
public int IsMouseOver
{
get { return (bool)GetValue(IsMouseoverProperty); }
private set { SetValue(IsMouseOverPropertyKey, value); }
}
附加属性是一种特殊的 DependencyProperties。它们允许您将一个值附加到一个对该值一无所知的对象上。
这个概念的一个很好的例子是布局面板。每个布局面板需要不同的数据来对齐其子元素。 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 DependencyProperty TopProperty =
DependencyProperty.RegisterAttached("Top",
typeof(double), typeof(Canvas),
new FrameworkPropertyMetadata(0d,
FrameworkPropertyMetadataOptions.Inherits));
public static void SetTop(UIElement element, double value)
{
element.SetValue(TopProperty, value);
}
public static double GetTop(UIElement element)
{
return (double)element.GetValue(TopProperty);
}
如果要侦听依赖项属性的更改,可以将定义该属性的类型子类化并覆盖属性元数据并传递 PropertyChangedCallback。但更简单的方法是通过调用 AddValueChanged() 获取 DependencyPropertyDescriptor 并连接回调
DependencyPropertyDescriptor textDescr = DependencyPropertyDescriptor.
FromProperty(TextBox.TextProperty, typeof(TextBox));
if (textDescr!= null)
{
textDescr.AddValueChanged(myTextBox, delegate
{
// Add your propery changed logic here...
});
}
因为 null 也是一个有效的本地值,所以有一个常量 DependencyProperty.UnsetValue 来描述一个未设置的值。
button1.ClearValue( Button.ContentProperty );