WPF依赖属性学习

概述

WPF 依赖属性(Dependency Property)是 WPF 框架的核心基础设施之一,它扩展了传统 .NET 属性的能力,为 WPF 提供数据绑定、动画、样式、继承值、属性值变更通知等高级功能。

为什么需要设计依赖属性?

因为依赖属性做到了CLR属性没做到的一些事情。

列举几个场景:

1、数据驱动 UI 的动态性需要"可计算的值

在 WPF 里,绑定的值、样式 Setter 的值、触发器的值、动画帧的值,都是事后才知道的,甚至可以在运行时不断切换来源。

CLR 属性:值写死在一个私有字段里,谁最后 set 就留谁。

依赖属性:属性系统先查看"当前这一帧到底是谁最有发言权",再给出最终值------也就是"值是从外部来的,我只是按优先级算一算"的依赖计算。

2、大规模对象树的内存压力要求"默认值共享

WPF 的控件树随随便便成千上万实例,如果每个 Button 都把 FontSize = 11 存一份 double,内存就爆炸了。

依赖属性把"默认值"压缩到一个静态全局哈希表里,没显式设置的实例,直接查表用同一份值。

3、样式 / 动画 / 绑定 / 继承 / 触发器 / 资源多路输入需要统一的"优先级规则

同一个 Background,可以是:本地值(红),主题样式(蓝),动画(绿),触发器(黄)......

传统属性里谁最后 set 谁赢,根本无法表达这种"多源头分时复用"的复杂策略。

依赖属性为此内置了一套显式的优先级表(动画>本地值>触发器>样式...),系统每次重新评估就行,无需控件开发者自己写状态机。

4、跨父子树的"属性值继承

典型例子:FontSize 设到 Window 上,所有子孙 TextBlock 直接复用该值,但中途随时可以用样式或本地值覆盖。

传统字段存储实现:父级改一次就要递归遍历整棵树;

依赖属性:子元素在取值时惰性向上询问,逻辑/性能都优雅。

学习依赖属性

在创建自定义的时候,创建一个依赖属性的示例如下所示:

csharp 复制代码
  public int Value
  {
      get => (int)GetValue(ValueProperty);
      set => SetValue(ValueProperty, value);
  }
  public static readonly DependencyProperty ValueProperty = DependencyProperty.Register(
      nameof(Value), typeof(int), typeof(RatingControl),
      new FrameworkPropertyMetadata(0, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnValueChanged, CoerceValue));

首先来看看命名,一个CLR属性是Value,依赖属性是ValueProperty,这是一种命名约定,可以很容易将这两个东西关联起来。

依赖属性都是通过DependencyProperty.Register方法注册:

csharp 复制代码
public static readonly DependencyProperty ValueProperty =
  DependencyProperty.Register(
      nameof(Value),          // 属性名 Value
      typeof(int),            // 属性类型
      typeof(RatingControl),  // 所属类型
      new FrameworkPropertyMetadata(
          0,                                   // 默认值
          FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, // 默认为双向绑定
          OnValueChanged,                      // 值发生变更时的回调
          CoerceValue)                         // 强制值回调
  );

CoerceValue是强制回调:

csharp 复制代码
private static object CoerceValue(DependencyObject d, object baseValue)
{
  var ctl = (RatingControl)d;
  int v = Math.Max(0, (int)baseValue); // 下限
  v = Math.Min(v, ctl.Max);            // 上限
  return v;
}

OnValueChanged是变更回调:

csharp 复制代码
private static void OnValueChanged(
  DependencyObject d, DependencyPropertyChangedEventArgs e)
{
  var ctl = (RatingControl)d;
  ctl.UpdateVisualStates();
}

作用:值真正改变后通知控件更新 UI。

e 中包含旧值 e.OldValue 与新值 e.NewValue,可进一步比较差异。

生命周期小结(一个赋值的全过程)

代码 / 绑定 / 动画尝试改变 Value。

WPF 调用 CoerceValue 让控件有机会矫正值。

如果矫正后的值与当前存储值相同,流程结束;否则进入下一步。

触发 OnValueChanged → 更新UI。

因为是 BindsTwoWayByDefault,若存在绑定的源(ViewModel),其对应属性也会被同步。

现在大概了解了依赖属性的设计,你可能也听说过"附加属性"与"继承属性"。

其实官方并没有"继承属性"这个称谓,继承属性只是将依赖属性设置成可继承罢了。

要想更好地理解依赖属性的概念,一个很好的方式就是去看WPF的源码,看看在源码中是如何使用的,现在就让我们一起去源码中找找看吧!!

先来看看普通的依赖属性定义:

目前我们接触到了DependencyPropertyDependencyPropertyKey

DependencyPropertyKey表示只读依赖属性。

