WPF新手村教程(二) - 铁匠铺攻略:如何给隔壁张铁匠带两块铁(依赖属性)

WPF - 依赖属性 & 附加属性


回顾 - 属性 & 属性元素 & 对象元素

	// C# -> 属性
    public string Name
    {
        set => name = value;
        get => name;
    }
<!-- WPF -->
<控件>
    <!-- 属性元素 -->
	<控件.属性名>
        <!-- 对象元素 -->
        <属性类型 属性名="属性值"/>
    </控件.属性名>   
</控件>	

<!-- 示例代码 -->
<Button>
    <Button.Background>
        <SolidColorBrush Color="Red"/>
    </Button.Background>
</Button>

一.依赖属性

先放结论,留个印象
🍀依赖属性 —— 使用普通属性语法控制WPF中属性值的一种机制

1.定义 - 微软官方

通常,官方文档讲的很好,虽然我不知道他咕噜咕噜地在说什么
所以,我们对于理解概念,最好的方法就是请求中译中

  • 依赖属性

    • 依赖属性是一种由 WPF 属性系统统一管理的属性(由DependencyProperty支持的属性),
      其值并不直接存储在对象实例中,而是由属性系统根据多种输入源按优先级计算得出
  • 依赖属性标识符

    • 它是 DependencyProperty 注册依赖属性时作为返回值的实例,然后存储为类的静态成员
      与 WPF 属性系统交互的许多 API 使用依赖属性标识符作为参数
  • CLR"包装器"getset对属性的实现

    • 这些实现通过在GetValueSetValue调用中使用依赖项属性标识符来整合该标识符,
      使WPF属性系统为该属性提供支持
  • public static readonly DependencyProperty IsSpinningProperty = DependencyProperty.Register(
        	"IsSpinning",
        	typeof(bool),
        	typeof(MainWindow)
        );
    
    public bool IsSpinning
    {
        get => (bool)GetValue(IsSpinningProperty);
        set => SetValue(IsSpinningProperty, value);
    }
    

2.定义 - 中译中后

在看定义之前,先来看两段代码,一个是C#中普通属性的示例代码,一个是WPF的依赖属性的示例代码

	// C# -> 普通属性(CLR普通属性)
    public string Name
    {
        set => name = value;
        get => name;
    }


/* ================================================================================= */

	// WPF -> 依赖属性
	// 输入propdp,然后连续按两下Tab键,VS会帮我们自动生成下面的这段代码
    public int MyProperty
    {
        get { return (int)GetValue(MyPropertyProperty); }
        set { SetValue(MyPropertyProperty, value); }
    }

    // Using a DependencyProperty as the backing store for MyProperty.  
    // This enables animation, styling, binding, etc...
    public static readonly DependencyProperty MyPropertyProperty =
        DependencyProperty.Register(
        "MyProperty",
        typeof(int),
        typeof(ownerclass),
        new PropertyMetadata(0));

然后,初见依赖属性的这段代码,大多数人都是看不懂的,我们来逐行解释一下

  • 	// MyProperty成员,使用CLR属性包装器实现get和set => 参考普通属性
    	public int MyProperty
        {
            // 并且使用了GetValue 和 SetValue成员 读/写 依赖属性 MyPropertyProperty
            // GetValue和SetValue -> 由DependencyObject类定义的
            // GetValue -> 获取依赖属性值
            // SetValue -> 写入依赖属性值
            get { return (int)GetValue(MyPropertyProperty); }
            set { SetValue(MyPropertyProperty, value); }
        }
    
        // Using a DependencyProperty as the backing store for MyProperty.  
        // This enables animation, styling, binding, etc...
    	// 翻译一下官方的这两句话:
            // 使用依赖属性作为 MyProperty 的存储方式
    			// 再翻译一下:MyProperty 的真实存储位置是一个 DependencyProperty,而不是字段
            // 从而启用动画、样式、数据绑定等功能
    
    	/* 注册int类型的依赖属性 MyProperty */
    	// 被声明为 DependencyProperty 类型的静态只读类型的 MyPropertyProperty 成员
    	// 使用 DependencyProperty 的Register方法注册
        public static readonly DependencyProperty MyPropertyProperty =
            DependencyProperty.Register(
            "MyProperty",				// 属性名,必须与上面的包装属性名称一致
            typeof(int),				// 属性类型
            typeof(ownerclass),			 // 属性所属类型 -> 这个属性"属于谁"
            new PropertyMetadata(0));	 // 默认值   -> 可以不写
    
  • 但是上面只是一个依赖属性的模板,下面是一个实际的代码案例

    •     // 依赖属性(读取和写入) -> 具备自动通知界面的能力
          public string MyPasswordVM
          {
              get { return (string)GetValue(MyPasswordVMProperty); }
              set { SetValue(MyPasswordVMProperty, value); }
          }
      
          // 注册string类型的依赖属性 MyPasswordVM
          public static readonly DependencyProperty MyPasswordVMProperty =
              DependencyProperty.Register(
                  "MyPasswordVM",         // 属性名,必须与上面的包装属性名称一致
                  typeof(string),         // 属性类型
                  typeof(MainWindow));    // 所属类型
      

