WPF中的依赖属性

依赖属性

在传统的c#开发中,我们使用的是CLR 公共语言运行时,属性。它的本质是封装了一个私有字段,每次new一个对象,无论这个属性用不用,系统都会在内存中为这个字段分配空间。

而依赖属性 彻底颠覆了这个设置:它的值是依赖于多个数据源(比如动画、绑定、样式、默认值等)综合计算得出来的,并且使用了类似于全局字典的机制来节约内存。

依赖属性没有自己的值,它的值是从其他的绑定的数据源来的(数据绑定,样式,动画,继承)

在WPF的基础框架中,依赖属性的基类是DependencyObject,这个类位于基础的.net层中,DependencyObject 作为极其重要的地基,为WPF强大的数据绑定和动画系统提供了最核心的底层支持。

任何想要拥有依赖属性的控件活类,其继承链条的最顶端,必然需要直接或者间接地继承自DependencyObject

依赖

第一步:确保类继承自 DependencyObject

C#

复制代码
public class MyControl : DependencyObject // 必须继承此基类
{
    // 后续代码写在这里
}

第二步:声明并注册依赖属性 (全局静态变量)

必须使用 public static readonly 修饰,并通过 DependencyProperty.Register 向 WPF 系统注册。

复制代码
// 命名规范:属性名 + "Property" 后缀
public static readonly DependencyProperty MyValueProperty =
    DependencyProperty.Register(
        "MyValue",                  // 1. 属性的名字 (字符串,必须和第三步的包装器名字完全一致)
        typeof(int),                // 2. 属性的类型 (这里是 int)
        typeof(MyControl),          // 3. 拥有这个属性的宿主类的类型 (就是当前的类)
        new PropertyMetadata(0, OnMyValueChanged) // 4. 属性元数据:设置默认值(0)和值改变时的回调函数
    );

第三步:编写 CLR 属性包装器 (供写代码时方便调用)

利用 GetValue 和 SetValue 方法,将传统 CLR 属性与依赖属性连接起来。

复制代码
public int MyValue
{
    get { return (int)GetValue(MyValueProperty); }
    set { SetValue(MyValueProperty, value); }
}

第四步:编写值改变时的回调逻辑 (可选但极常用)

复制代码
// 必须是静态方法!
private static void OnMyValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    // d 是当前触发更改的实例对象
    MyControl control = (MyControl)d;
    
    // e.OldValue 是旧值,e.NewValue 是新值
    int newValue = (int)e.NewValue;
    Console.WriteLine($"值从 {e.OldValue} 变为了 {newValue}");
}

Set

一、 赋值 (Set) 阶段:值发生改变时的"三步曲"

无论是 XAML 赋值、动画改变,还是你在 C# 里调用了包装器的 set,最终都会汇总到底层的 SetValue。 一旦 SetValue 被触发,WPF 会严格按顺序执行以下三个内部回调方法(如果你在注册时提供了它们):

步骤 1:ValidateValueCallback (验证回调)
  • 动作:WPF 先问:"这个传进来的新值合法吗?"

  • 执行 :如果你注册了验证函数,系统会把新值传进去。如果你的函数返回 false(比如规定 MyValue 只能是正数,但传进来了 -1),程序会直接抛出异常,赋值被强行终止。

步骤 2:CoerceValueCallback (强制转换回调)
  • 动作:WPF 再问:"这个值虽然合法,但结合当前程序的其他状态,我需要强行纠正它吗?"

  • 执行 :比如一个进度条的 Value 传进来了 150,但是它的 Maximum 属性当前是 100。在这里,你可以把 150 强行"砍"成 100 并返回。这就是著名的"值胁迫"机制。

步骤 3:PropertyChangedCallback (属性更改回调)
  • 动作:WPF 宣布:"值已经安全、合法地修改完毕了!"

  • 执行 :这就是我们在注册时写的 OnMyValueChanged 方法。只有当新值和旧值真的不一样时,这个方法才会被触发。

  • 然后呢?(闭环) :在这里,WPF 会拿着 DependencyPropertyChangedEventArgs (依赖属性更改事件参数) 告诉你旧值是多少、新值是多少。此时,你才应该在这个方法里去写业务逻辑 (比如根据新的 MyValue 重新计算界面大小、触发特定的自定义事件等)。

疑问?

相反,它会直接拿着这根"指针",在底层执行:实例对象.SetValue(MyValueProperty, 10)。你这里说的这跟指针是什么?并且是在哪一句代码上的体现的

还有这一句当代码执行到 GetValue 时,WPF 不会傻傻地只去内存里拿一个固定值,而是会按照从高到低的优先级像闯关一样去寻找这个值。只要在高优先级找到了,就立刻返回,不再往下找。从高到低的优先级指的是什么?