这里官方源码将按钮是否按下这个属性设置为了只读依赖属性,为什么官方是这样做的呢?

想象一下一个按钮的 IsPressed 属性。这个属性应该是 true 还是 false,不应该由应用程序的逻辑直接决定(比如,你不应该写 myButton.IsPressed = true; 来"按下"一个按钮)。它的状态应该完全由用户的交互行为(鼠标按下、触摸、键盘空格键等)来驱动。

如果你把它做成一个普通的可以随意读写的属性:

csharp 复制代码
public bool IsPressed { get; set; }

那么任何代码都可以修改它,这会破坏按钮的内在逻辑和行为一致性。

如果你把它做成一个普通的只读属性:

csharp 复制代码
private bool _isPressed;
public bool IsPressed { get { return _isPressed; } }

虽然外部代码不能修改了,但这样做有几个缺点:

不支持 WPF 高级功能:它不再是一个依赖属性,因此无法享受数据绑定、样式、动画、属性值继承等 WPF 的核心特性。比如,你无法在 XAML 中写一个 Trigger 来在 IsPressed 为 true 时改变按钮的背景色。

缺少变更通知:如果 _isPressed 的值改变了,WPF 的其他部分(比如 UI 渲染系统)不会自动知道。你需要手动实现 INotifyPropertyChanged 接口,这额外增加了复杂性。

为了解决上述问题,WPF 引入了*"只读依赖属性" (Read-Only Dependency Property)* 。这种属性拥有两全其美的优势:

对外是只读的:保护了属性的完整性,防止外部代码随意篡改。

内部是可读写的:属性的"所有者"可以在特定逻辑下修改其值。

拥有依赖属性的全部特性:支持数据绑定、样式、动画、触发器等。

再来看看附加依赖属性:

Grid.Row是一个很经典的附加依赖属性。

注册附加依赖属性使用的是DependencyProperty.RegisterAttached方法。

附加属性必须提供静态的GetSet 方法:

在WPF中一个很经典的可继承依赖属性的例子就是FontSize,让我们来看看它的定义:

使用了FrameworkPropertyMetadataOptions.Inherits

这个枚举类有以下几个选项:

名称 十六进制值 十进制值 说明
None 0x000 0 无标志。
AffectsMeasure 0x001 1 此属性影响测量(Measure)过程。当此属性值改变时,元素需要重新计算其所需大小。
AffectsArrange 0x002 2 此属性影响布局(Arrange)过程。当此属性值改变时,元素需要重新定位并确定其最终大小。
AffectsParentMeasure 0x004 4 此属性影响父级的测量过程。当此属性值改变时,其父元素需要重新进行测量。
AffectsParentArrange 0x008 8 此属性影响父级的布局过程。当此属性值改变时,其父元素需要重新进行布局。
AffectsRender 0x010 16 此属性影响渲染。当此属性值改变时,元素可能需要部分或完全重绘。
Inherits 0x020 32 此属性的值可以被子元素继承。
OverridesInheritanceBehavior 0x040 64 此属性会导致继承和资源查找过程,忽略在查找路径上任何元素 (FE) 设置的 InheritanceBehavior 值。
NotDataBindable 0x080 128 此属性不支持数据绑定。
BindsTwoWayByDefault 0x100 256 对此属性的数据绑定默认为双向(Two-Way)模式。
Journal 0x400 1024 在通过 URI 进行日志记录/导航时,此属性的值应该被保存和恢复。
SubPropertiesDoNotAffectRender 0x800 2048 此属性的子属性不会影响渲染。例如,若属性 X 有子属性 Y,则修改 X.Y 不会触发渲染更新。

现在只是差不多了解了WPF中的依赖属性的一些概念与使用,要想真正明白依赖属性的设计与实现,还得多研究研究源码。

相关推荐
Scout-leaf3 天前
WPF新手村教程(三)—— 路由事件
c#·wpf
西岸行者4 天前
学习笔记:SKILLS 能帮助更好的vibe coding
笔记·学习
悠哉悠哉愿意5 天前
【单片机学习笔记】串口、超声波、NE555的同时使用
笔记·单片机·学习
别催小唐敲代码5 天前
嵌入式学习路线
学习
毛小茛5 天前
计算机系统概论——校验码
学习
babe小鑫5 天前
大专经济信息管理专业学习数据分析的必要性
学习·数据挖掘·数据分析
winfreedoms5 天前
ROS2知识大白话
笔记·学习·ros2
在这habit之下5 天前
Linux Virtual Server(LVS)学习总结
linux·学习·lvs
我想我不够好。5 天前
2026.2.25监控学习
学习
im_AMBER5 天前
Leetcode 127 删除有序数组中的重复项 | 删除有序数组中的重复项 II
数据结构·学习·算法·leetcode