最后,我们再来看看概念,以及官方咕噜咕噜的那段话是什么意思

  • CRL包装器:说白了就是类似于普通属性的getset,让依赖属性也可以像普通属性一样使用
  • 依赖属性标识符:就是使用 DependencyPropertyRegister 方法注册 了一个依赖属性标识符
    • 依赖属性标识符,是给WPF使用的,例如上面的 MyPasswordVMProperty
    • CRL包装属性才是给程序员们使用的,例如上面的 MyPasswordVM
  • 依赖属性:
    • 依赖属性不是“特殊的属性写法”,而是 WPF 将属性值的控制权从对象实例中抽离,交由属性系统统一调度的一种机制
      • 即:🍀使用普通属性语法控制WPF中属性值的一种机制
        • 当然,我们可以让这句话 复杂一点 官方一点:
          • 依赖属性是一种使用普通 CLR 属性语法,来交由 WPF 属性系统统一控制属性值的机制

[!TIP]

一种更简单明了的理解

这里在B站发现了一个up的视频,他的理解方式能更好的让我们理解依赖属性

视频链接:17.第4章_依赖属性定义_哔哩哔哩_bilibili

 // 依赖属性 -> 定义,注册,保证

 /* 1.定义 */
 public static readonly DependencyProperty MyPasswordVMProperty =
     /* 2.注册 */
     DependencyProperty.Register(
         "MyPasswordVM",         // 属性名,必须与上面的包装属性名称一致
         typeof(string),         // 属性类型
         typeof(MainWindow));    // 所属类型

/* 3.包装 */
 public string MyPasswordVM
 {
     get { return (string)GetValue(MyPasswordVMProperty); }
     set { SetValue(MyPasswordVMProperty, value); }
 }

3.依赖属性的回调函数

在微软官方文档中,关于依赖属性回调和验证,提到了三种不同的回调:验证值回调,属性更改回调,强制转换值回调下面,我们姑且叫他们验证回调改变回调强制回调这样叫,挺好听的不是吗

[!WARNING]

有经验比较丰富的老程序员在他们的教程中提到,
工作中,改变回调是使用比较多的,验证回调和强制回调使用是比较少的,因为验证和强制回调都有可替代的方案

在依赖属性和附加属性这里,我感觉我浪费的很多的时间来学习,一开始非常蒙圈
然后现在也感觉是懵懵懂懂的,但是大致还是理解了基本的概念

真的就像之前的一个.net(跟着我一起念,刀奈特,字正圆腔)程序员告诉我的:
WPF这种东西,你会的那就是会,不会的看半天也不会

为了写这一篇随笔,感觉来来回回反反复复改了很久,之前总是在吐槽网上的教程为什么总是讲不明白
现在,哈!轮到我了!
WPF这东西看文档,真不如自己多敲几遍,然后反复在教程和代码反复横跳几次才会明白.......

[2025.12.26] 留言:不对啊,反正都是上班摸鱼,我叹什么气啊,接着摸鱼接着敲啊