一、 所谓的"指针"是什么?体现在哪句代码上?

我之前比喻的"指针",并不是 C/C++ 语言中那种指向内存物理地址的指针,而是指一个"全局唯一索引键 (Index Key) / 访问令牌 (Token)"。

1. 代码上的体现

它就体现在我们注册依赖属性时定义的那个 public static readonly 静态只读变量上。

复制代码
// 【这根"指针"就是 MyValueProperty 这个静态变量本身!】
public static readonly DependencyProperty MyValueProperty = 
    DependencyProperty.Register("MyValue", typeof(int), typeof(MyControl), new PropertyMetadata(0));

2. 底层运行原理(为什么需要这根指针?)

在传统 C# (CLR, Common Language Runtime,公共语言运行时) 类中,数据是存在实例的私有字段里的。但在 WPF 的 DependencyObject (依赖对象) 底层,它其实维护的是一个类似于字典 (Dictionary) 的"稀疏存储表" (Sparse Storage)。

  • 当你执行 实例对象.SetValue(MyValueProperty, 10) 时,底层的真实画面是这样的: WPF 引擎拿着 MyValueProperty 这个"指针(索引键)",去当前这个实例对象的内部字典里查找:

    • WPF 引擎问 :"这个对象的字典里,有没有 MyValueProperty 这把钥匙对应的记录?"

    • 如果没有 :就在字典里新建一条记录,Key 是 MyValueProperty,Value 是 10。

    • 如果有 :就覆盖旧的值,并触发后续的 PropertyChangedCallback (属性更改回调)。

总结: MyValueProperty 就是一张"门禁卡",XAML (Extensible Application Markup Language,可扩展应用程序标记语言) 解析器和绑定引擎在底层只认这张卡,根本不认识你写的 get 和 set 包装器。

get

一、 读取 (Get) 阶段:GetValue 之后的暗流涌动

你问到:"如果是 get 的读取该数据,就会调用 GetValue(MyValueProperty)?然后呢?" 然后,WPF 会开启一个极其精密且严格的"值解析优先级 (Value Resolution Strategy)"查找过程。

当代码执行到 GetValue 时,WPF 不会傻傻地只去内存里拿一个固定值,而是会按照从高到低的优先级像闯关一样去寻找这个值。只要在高优先级找到了,就立刻返回,不再往下找。

二、 核心原理:"从高到低的优先级"指的是什么?

在传统编程中,一个变量的值是多少,就是你最后一次赋给它的值(后来者居上)。 但在 WPF 中,一个属性的值,往往同时受到多方势力的"争夺"。

比如一个按钮的背景色 (Background):

  1. 系统默认是灰色的。

  2. 你在 Style (样式) 里规定所有按钮是蓝色的。

  3. 你在 Style Trigger (触发器) 里规定鼠标悬停时变成绿色的。

  4. 你在 XAML 里直接写死了 Background="Red"。

  5. 你用 Storyboard (故事板动画) 让它在红黄之间闪烁。

这么多人都要改这个颜色,听谁的? 这就必须有一套严格的"闯关机制"(Dependency Property Value Resolution Strategy,依赖属性值解析策略)。当代码调用 GetValue 时,WPF 会从最高优先级开始往下找,谁的官大(优先级高),就用谁的值,低优先级的统统被无视。

属性的继承

这是一个极具深度的问题!当我们在 WPF (Windows Presentation Foundation,Windows 演示基础) 中谈论"依赖属性的继承"时,90% 的新手都会陷入一个思维误区:把它等同于 C# 面向对象编程 (OOP, Object-Oriented Programming) 中的"类继承" (子类继承父类)。

但这完全是两码事!WPF 中的依赖属性继承,指的是"UI 树形结构上的值传递 (Value Inheritance)"------即父节点控件的值,可以自动顺着 UI 树流淌并覆盖给子节点控件。

当 WPF 渲染界面并调用 GetValue 去获取某个控件的属性时,会执行一套严格的优先级策略(我们之前提到过的 10 级闯关)。"属性继承 (Inheritance)"排在第 9 级(仅高于默认值)。

  • 双树遍历机制 :WPF 内部维护着两棵树:逻辑树 (Logical Tree) 和 视觉树 (Visual Tree)。

  • 依赖属性的继承,优先沿着逻辑树 (Logical Tree) 向上查找。如果一个元素不在逻辑树上,它才会尝试沿着视觉树去寻找。只要在向上的路途中,遇到任何一个祖先节点明确设置过这个属性的值,查找就会立刻停止,并把该值拿来己用。

如何自定义一个支持继承的依赖属性?

