📚 严老师·Day 15 | WPF 依赖属性(DependencyProperty)核心
4推送消息数
21652字符数
2026-08-14教学日期
<!-- message_id: om_x100b68d1febdb8a0b31eaf5d765064d -->
📚 严老师·Day 15 | WPF 依赖属性(DependencyProperty)核心
**Phase 3 第 2 课**。WPF 属性系统的**基石**。学完这个,你对 DP 的认知就超过 80% 的 WPF 开发者。
───
今日知识点:依赖属性(DependencyProperty)
🎯 是什么
WPF 依赖属性(DP)是 WPF 属性系统的**核心基石**。它是一个 **静态注册 + 动态存储** 的属性系统,由两部分组成:
• **标识符**:DependencyProperty 静态字段(static readonly),**全局单例**
• **存储**:DependencyObject 内部维护的 EffectiveValueEntry[] 数组(**按需懒分配**)
**核心定义**:
DP 是一个**值解析框架** —— 当你 GetValue(dp) 时,WPF 会按 **11 级优先级** 从动画、绑定、本地值、样式、继承等来源**计算**最终值,而不是简单的"读字段"。
───
🤔 为什么(CLR 属性的 9 大局限)
| # | 局限 | CLR 属性 | DP |
| --- | -------------------------------------- | ----------------------------- | ----- |
| 1 | 值继承(子元素继承父元素) | ❌ | ✅ |
| 2 | 数据绑定(Binding) | ❌ | ✅ |
| 3 | 动画(Storyboard) | ❌ | ✅ |
| 4 | 样式 Setter(<Setter>) | ❌ | ✅ |
| 5 | 模板 TemplatedParent | ❌ | ✅ |
| 6 | 强制值(CoerceValueCallback) | ❌ | ✅ |
| 7 | 验证回调(ValidateValueCallback) | ❌ | ✅ |
| 8 | 变更通知 + 依赖链 | ❌(需手写 INotifyPropertyChanged) | ✅(自动) |
| 9 | 资源引用(StaticResource / DynamicResource) | ❌ | ✅ |
💡 关键洞察:WPF 为什么"不抛弃"CLR 属性?
| 维度 | DP | CLR 属性 |
| ---------------- | ------------- | ------------ |
| 编译时类型检查 | ❌(运行时才报错) | ✅(编译期拦截) |
| IDE IntelliSense | ❌ | ✅ |
| XAML 设计器智能提示 | ❌ | ✅ |
| 重构友好 | ❌ | ✅(rename 同步) |
| 普通访问性能 | 慢(查表 + 优先级计算) | 快(直接读字段) |
| 静态分析 | 弱 | 强 |
**结论**:WPF 的设计哲学是 **CLR 属性做"编译期门面" + DP 做"运行时能力"**。CLR 包装的 get/set 必须**纯转发**(只调 GetValue/SetValue),**禁止有任何业务逻辑** —— 业务逻辑统一放 3 个回调里。
───
🛠️ 怎么用(完整生产代码)
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
public class NumericUpDown : Control
{
// ============== ① DP 注册:静态字段、类加载时执行一次 ==============
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register(
name: nameof(Value),
propertyType: typeof(int),
ownerType: typeof(NumericUpDown),
typeMetadata: new FrameworkPropertyMetadata(
defaultValue: 0,
FrameworkPropertyMetadataOptions.AffectsRender
| FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
propertyChangedCallback: OnValueChanged, // ③ 变更回调(最后)
coerceValueCallback: CoerceValue), // ② 强制值回调(中间)
validateValueCallback: ValidateValue); // ① 验证回调(最先)
// ============== ② CLR 包装:纯转发,禁止有业务逻辑 ==============
public int Value
{
get => (int)GetValue(ValueProperty);
set => SetValue(ValueProperty, value);
}
// ① 验证值(最早,非法值直接抛 ArgumentException)
private static bool ValidateValue(object value) => value is int;
// ② 强制值(调整到 [Min, Max] 范围)
private static object CoerceValue(DependencyObject d, object baseValue)
{
var ctrl = (NumericUpDown)d;
return Math.Clamp((int)baseValue, ctrl.Min, ctrl.Max);
}
// ③ 值变更(最后,值已确定,触发业务逻辑)
private static void OnValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
=> ((NumericUpDown)d).RaiseValueChanged((int)e.OldValue, (int)e.NewValue);
protected virtual void RaiseValueChanged(int oldValue, int newValue) { /* 业务 */ }
// Min/Max DP(改了之后要触发 Value 的 Coerce)
public static readonly DependencyProperty MinProperty =
DependencyProperty.Register(nameof(Min), typeof(int), typeof(NumericUpDown),
new PropertyMetadata(0,
(d, e) => ((NumericUpDown)d).CoerceValue(ValueProperty)));
public int Min
{
get => (int)GetValue(MinProperty);
set => SetValue(MinProperty, value);
}
public static readonly DependencyProperty MaxProperty =
DependencyProperty.Register(nameof(Max), typeof(int), typeof(NumericUpDown),
new PropertyMetadata(100,
(d, e) => ((NumericUpDown)d).CoerceValue(ValueProperty)));
public int Max
{
get => (int)GetValue(MaxProperty);
set => SetValue(MaxProperty, value);
}
}
XAML 中使用:
<local:NumericUpDown Value="50" Min="0" Max="100" x:Name="ctrl" />
📊 三大回调执行顺序
| 顺序 | 回调 | 作用 | 失败行为 |
| --- | ----------------------- | --------- | ------------------------------ |
| ① | ValidateValueCallback | 验证值的合法性 | 返回 false → 抛 ArgumentException |
| ② | CoerceValueCallback | 把值强制到合法范围 | 返回 baseValue 或调整后的值 |
| ③ | PropertyChangedCallback | 通知值已变更 | 仅通知,无返回 |
**关键洞察**:
• ValidateValue = **门卫**(拒绝非法值)
• CoerceValue = **矫正师**(调整值到合法范围)
• PropertyChanged = **广播员**(通知外界)
───
⚠️ 常见误区(8 个高频陷阱)
1. **DP 注册必须在静态字段** —— static readonly 是**类加载期单次执行**的保证,绝不能在实例构造里 Register
2. **CLR 包装必须纯转发** —— 业务逻辑写进 get/set 是大忌(XAML 赋值、动画、绑定都不走 CLR 包装)
3. **SetValue 不触发 CLR 属性 setter** —— DP 内部 PropertyChanged 不会回调 CLR 包装的 set(WPF 设计)
4. **CoerceValue 内 SetValue(自己) = 无限递归** —— 必须直接 return 新值,或用 SetCurrentValue
5. **ClearValue ≠ SetValue(defaultValue)** —— ClearValue 删除本地值(回到继承/默认),SetValue(defaultValue) 是显式本地值(优先级最高的本地层)
6. **RegisterAttached ≠ 普通 DP** —— 附加属性(Attached Property)是"寄生"在其他类上的 DP(如 Grid.Row),**不能写 CLR 包装**
7. **RegisterReadOnly 才是只读 DP 的正确方式** —— 用 DependencyPropertyKey 保护 SetValue,外部只能 GetValue
8. **OverrideMetadata vs AddOwner 区别** —— 前者是**改变继承类的元数据**(同 DP),后者是**把别人的 DP 借到自己类上**(多 Owner)
───
🔗 关联知识网络(对接未来 Phase 3-4 课程)
• **Day 14 WPF 架构** → DP 注册走 static readonly 字段反射 → 是 WPF **不能 NativeAOT** 的第二大根因
• **Day 11 ThreadPool** → DispatcherSynchronizationContext 让 await 后自动回 UI 线程,触发 OnValueChanged 在 UI 线程执行
• **Day 6 反射** → DependencyPropertyDescriptor 是反射访问 DP 的入口
• **Day 8 GC** → DependencyObject._effectiveValues 数组,只在显式 SetValue 时分配槽位,**未设置的 DP 不占空间**
• **未讲 Day 17 路由事件** → RoutedEventArgs.OriginalSource 本质是 DP
• **未讲 Phase 4 数据绑定** → Binding 的目标**必须是 DP**(CLR 属性无法绑定)
• **未讲 Phase 4 样式/模板** → <Setter Property="..."> 只能填 DP,{TemplateBinding} 只能绑 DP
• **未讲 Phase 4 动画** → Storyboard 改写 DP 时优先级 **2️⃣ 最高**(超过本地值)
───
(第 2 段:三道递进面试题,稍后送达)
---
<!-- message_id: om_x100b68d1fe28a8a8b2714b03d82204e -->
📝 Day 15 | 三道递进面试题
Q1(基础概念)
**请简述 WPF 中 DependencyProperty 与普通 CLR 属性的本质区别,并列举依赖属性的 5 大核心能力。**
**追问:WPF 为什么不完全抛弃 CLR 属性,反而要为每个 DP 保留 CLR 包装?**
**考察点**:对 DP 本质的准确理解,以及 WPF "CLR 门面 + DP 能力" 的设计哲学。
Q2(原理与辨析)
**WPF 依赖属性的值解析优先级是怎样的?请至少列出 8 级。**
**追问 1**:当 CoerceValueCallback、PropertyChangedCallback、ValidateValueCallback 三个回调**同时存在**时,执行顺序是什么?
**追问 2**:SetValue(dp, newValue) 触发的**完整事件链**是怎样的(用伪代码或调用序列说明)?
**考察点**:对 DP 值解析机制的深度理解,以及三大回调时序的精确把握。
Q3(实战与深度)
在 WPF 中实现一个 **PasswordStrengthIndicator** 控件,要求:
1. 接受 Password 字符串作为依赖属性(**默认空字符串**)
2. 实时计算密码强度(**Weak / Medium / Strong**)作为**只读**依赖属性 Strength
3. Password 变化时**自动重算** Strength
4. 当 Password 为空时,Strength **强制为 Weak**(即使 Coerce 试图返回 Strong)
5. 用 Foreground 颜色区分(Weak=红 / Medium=黄 / Strong=绿)
**请给出完整生产级代码**:
- DP 注册(包含 3 个关键回调的完整签名)
- Password 变更触发 Strength 重算的关键代码
- SetCurrentValue vs SetValue 的选择理由
- XAML 演示
- **至少指出 4 个生产级陷阱及解决方案**
**考察点**:对 RegisterReadOnly / DependencyPropertyKey / SetCurrentValue / CoerceValueCallback 协同的实战掌握。
(第 3 段:全面复盘讲解,稍后送达)
---
<!-- message_id: om_x100b68d1fcf2f4a0b11a101b381d6e2 -->
🎯 Day 15 | 全面复盘讲解(上篇:Q1 + Q2)
───
Q1 复盘
✅ 答案要点
1. **本质区别**:
• **CLR 属性** = **编译期字段**(IL 指令直接读字段,0 间接)
• **DP** = **运行时值解析系统**(每次 GetValue 都要查 11 级优先级表,可能触发 EffectiveValueEntry 重新计算)
2. **5 大核心能力**(任选 5 个即可):
3. 数据绑定(Binding)
4. 样式(Style.Setter)
5. 动画(Storyboard)
6. 值继承(Inheritance)
7. 强制值(CoerceValueCallback)
8. 资源引用(StaticResource / DynamicResource)
9. 模板绑定(TemplateBinding / TemplatedParent)
10. **不抛弃 CLR 包装的 3 大原因**:
• 编译时类型检查(Value="abc" 编译期报错 vs DP 运行时抛 XamlParseException)
• IDE IntelliSense + XAML 设计器智能提示
• 重构友好(rename 同步所有引用)
11. **隐藏第 4 点(加分项)**:CLR 包装是 XAML 解析器发现属性的入口 —— XAML 只认 public CLR 属性 + 匹配的 DP 静态字段
🔍 深度解析
很多候选人回答时会把"5 大能力"答成"绑定、样式、动画、值继承、强制值",但漏掉一个关键点:**DP 的"动态存储"本身就是核心能力** —— DependencyObject 内部 EffectiveValueEntry[] 按需分配,未设置的 DP **零内存占用**。1000 个 Button 只设 10 个的 Width,只有 10 个槽位被分配。
另一个常被忽略的设计点:**DP 是进程级全局单例**。Button.WidthProperty 整个 AppDomain 只有一个,所有 Button 实例共享,只是各自的 EffectiveValueEntry 不同。这是 WPF "**内存换性能**" 的典型设计 —— 注册一次,使用无数次。
⚠️ 易错点
1. ❌ 答成"DP 是线程安全的" → **半对**。DP 自身注册是线程安全的(类构造期单次执行),但 SetValue 不是线程安全的,**跨线程 SetValue 必须 Dispatcher.Invoke**
2. ❌ 漏掉"CLR 包装"是 XAML 入口 → 漏掉这个就答不到满分
3. ❌ 把"值继承"理解成"继承链上的属性" → 错,值继承是**沿着视觉树**向下传递(子元素没显式设置时继承父元素的值,如 FontSize、Foreground)
4. ❌ 把 DP 当成"高级版 CLR 属性" → 错,DP 是**值解析框架**,CLR 属性是**编译期字段**,二者是**正交**的两个系统,WPF 用 CLR 包装把二者桥接
───
Q2 复盘
✅ 答案要点
**WPF 依赖属性值解析优先级**(11 级,合并为 8 大类):
| 优先级 | 来源 | 说明 | 覆盖行为 |
| ------ | --------------- | --------------------------------------- | ------------- |
| 1️⃣ 最高 | 强制值(Coerced) | 通过 CoerceValueCallback | 任何来源都被 Coerce |
| 2️⃣ | 动画值(Animation) | Storyboard / DynamicResource 动画 | 覆盖本地值 |
| 3️⃣ | 本地值(Local) | SetValue 显式设置 / XAML 赋值 | 覆盖样式/继承 |
| 4️⃣ | TemplatedParent | 模板内的属性 | 模板生效 |
| 5️⃣ | 样式触发器(Triggers) | Trigger / DataTrigger / MultiTrigger | 覆盖 Setter |
| 6️⃣ | 样式 Setter | <Style><Setter Property="..."/></Style> | 覆盖主题样式 |
| 7️⃣ | 主题样式触发器 | ThemeStyle.Triggers | 覆盖主题 Setter |
| 8️⃣ | 主题样式 Setter | Generic.xaml 主题默认 | 覆盖继承值 |
| 9️⃣ | 继承值(Inherited) | 父元素 AddOwner(Inherits=true) 的 DP | 子元素默认 |
| 🔟 最低 | 默认值 | Register 时的 defaultValue | 兜底 |
**三大回调执行顺序**(SetValue 完整事件链伪代码):
void SetValue(DependencyProperty dp, object newValue)
{
// ① 验证值(最早,非法值直接抛 ArgumentException)
if (dp.ValidateValueCallback != null && !dp.ValidateValueCallback(newValue))
throw new ArgumentException($"Invalid value for {dp.Name}");
// ② 强制值(把值调整到合法范围)
object coerced = dp.CoerceValueCallback != null
? dp.CoerceValueCallback(this, newValue)
: newValue;
// ③ 检测是否真变化(短路优化)
object oldValue = GetValue(dp);
if (object.Equals(oldValue, coerced)) return; // 没变就什么都不做
// ④ 写入 EffectiveValueEntry 内部存储
SetEffectiveValue(dp, coerced, EffectiveValueSource.Local);
// ⑤ 触发值变更回调
dp.PropertyChangedCallback?.Invoke(this,
new DependencyPropertyChangedEventArgs(dp, oldValue, coerced));
// ⑥ 触发 PropertyChanged 事件 + 通知数据绑定源
OnPropertyChanged(dp.Name); // 给 INotifyPropertyChanged 订阅者
BindingExpression.UpdateSource(); // 双向绑定反向通知
}
**执行顺序总结**:**Validate → Coerce → PropertyChanged**
🔍 深度解析
**Q2 真正考察的是对"值解析"的本质理解 —— DP 不是字段,是表达式**。WPF 内部用 **表达式树** 表示 DP 的"当前值来源":
EffectiveValueEntry {
DependencyProperty dp;
object value;
EffectiveValueSource source; // Local / Inherited / Animated / Coerced / TemplatedParent / Style / Default
Expression expression; // 绑定/动态资源的"未求值"表达式
}
GetValue 时的实际行为:
1. **找到最高优先级的源**(从 1️⃣ 到 🔟 扫描)
2. **如果该源是 Expression(绑定/动态资源)**,调用 GetValue() 求值
3. **应用 CoerceValueCallback**(如果存在)
4. **返回最终值**
这就是为什么 Binding 不需要 SetValue 就能改变 DP —— WPF 在 Expression 层就拦截了。
⚠️ 易错点
1. ❌ **答成"动画 > 本地值 > 样式 > 继承"**(把强制值漏了) → 强制值永远是**最顶层**,任何来源都被 Coerce
2. ❌ **混淆"样式触发器"和"样式 Setter"** → 触发器优先级**高于** Setter(Trigger 条件命中时覆盖 Setter)
3. ❌ **答"Coerce 在 PropertyChanged 之后"** → 错!Coerce 永远在 PropertyChanged **之前**
4. ❌ **漏掉 SetValue 内部短路** → 同一个值连续 SetValue,PropertyChangedCallback 不会重复触发
5. ❌ **混淆 TemplatedParent 和 TemplateBinding** → TemplatedParent 是**值来源**({RelativeSource TemplatedParent}),TemplateBinding 是 XAML 标记扩展
6. ❌ **不知道 Inherited 必须在元数据里显式标记** → FrameworkPropertyMetadataOptions.Inherits,否则值不会沿视觉树向下传
7. ❌ **默认值的"陷阱"** → 默认值是**最低优先级**,但 SetValue(defaultValue) 是**本地值**(最高优先级之一),二者完全不同
───
(第 4 段:Q3 实战复盘 + 扩展思考,稍后送达)
---
<!-- message_id: om_x100b68d1fd1c4ca4b4ae22e595d8cd9 -->
🎯 Day 15 | 全面复盘讲解(下篇:Q3 实战)
───
Q3 复盘
✅ 完整生产级代码
public enum PasswordStrength { Weak, Medium, Strong }
public class PasswordStrengthIndicator : Control
{
// ============== ① Password DP(双向绑定 + 影响渲染) ==============
public static readonly DependencyProperty PasswordProperty =
DependencyProperty.Register(
nameof(Password), typeof(string), typeof(PasswordStrengthIndicator),
new FrameworkPropertyMetadata(
string.Empty,
FrameworkPropertyMetadataOptions.AffectsRender
| FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
OnPasswordChanged)); // 密码变 → 触发重算
public string Password
{
get => (string)GetValue(PasswordProperty);
set => SetValue(PasswordProperty, value);
}
// ============== ② Strength DP(只读,RegisterReadOnly) ==============
private static readonly DependencyPropertyKey StrengthPropertyKey =
DependencyProperty.RegisterReadOnly(
nameof(Strength), typeof(PasswordStrength), typeof(PasswordStrengthIndicator),
new PropertyMetadata(
PasswordStrength.Weak,
null,
CoerceStrength)); // ⭐ 关键:Coerce 强制空密码为 Weak
public static readonly DependencyProperty StrengthProperty =
StrengthPropertyKey.DependencyProperty; // 公开给外部只读访问
public PasswordStrength Strength
=> (PasswordStrength)GetValue(StrengthProperty);
// ============== ③ Foreground DP(Inherits 让子元素继承) ==============
public new static readonly DependencyProperty ForegroundProperty =
Control.ForegroundProperty.AddOwner(
typeof(PasswordStrengthIndicator),
new FrameworkPropertyMetadata(
Brushes.Gray,
FrameworkPropertyMetadataOptions.AffectsRender
| FrameworkPropertyMetadataOptions.Inherits));
// ============== ④ 密码变更 → 重算强度(用 SetCurrentValue) ==============
private static void OnPasswordChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var ctrl = (PasswordStrengthIndicator)d;
var pwd = (string?)e.NewValue ?? string.Empty;
var newStrength = ComputeStrength(pwd);
// ⭐ 用 SetCurrentValue(只改当前值,保留 Coerce/绑定/动画/样式触发器)
ctrl.SetCurrentValue(StrengthPropertyKey, newStrength);
// 若用 SetValue → 写入本地层,会"覆盖" Coerce(下一帧才被 Coerce 纠正)
}
// ============== ⑤ 强制值:空密码必须 Weak(题目核心陷阱) ==============
private static object CoerceStrength(DependencyObject d, object baseValue)
{
var ctrl = (PasswordStrengthIndicator)d;
// 即使 OnPasswordChanged 计算出 Strong,空密码也必须 Weak
return string.IsNullOrEmpty(ctrl.Password)
? PasswordStrength.Weak
: baseValue;
}
// ============== ⑥ 强度算法(生产级简化版) ==============
private static PasswordStrength ComputeStrength(string pwd)
{
if (string.IsNullOrEmpty(pwd)) return PasswordStrength.Weak;
if (pwd.Length < 6) return PasswordStrength.Weak;
bool hasUpper = pwd.Any(char.IsUpper);
bool hasDigit = pwd.Any(char.IsDigit);
bool hasSpecial = pwd.Any(c => !char.IsLetterOrDigit(c));
int score = (hasUpper ? 1 : 0) + (hasDigit ? 1 : 0) + (hasSpecial ? 1 : 0);
return score switch
{
0 => PasswordStrength.Weak,
1 => PasswordStrength.Medium,
_ => PasswordStrength.Strong
};
}
}
**XAML 演示**:
<Window xmlns:local="clr-namespace:MyApp.Controls">
<StackPanel>
<TextBox x:Name="pwdBox" />
<!-- 双向绑定:用户输入 → Password,代码改 Password → TextBox 文本 -->
<local:PasswordStrengthIndicator>
<local:PasswordStrengthIndicator.Password>
<Binding ElementName="pwdBox" Path="Text"
Mode="TwoWay"
UpdateSourceTrigger="PropertyChanged" />
</local:PasswordStrengthIndicator.Password>
</local:PasswordStrengthIndicator>
</StackPanel>
</Window>
───
🔍 深度解析:SetValue vs SetCurrentValue 的本质区别
| 行为 | SetValue(dp, v) | SetCurrentValue(dp, v) |
| ------------------ | --------------- | ---------------------- |
| 写入层级 | 本地值(Local) | 当前值(Current) |
| 打断动画 | ✅(动画被覆盖) | ❌(保留动画优先级) |
| 打断数据绑定 | ✅(绑定源被清空) | ❌(保留绑定源) |
| 打断样式触发器 | ✅ | ❌ |
| 触发 PropertyChanged | ✅ | ✅ |
| 触发 Coerce | ✅ | ✅ |
| 适用场景 | 业务层主动设值 | 回调内更新(避免打断其他源) |
**关键洞察**:
• SetValue = **完全掌控**(清空所有其他源)
• SetCurrentValue = **柔和更新**(只改当前展示值,不动其他优先级)
**Q3 的核心考点**:在 OnPasswordChanged 内更新 Strength,必须用 SetCurrentValue。如果用 SetValue(StrengthProperty, ...),会:
1. 把 Strength 写入**本地层**(优先级 3️⃣)
2. 下一帧 WPF 做 Coerce 时,看到本地值已经是 Strong,就不再 coerce 了
3. 导致**空密码也被显示为 Strong** → 题目要求 4️⃣ 失败
───
⚠️ 5 大生产级陷阱 + 解决方案
| # | 陷阱 | 根因 | 解决方案 |
| --- | -------------------------------------- | ------------------------------------------------------ | ------------------------------------------------------------------------------------ |
| T1 | Password 双向绑定死循环 | TextBox.Text ↔ Password 双向 + Password 触发 Coerce → 无限循环 | 单向 Mode=OneWay,或 UpdateSourceTrigger=PropertyChanged(用户输入立即同步,但 Password→TextBox 单向) |
| T2 | Coerce 无限递归 | CoerceStrength 内 SetValue(StrengthProperty, ...) → 递归 | 直接 return 计算结果,绝不在 Coerce 内 SetValue 自己 |
| T3 | OnPasswordChanged 用 SetValue 清空 Coerce | SetValue 写入本地层,Coerce 看到本地值不再 coerce | 改用 SetCurrentValue(StrengthPropertyKey, ...) —— 只改当前值,Coerce 仍生效 |
| T4 | Strength DP 没注册为只读 | 外部可以直接 SetValue(StrengthProperty, Weak) 绕过 Coerce | 用 RegisterReadOnly + 私有 StrengthPropertyKey 保护 SetValue |
| T5 | Foreground 没标记 Inherits | 子元素(如嵌入的 TextBlock)无法继承颜色 | FrameworkPropertyMetadataOptions.Inherits 标志 + AddOwner 到 Control.ForegroundProperty |
**隐藏陷阱(加分项)**:
• T6: ComputeStrength 是**纯函数**,但**生产环境需要异步**(防 HaveIBeenPwned API 调用阻塞 UI)→ async void + ConfigureAwait(false) + SetCurrentValue 在 finally 块
• T7: OnPasswordChanged 内**不要抛异常** —— 会打断 WPF 内部状态机,导致 EffectiveValueEntry 不一致
• T8: ForegroundProperty 用 AddOwner 而非 new PropertyMetadata —— 直接复用 Control 的 Foreground,避免两个不同名的 Foreground DP
───
🚀 扩展思考(2 个引导性问题)
**Q-A**:如何让这个控件支持**异步密码强度计算**(例如调用 HaveIBeenPwned API 检查泄露)?
• 提示:ComputeStrength 改为 Task<PasswordStrength>,OnPasswordChanged 改为 async void,用 CancellationTokenSource 取消上一次未完成的计算(防抖动),SetCurrentValue 在 try/finally 中保证执行
• 进阶:UI 显示"检查中..."状态(用 IsChecking DP + IsChecked 触发器联动)
**Q-B**:如何在 ControlTemplate 中用 TemplateBinding 把 Strength 暴露给模板里的 TextBlock?
• 提示:模板根元素 <ControlTemplate TargetType="local:PasswordStrengthIndicator">
• 子元素 <TextBlock Text="{TemplateBinding Strength}" /> —— TemplateBinding 走 4️⃣ **TemplatedParent 优先级**(高于样式 Setter)
• 进阶:把 Foreground 改成颜色转换器(Strength → Brush),用 TemplateBinding Foreground 暴露
───
📊 今日总结
| 维度 | 关键收获 |
| ----- | -------------------------------------------------------------------------------------------- |
| 核心抽象 | DP = static readonly DependencyProperty 标识符 + DependencyObject 内部 EffectiveValueEntry[] 动态存储 |
| 设计哲学 | CLR 属性 = 编译期门面,DP = 运行时能力,二者通过 CLR 包装桥接 |
| 优先级系统 | 11 级优先级,Coerce 最高,动画次之,本地值再次,默认值最低 |
| 3 大回调 | Validate(门卫)→ Coerce(矫正师)→ PropertyChanged(广播员) |
| 实战铁律 | 回调内更新自己用 SetCurrentValue,Coerce 内绝不 SetValue 自己,只读 DP 必须 RegisterReadOnly |
| 核心易错点 | SetValue 写本地层会打断 Coerce/绑定/动画,SetCurrentValue 才是"柔和更新" |
───
🔗 关联知识网络(今日新建立)
• **Day 14 WPF 架构** → DP 注册走 static readonly 反射,剪裁器看不到运行时枚举 → 是 WPF 不能 NativeAOT 第二大根因
• **Day 11 ThreadPool** → DispatcherSynchronizationContext 让 OnPasswordChanged 自动在 UI 线程执行
• **Day 6 反射** → DependencyPropertyDescriptor 是反射访问 DP 的元数据描述器
• **Day 8 GC** → EffectiveValueEntry[] 按需分配,未设置的 DP 零内存占用
• **未讲 Day 17 路由事件** → RoutedEventArgs.OriginalSource 本质是 DP
• **未讲 Phase 4 数据绑定** → Binding 目标必须是 DP
• **未讲 Phase 4 样式/动画** → <Setter> 和 Storyboard 只能操作 DP
───
🎯 下次起点(预告)
**Day 16:路由事件(Routed Event)** —— DP 解决了"值怎么存",路由事件解决"事件怎么传"。WPF 的事件系统是**树形传播**(冒泡 + 隧道 + 直接),不是 WinForms 的"控件各自为政"。包括:
• 路由事件 vs CLR 事件的本质区别
• 3 种路由策略(Bubble / Tunnel / Direct)的执行顺序
• RoutedEventArgs 的 Source / OriginalSource 区别
• 自定义路由事件 + EventManager.RegisterRoutedEvent + RaiseEvent
• 附加事件(Attached Event)与路由事件的关系
• WPF 路由事件 5 大使用陷阱(已注册事件不可修改路由策略 / 隧道事件必须配对 / Handled 一旦置 true 就无法重置 / 路由事件在模板中的传播 / CommandManager 与路由事件的关系)
───
📚 严老师 · 2026-08-14 · Day 15 完成
⏰ 等待主人回复(Q1/Q2/Q3 任选作答,我来评估 + 推进)