// 我们先用下面的一长段代码理解一下API的使用,不需要太过于理解,只是让我们知道这个东西是什么即可
// 你要是不想看也不是不行,直接看下面的表格,也不会有太大的影响其实
public class PropertyMetadata
{
    public PropertyMetadata();
    public PropertyMetadata(object defaultValue);
    public PropertyMetadata(PropertyChangedCallback propertyChangedCallback);
    public PropertyMetadata(object defaultValue, PropertyChangedCallback propertyChangedCallback);
    public PropertyMetadata(object defaultValue, 
                            PropertyChangedCallback propertyChangedCallback,
                            CoerceValueCallback coerceValueCallback);

    public object DefaultValue { get; set; }
    public PropertyChangedCallback PropertyChangedCallback { get; set; }
    public CoerceValueCallback CoerceValueCallback { get; set; }
    protected bool IsSealed { get; }

    protected virtual void Merge(PropertyMetadata baseMetadata, DependencyProperty dp);
    protected virtual void OnApply(DependencyProperty dp, Type targetType);
}


// 注释版
public class PropertyMetadata
{
    // ================= 构造函数 =================
	// 默认构造函数 -> 无默认值和回调
    public PropertyMetadata();
    
	// 指定 属性的默认值 -> 当没有本地值、绑定值或样式时,依赖属性会使用这个默认值
    public PropertyMetadata(object defaultValue);
    // 指定 改变回调 -> 当属性值真正发生变化时触发,用于通知或更新 UI
    public PropertyMetadata(PropertyChangedCallback propertyChangedCallback);
	// 指定 默认值和 改变回调
    public PropertyMetadata(object defaultValue, PropertyChangedCallback propertyChangedCallback);
    // 指定 默认值、属性改变回调和强制值回调
    // 强制回调 -> 用于在值写入之前动态调整或修正属性值
    public PropertyMetadata(object defaultValue, 
                            PropertyChangedCallback propertyChangedCallback,
                            CoerceValueCallback coerceValueCallback);

    // ================= 属性 =================
    // 属性的默认值 -> 当没有本地值、绑定值或样式提供值时使用
    public object DefaultValue { get; set; }
    
	// 改变回调 -> 属性值真正变更后触发
    // 用于执行副作用,如刷新 UI 或同步其他属性
    public PropertyChangedCallback PropertyChangedCallback { get; set; }

    // 强制回调 -> 在值写入属性系统之前调用
    // 可以根据对象状态调整值,确保属性值的一致性或约束
    public CoerceValueCallback CoerceValueCallback { get; set; }

    // 元数据是否已经被注册到 DependencyProperty 上
    // 一旦注册,元数据不可修改,保证属性系统稳定
    protected bool IsSealed { get; }

    // ================= 保护方法 =================
    // 当依赖属性继承元数据(子类依赖属性覆盖父类依赖属性)时调用
    // 用于合并父类元数据和当前元数据
    protected virtual void Merge(PropertyMetadata baseMetadata, DependencyProperty dp);
    // 当元数据应用到某个依赖属性上时调用
    // 框架内部使用,一般开发者不需要调用
    protected virtual void OnApply(DependencyProperty dp, Type targetType);
}

回调类型 概念 调用时机 可访问内容 作用
验证回调(Validate) 检查值是否合法 写入前 value 不合法则抛异常,阻止写入
强制回调(Coerce) 调整值 写入前 value + 对象实例 修正或约束值,保证一致性
改变回调(PropertyChanged) 响应变化 写入后 旧值 + 新值 + 对象实例 执行副作用,如刷新 UI、同步其他属性

[!IMPORTANT]

⏳这是某个依赖属性值的一生

  • 有人试图给属性一个新值
    • 1.验证回调
    • 2.强制回调
    • 3.真正写入属性系统
    • 4.改变回调
  • 能不能进(验证回调) → 要不要改(强制回调) → 已经变了(改变回调)

(1)验证回调 -> 判断值是否合法 -> 看看能不能给人放进去


(2)改变回调 -> 数值改变后通知(响应变化) -> 我只能告诉你事情已经发生了,你也阻止不了了,不是吗


(3)强制回调 -> 数值未在合理范围,强制赋予一个数值 -> 200块钱还想买内存条?加钱!!!必须2000!!!