如果你自己写了一个自定义控件,希望它身上的某个属性也能像 FontSize 一样遗传给子孙后代,你需要使用 FrameworkPropertyMetadata 并开启一个极其关键的枚举标志:FrameworkPropertyMetadataOptions.Inherits。

完整代码演示(细节拉满,不跳步):

第一步:注册依赖属性并打上 Inherits 标记 注意,普通的属性一般用 PropertyMetadata,但为了支持继承这种高级 UI 特性,我们必须使用它的派生类 FrameworkPropertyMetadata。

复制代码
public class MyCustomPanel : StackPanel
{
    // 1. 注册依赖属性
    public static readonly DependencyProperty ThemeColorProperty =
        DependencyProperty.Register(
            "ThemeColor",                 // 属性名
            typeof(Brush),                // 属性类型
            typeof(MyCustomPanel),        // 宿主类型
            new FrameworkPropertyMetadata(
                Brushes.Gray,             // 默认值
                // 【核心关键】:开启 Inherits 标志!告诉 WPF 引擎这个属性允许向下流淌
                FrameworkPropertyMetadataOptions.Inherits, 
                OnThemeColorChanged       // 属性改变时的回调
            )
        );

    // 2. 编写 CLR (公共语言运行时) 包装器
    public Brush ThemeColor
    {
        get { return (Brush)GetValue(ThemeColorProperty); }
        set { SetValue(ThemeColorProperty, value); }
    }

    // 3. 回调函数
    private static void OnThemeColorChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        Console.WriteLine($"对象 {d.GetType().Name} 的主题色变为了 {e.NewValue}");
    }
}

附加属性

如果说普通的依赖属性是让控件"拥有"自己的数据,那么附加属性就是允许控件把数据像"便利贴"一样,贴在别的控件身上。

一、 核心概念与作用:什么是附加属性?

在传统的 面向对象编程 (OOP, Object-Oriented Programming) 中,如果一个对象需要某个属性,它必须在自己的类里定义。比如,如果 Button 想知道自己在网格的第几行,它就得有一个 Row 属性。 但是,如果每个控件都要把所有可能的布局属性(如 Grid 的行/列、Canvas 的左/上、DockPanel 的停靠方向)都写进基类 UIElement (用户界面元素) 里,这个基类会变得极其臃肿,而且大部分属性在大部分场景下根本用不到。

附加属性的作用,就是"解耦"与"按需贴标签"。

二、 核心使用场景

在 WPF 开发中,附加属性主要有两大极其重要的应用场景:

1. 布局系统 (Layout System) 这是微软官方最基础的用法。父容器定义附加属性,子元素设置这些属性。

  • Grid.Row / Grid.Column:告诉 Grid 自己该在哪个格子里。

  • Canvas.Top / Canvas.Left:告诉 Canvas 自己的绝对坐标。

  • DockPanel.Dock:告诉 DockPanel 自己该停靠在哪个边缘。

2. 附加行为 (Attached Behaviors) ------ 【进阶与核心杀手锏】 在 MVVM (Model-View-ViewModel,模型-视图-视图模型) 架构中,我们强烈抵制在后台代码 (Code-Behind) 中写 UI 逻辑。但有时候,我们需要给现有的第三方控件或者原生控件增加一些"它本身没有的功能",而又不想去写一个庞大的继承类(子类化)。

  • 场景举例 :你想让一个普通的 TextBox 拥有"失去焦点时自动全选文本"的功能,或者想给一个图片加上"支持鼠标拖拽"的功能。

  • 解决方案 :你可以写一个附加属性 DragBehavior.IsDraggable="True" 贴在图片上。当 WPF 引擎看到这个属性变为 True 时,附加属性的内部回调函数就会偷偷拿到这个图片对象,并为它挂载鼠标按下、移动、抬起的事件。无需继承,即插即用。

三、 完整步骤:如何自定义一个附加属性?

在 Visual Studio 中,你可以输入代码片段快捷键 propa 然后按两下 Tab 键,系统会自动生成标准模板。 与普通依赖属性最大的不同在于:普通依赖属性使用 CLR (公共语言运行时) 的 get 和 set 包装器;而附加属性使用静态的 Get... 和 Set... 方法。

下面我们以实现一个 "让任何控件点击时都会在控制台打印一句话" 的附加属性(IsClickLogEnabled)为例,进行不跳步的演示。

第一步:定义一个独立的静态类 附加属性不需要定义在控件内部,通常我们会专门建一个 Helper 或者 Behavior 类。

复制代码
public static class LogBehavior
{
    // 这里的代码写在第二步到第四步
}

第二步:注册附加属性 (使用 RegisterAttached) 必须使用 DependencyProperty.RegisterAttached,而不是 Register。

复制代码
// 命名规范:属性名 + "Property" 后缀
public static readonly DependencyProperty IsClickLogEnabledProperty =
    DependencyProperty.RegisterAttached(
        "IsClickLogEnabled",          // 1. 属性名
        typeof(bool),                 // 2. 属性的类型 (这里是 true/false)
        typeof(LogBehavior),          // 3. 拥有这个属性的类 (当前的静态类)
        new PropertyMetadata(false, OnIsClickLogEnabledChanged) // 4. 默认值(false)与回调函数
    );

第三步:编写静态的 Get 和 Set 方法 (强制规范) 因为附加属性是"贴"在别人身上的,所以你必须把被贴的对象作为参数传进去。

复制代码
// 提供给 XAML 引擎或者 C# 代码读取便利贴的方法
public static bool GetIsClickLogEnabled(DependencyObject obj)
{
    return (bool)obj.GetValue(IsClickLogEnabledProperty);
}

// 提供给 XAML 引擎或者 C# 代码贴便利贴的方法
public static void SetIsClickLogEnabled(DependencyObject obj, bool value)
{
    obj.SetValue(IsClickLogEnabledProperty, value);
}

第四步:在回调函数中实现"借尸还魂" (业务逻辑核心) 当你在 XAML 里把这个属性设为 True 时,这个方法就会触发。

复制代码
private static void OnIsClickLogEnabledChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    // d 就是那个被贴了便利贴的倒霉蛋(比如一个 Button)
    // 我们可以把它强转为 UIElement,因为 UIElement 才有相关的鼠标/键盘事件
    if (d is UIElement element)
    {
        bool isEnabled = (bool)e.NewValue;

        if (isEnabled)
        {
            // 如果设置为 True,我们偷偷给它挂载上鼠标按下的事件
            element.MouseDown += Element_MouseDown;
        }
        else
        {
            // 如果设置为 False,我们把事件卸载掉,防止内存泄漏
            element.MouseDown -= Element_MouseDown;
        }
    }
}

// 实际干活的方法
private static void Element_MouseDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
    Console.WriteLine($"对象 {sender.GetType().Name} 被点击了!");
}

第五步:在 XAML 中使用它 现在,你可以把这个附加属性贴在任何继承自 UIElement 的控件上了。

<!-- 引入你代码所在的命名空间 -->
<Window xmlns:local="clr-namespace:YourNamespace" ...>
    <StackPanel>
        <!-- 给 Button 贴上我们的附加属性 -->
        <Button Content="点我测试" local:LogBehavior.IsClickLogEnabled="True" />
        
        <!-- 甚至可以贴在没有 Click 事件的 TextBlock 上,因为我们底层绑定的是 MouseDown -->
        <TextBlock Text="文本也可以被记录" local:LogBehavior.IsClickLogEnabled="True" />
    </StackPanel>
</Window>

一个类必须继承DependencyObject

public class lessonDp:DependencyObject{

public static readonly DependencyProperty AgeProperty=

DependencyProperty.Register("Age",typeof(int),typeof(lessonDp),new PropertyMetadata(0)

}

public int Age{

get{return (int)GetValue(AgeProperty);}

set{SetValue(AgeProperty,Value);}

}

new PropertyMetadata(0,OnAgeChange)

值发生改变的时候执行回调

private static void OnAgeChange(DependencyObject d,DependencyChangedEventArgs e){

执行业务逻辑

}

验证回调

public static readonly DependencyProperty AgeProperty =

DependencyProperty.Register("Age",

typeof(int),

typeof(LessonDp),

new PropertyMetadata(0,OnAgeChange) ,

new ValidateValueCallback(OnvalidAge));

相关推荐
海马1 小时前
Spring Cloud 开发知识整理
spring·spring cloud·wpf
闻哥2 小时前
分布式任务调度框架:XXL-Job / ElasticJob / DolphinScheduler / SchedulerX 架构对比与选型
分布式·架构·wpf
预立科技15 小时前
CDN(内容分发网络)知识总结
网络·wpf·cdn
FuckPatience2 天前
WPF DataGrid 行根据某个属性循环变色动画
wpf
一条闲鱼_mytube4 天前
高并发短链接系统设计
wpf
一个天蝎座 白勺 程序猿5 天前
IoTDB集群扩容:从慌乱到从容的经验分享
数据库·wpf·时序数据库·iotdb
samble5 天前
从零搭建灌装监控系统(十三):报警系统设计,生命周期与通知
c#·wpf·mvvm·modbus·工业控制
samble6 天前
从零搭建灌装监控系统(十二):配置系统,原子写入与容错
c#·wpf·mvvm·modbus·工业控制
蜗牛互联网9 天前
OLMo-core 3的token gerrymandering提醒:MoE路由要按时间窗验收
java·人工智能·wpf