(4)三种回调示例代码

  • 这里的代码仅做教学参考使用,所以并没有太大的实际意义(创建一个WPF项目,替换你的MainWindow.xaml.cs文件便可食用)

  • using System;
    using System.Windows;
    
    namespace Test
    {
        /// <summary>
        /// MainWindow.xaml 的交互逻辑
        /// </summary>
        public partial class MainWindow : Window
        {
            public MainWindow()
            {
                InitializeComponent();
    
                // 测试验证回调 -> 会抛异常,没事干记得注释掉
                // Test = "";
    
                // 测试强制回调 -> 会,没事干记得注释掉
                //Test = "123456789012345678901234567890";
    
                // 测试改变回调 -> 你敢变我就敢()
                Test = "11";
                Test = "22";
    
            }
    
            public string Test
            {
                get { return (string)GetValue(TestProperty); }
                set { SetValue(TestProperty, value); }
            }
    
            public static readonly DependencyProperty TestProperty =
                DependencyProperty.Register(
                    "Test",                         // 属性名,必须与上面的 CLR 包装属性一致
                    typeof(string),                 // 属性类型
                    typeof(MainWindow),             // 所属类型
                    new PropertyMetadata(
                        "1",                          // 默认值
                        OnTestChanged,       // 改变回调
                        CoerceTest           // 强制回调
                    ),
                    ValidateTest             // 验证回调
                );
    
            // ==================== 三种回调 ====================
            // 1️.验证回调 -> 在值进入属性系统之前进行合法性检查
            private static bool ValidateTest(object value)
            {
                // 禁止写入空值
                string s = value as string;
                return !string.IsNullOrEmpty(s);
            }
    
            // 2️.改变回调 -> 属性值真正生效后触发,用于通知或执行副作用
            private static void OnTestChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
            {
                MainWindow owner = d as MainWindow;
                string oldValue = (string)e.OldValue;
                string newValue = (string)e.NewValue;
    
                Console.WriteLine($"Test 变化:{oldValue} -> {newValue}");
            }
    
            // 3️.强制回调 -> 在值写入属性系统前对值进行调整
            private static object CoerceTest(DependencyObject d, object baseValue)
            {
                string s = baseValue as string;
                if (s != null && s.Length > 20)
                {
                    s = s.Substring(0, 20);
                }
    
                Console.WriteLine("TT当前数值:" + s);
                return s;
            }
        }
    }
    

4.依赖属性的实际使用(场景示例)- 自定义控件

通过自定义控件 MyControl 定义了一个依赖属性 MyTitle
并在 XAML 中进行声明式赋值,在代码中动态修改,
MyTitle属性变化时通过改变回调将主窗口标题也改变为相同文字

这是一个 依赖属性从定义 → 使用 → 变化 → 影响 UI 的最小闭环示例,
用来演示依赖属性如何作为状态源,驱动界面行为,而不是被动控件刷新

用人话来说,就是你可以通过类似的写法,实现一些软件中的UI操作
例如:当你鼠标进入某控件时进行颜色改变

# 目录
Test
├─ MainWindow.xaml        # 界面声明:创建并使用 MyControl,在 XAML 中给依赖属性 MyTitle 赋初值
├─ MainWindow.xaml.cs     # 行为逻辑:运行时修改 MyControl.MyTitle,触发依赖属性变化
└─ MyControl.cs           # 控件定义:注册依赖属性 MyTitle,并在属性变化时执行对应逻辑
/* MyControl.cs */
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;

namespace Test
{
    public class MyControl : Control
    {
        static MyControl()
        {
            // 指定默认样式,如果有自定义样式可以用这个
            DefaultStyleKeyProperty.OverrideMetadata(
                typeof(MyControl),
                new FrameworkPropertyMetadata(typeof(MyControl)));
        }


        public static readonly DependencyProperty MyTitleProperty =
            DependencyProperty.Register(
                "MyTitle",                      // 属性名
                typeof(string),               // 属性类型
                typeof(MyControl),            // 所属类
                new PropertyMetadata(         // 元数据
                    "默认标题",               // 默认值
                    new PropertyChangedCallback(OnMyTitleChanged)) // 改变回调
            );

        public string MyTitle
        {
            get => (string)GetValue(MyTitleProperty);
            set => SetValue(MyTitleProperty, value);
        }

        // 改变回调
        private static void OnMyTitleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var control = d as MyControl;
            // 当属性变化时打印新值
            System.Diagnostics.Debug.WriteLine($"MyTitle 改变为:{e.NewValue}");

            // 更新主窗口标题
            if (control != null)
            {
                // 找到依赖属性所在的窗口
                var window = Window.GetWindow(control);
                if (window != null)
                {
                    window.Title = e.NewValue?.ToString(); // 更新主窗口标题
                }
            }
        }
    }
}
/* MainWindow.xaml.cs */
using System;
using System.Windows;

namespace Test
{
    /// <summary>
    /// MainWindow.xaml 的交互逻辑
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            // myControl 是 MainWindow.xaml 中定义的控件实例
            // 对应创建的自定义控件 MyControl
            myControl.MyTitle = "我是窗口";
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            // 点击按钮时更新依赖属性
            myControl.MyTitle = textBox.Text;
        }
    }
}
<Window x:Class="Test.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:Test"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    
    <StackPanel Margin="20">
        <!-- 利用依赖属性生成一个自定义控件myControl -->
        <!-- local:MyControl x:Name="myControl" => 在Test命名空间中找一个叫MyControl的类 -->
        <!-- 这里的依赖属性是Title -->
        <local:MyControl x:Name="myControl" MyTitle="初始标题" Height="50"/>
        <!-- 一个文本框 -->
        <TextBox x:Name="textBox" Margin="0,10,0,0" Height="30"/>
        <!-- 一个按钮 -->
        <Button Content="更新标题" Click="Button_Click" Height="30"/>
    </StackPanel>
</Window>

二.附加属性

1.定义 - 微软官方

  • 附加属性
    • 附加属性允许在派生自DependencyObject的任何 XAML 元素上设置额外的属性/值对
      • 即使该元素在其对象模型中未定义这些额外属性
      • 额外属性可全局访问
    • 附加属性通常定义为一种没有传统属性包装器的特定类型的依赖属性

他到底在说什么抽象东西啊


2.定义 - 中译中

  • 附加属性

    • 附加属性 = 把属性加到一个并不属于它的对象上

      • 换句话说就是:一个原来没有属性A的对象,你给别人硬生生添加了上去

        • 附加:原来没有的东西你硬生生给别人加了上去的东西
    • 示例:(假设我们随便绑定了一个,先别管它对不对,反正就是绑了2个属性上去)

    • <Grid>
          <Button Grid.Row="1" Grid.Column="2"/>
      </Grid>
      

    • 附加属性是一种特殊的依赖属性

      • 普通属性是不能进行绑定的,可以绑定的数据至少也要是依赖属性

3.附加属性 VS 依赖属性

先来看附加属性语法

	// WPF -> 依赖属性
	// 输入propdp,然后连续按两下Tab键,VS会帮我们自动生成下面的这段代码


    public int MyProperty
    {
        get { return (int)GetValue(MyPropertyProperty); }
        set { SetValue(MyPropertyProperty, value); }
    }

    // Using a DependencyProperty as the backing store for MyProperty.  
    // This enables animation, styling, binding, etc...
    public static readonly DependencyProperty MyPropertyProperty =
        DependencyProperty.Register(
        "MyProperty",
        typeof(int),
        typeof(ownerclass),
        new PropertyMetadata(0));

然后是附加属性语法

  • 为什么附加属性要使用Getxx()Setxx()函数,而不是像依赖属性一样使用CLR 包装属性?

    • 因为普通的依赖属性使用的是自己的属性,所以可以使用属性的语法直接读取和修改
      • 即:附加属性不属于被设置的对象类型,因此不可能有实例属性
  • 为什么附加属性注册要使用RegisterAttached而不是Register

    • Register:注册 => 声明一个“类型自身拥有”的依赖属性

    • RegisterAttached:注册 + 附加 => 声明一个“由某类型定义、但设计为可附加到其他对象”的依赖属性

      • 对属性“所有权模型”的声明
    • // 这个属性,名义上属于 MyControl 类型
      DependencyProperty.Register(
              "MyProperty",
              typeof(int),
              typeof(MyControl),
          ...)
      
       // 这个属性由 Grid 定义, 但可以附加到任何 DependencyObject 上   
       DependencyProperty.RegisterAttached(
              "MyProperty",
              typeof(int),
              typeof(Grid),
          ...)   
      
 	// WPF -> 附加属性
	// 输入propa,然后连续按两下Tab键,VS会帮我们自动生成下面的这段代码   


	public static int GetMyProperty(DependencyObject obj)
    {
        return (int)obj.GetValue(MyPropertyProperty);
    }

    public static void SetMyProperty(DependencyObject obj, int value)
    {
        obj.SetValue(MyPropertyProperty, value);
    }

    // Using a DependencyProperty as the backing store for MyProperty.  
    //This enables animation, styling, binding, etc...
    public static readonly DependencyProperty MyPropertyProperty =
        DependencyProperty.RegisterAttached(
            "MyProperty",
            typeof(int),
            typeof(ownerclass),
            new PropertyMetadata(0));

4.示例代码 —— VMMV和非VMMV架构的附加属性使用

(1)非VMMV架构

# 目录
TT
├─ MainWindow.xaml        # 界面声明 => 定义 PasswordBox,并使用 PasswordHelper 的附加属性进行绑定
├─ MainWindow.xaml.cs     # 交互逻辑 => 作为界面代码后置,负责读取附加属性值并响应按钮事件
└─ PasswordHelper.cs      # 附加属性定义 => 提供 Password 附加属性,用于在 PasswordBox 与外部逻辑之间同步密码

MainWindow.xaml

<Window x:Class="TT.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:TT"
        mc:Ignorable="d"
        Title="我不是标题" Height="450" Width="800">
    <StackPanel Margin="20">
        <PasswordBox x:Name="pwdBox"
                     local:PasswordHelper.Password=""/>

        <Button Content="读取密码"
                Margin="0,10,0,0"
                Click="Button_Click"/>
    </StackPanel>
</Window>
/* MainWindow.xaml.cs */

using System.Windows;

namespace TT
{
    /// <summary>
    /// MainWindow.xaml 的交互逻辑
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            // 获取附加属性值
            string password = PasswordHelper.GetPassword(pwdBox);
            // 消息弹窗打印
            MessageBox.Show($"当前密码:{password}");
        }
    }
}
/* PasswordHelper.cs */

using System;
using System.Windows;
using System.Windows.Controls;

namespace TT
{
    public static class PasswordHelper
    {
        public static readonly DependencyProperty PasswordProperty =
            DependencyProperty.RegisterAttached(
                "Password",
                typeof(string),
                typeof(PasswordHelper),
                // string.Empty相当于默认值 = ""
                // C/C++程序员请注意:C#中 "" ≠ null 
                // new PropertyMetadata(string.Empty, OnPasswordChanged));
                new PropertyMetadata(null, OnPasswordChanged));

        public static string GetPassword(DependencyObject obj)
        {
            return (string)obj.GetValue(PasswordProperty);
        }

        public static void SetPassword(DependencyObject obj, string value)
        {
            obj.SetValue(PasswordProperty, value);
        }

        // 改变回调
        private static void OnPasswordChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            if (d is PasswordBox passwordBox)
            {
                // 同步到真实 Password
                if (passwordBox.Password != (string)e.NewValue)
                {
                    passwordBox.Password = (string)e.NewValue;
                }

                // 注册事件(先移除,避免重复)
                passwordBox.PasswordChanged -= PasswordBox_PasswordChanged;
                passwordBox.PasswordChanged += PasswordBox_PasswordChanged;
            }
        }

        private static void PasswordBox_PasswordChanged(object sender, RoutedEventArgs e)
        {
            var passwordBox = (PasswordBox)sender;
            SetPassword(passwordBox, passwordBox.Password);
        }
    }
}

(2)MVVM架构

为什么要在这里讲MVVM架构,因为突然发现附加属性和MVVM结构好像关系比较密切
所以在这里可以留个印象或者概念,后面的文章中会深入讲解MVVM架构
你问什么时候?春节之后吧.......
之前就接触过QT的一套图形界面,和WFM有点相识,谁说前端简单的,我要把他的头拧下来......

PasswordBox.Password  ←→  附加属性 Password  ←→  ViewModel.VMod.Password

MVVM
├─ App.xaml                     # 应用入口:声明应用级资源并指定启动窗口
│
├─ Views
│  ├─ MainWindow.xaml           # 界面声明:定义 PasswordBox,并通过附加属性绑定 ViewModel 的 Password
│  └─ MainWindow.xaml.cs        # 界面逻辑:初始化窗口并设置 DataContext
│
├─ ViewModel
│  └─ VMod.cs    			   # 视图模型:定义 Password 状态并实现属性变更通知
│
└─ Helpers
   └─ PasswordHelper.cs         # 附加属性定义:桥接 PasswordBox 与可绑定的字符串属性

MainWindow.xaml

<Window x:Class="MVVM.Views.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:MVVM.Helpers"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <StackPanel Margin="20">
        <PasswordBox
            local:PasswordHelper.Password="{Binding Password, Mode=TwoWay}" />

        <Button Content="读取密码"
                Margin="0,10,0,0"
                Click="Button_Click"/>
    </StackPanel>
</Window>
using System.Windows;
using MVVM.ViewModel;
using MVVM.Helpers;

namespace MVVM.Views
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            DataContext = new VMod();
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            var vm = (VMod)DataContext;
            MessageBox.Show(vm.Password);
        }
    }
}
/* PasswordHelper.cs */

using System.Windows;
using System.Windows.Controls;

namespace MVVM.Helpers
{
    public static class PasswordHelper
    {
        public static readonly DependencyProperty PasswordProperty =
            DependencyProperty.RegisterAttached(
                "Password",
                typeof(string),
                typeof(PasswordHelper),
                new FrameworkPropertyMetadata(
                    string.Empty,
                    //null,
                    FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
                    OnPasswordChanged));

        public static void SetPassword(DependencyObject obj, string value)
        {
            obj.SetValue(PasswordProperty, value);
        }

        public static string GetPassword(DependencyObject obj)
        {
            return (string)obj.GetValue(PasswordProperty);
        }

        private static void OnPasswordChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            if (d is not PasswordBox box)
                return;

            box.PasswordChanged -= PasswordBox_PasswordChanged;

            if (box.Password != (string)e.NewValue)
            {
                box.Password = (string)e.NewValue;
            }

            box.PasswordChanged += PasswordBox_PasswordChanged;
        }

        private static void PasswordBox_PasswordChanged(object sender, RoutedEventArgs e)
        {
            var box = (PasswordBox)sender;
            SetPassword(box, box.Password);
        }
    }
}
/* VMod.cs */

using System.ComponentModel;

namespace MVVM.ViewModel
{
    public class VMod: INotifyPropertyChanged
    {
        private string? _password;

        public string? Password
        {
            get => _password;
            set
            {
                if (_password == value) return;
                _password = value;
                OnPropertyChanged(nameof(Password));
            }
        }

        public event PropertyChangedEventHandler? PropertyChanged;

        protected void OnPropertyChanged(string name)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
        }
    }
}

[!TIP]

为什么附加属性的使用经常使用MVVM架构?


三.小结

  • 依赖属性:
    • 依赖属性不是“特殊的属性写法”,而是 使用普通属性语法控制WPF中属性值的一种机制
      • 即:依赖属性是一种使用普通 CLR 属性语法,来交由 WPF 属性系统统一控制属性值的机制
    • 应用场景:
      • 1.三种回调:验证回调,改变回调,强制回调
      • 2.附加属性(附加属性是一种特殊的依赖属性)
      • 3.自定义控件属性
      • 4.属性绑定
      • 5.继承
  • 附加属性:
    • 附加属性 = 把属性加到一个并不属于它的对象上
      • 换句话说就是:一个原来没有属性A的对象,你给别人硬生生添加了上去
维度 依赖属性 附加属性
属性语义 控件自身状态 外部系统附加的信息
所有权 对象自己 定义者 ≠ 承载者
XAML 使用 Control.Prop Owner.Prop
典型用途 UI 状态 / API 布局 / 行为 / 桥接
是否可互换

小登资历尚浅,如有错误还望提出

本篇随笔参考:
1.什么是依赖属性 - WPF中文网 - 从小白到大佬
2.WPF | Microsoft Learn
3.17.第4章_依赖属性定义_哔哩哔哩_bilibili

posted @ 2026-01-13 09:52  假设狐狸有信箱  阅读(51)  评论(0)    收藏  举报