WPF 依赖属性速查手册

一、核心概念

术语 说明
依赖属性(Dependency Property) 通过 DependencyProperty.Register 注册的属性,存储在 WPF 属性系统中而非对象字段里
附加属性(Attached Property) 通过 DependencyProperty.RegisterAttached 注册,可附加到任意 DependencyObject
CLR 属性包装器 对依赖属性的 GetValue/SetValue 封装,提供与普通属性一致的访问语法
DependencyObject 所有可拥有依赖属性的 WPF 对象的基类(如 UIElementFrameworkElement
PropertyMetadata 属性元数据,定义默认值、值变更回调、强制值回调等
FrameworkPropertyMetadata PropertyMetadata 的子类,额外支持数据绑定、继承等框架级选项
Binding WPF 数据绑定机制,只能用于依赖属性,不能用于普通 CLR 属性

二、常用操作

常用方式速查

操作 关键 API 说明
注册依赖属性 DependencyProperty.Register("Name", typeof(T), typeof(Owner), metadata) 标准依赖属性注册
注册附加属性 DependencyProperty.RegisterAttached("Name", typeof(T), typeof(Owner), metadata) 可附加到任意控件
获取属性值 (T)GetValue(XxxProperty) 从属性系统读取值
设置属性值 SetValue(XxxProperty, value) 写入属性系统
监听变更 new PropertyMetadata(defaultVal, OnChanged) 在元数据中指定回调
强制双向绑定 FrameworkPropertyMetadataOptions.BindsTwoWayByDefault 框架级元数据选项

1. 注册依赖属性

cs 复制代码
public class MyControl : Control
{
    // 步骤1:注册依赖属性(必须是 static readonly)
    public static readonly DependencyProperty TitleProperty =
        DependencyProperty.Register(
            "Title",                    // 属性名
            typeof(string),             // 属性类型
            typeof(MyControl),          // 拥有者类型
            new PropertyMetadata(       // 元数据:默认值 + 变更回调
                string.Empty,           // 默认值
                OnTitleChanged          // 变更回调
            ));
​
    // 步骤2:CLR 包装器(用于代码中直接访问)
    public string Title
    {
        get => (string)GetValue(TitleProperty);
        set => SetValue(TitleProperty, value);
    }
​
    // 步骤3:变更回调(可选)
    private static void OnTitleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var control = (MyControl)d;
        // 响应属性变更逻辑
    }
}

2. 注册附加属性(PasswordBoxHelper 实战)

cs 复制代码
// 附加属性:可以"附加"到任何 DependencyObject 上
// 典型案例:PasswordBoxHelper 为 PasswordBox 添加绑定支持
// PasswordBox.Password 不是依赖属性,无法直接使用 {Binding},需通过附加属性间接绑定
​
using System.Windows;
using System.Windows.Controls;
​
public static class PasswordBoxHelper
{
    // 注册附加属性 Password
    public static readonly DependencyProperty PasswordProperty =
        DependencyProperty.RegisterAttached(
            "Password",                         // 属性名
            typeof(string),                     // 属性类型
            typeof(PasswordBoxHelper),          // 拥有者类型
            new FrameworkPropertyMetadata(
                string.Empty,                   // 默认值
                OnPasswordPropertyChanged       // 变更回调
            ));
​
    // 注册附加属性 Attach
    public static readonly DependencyProperty AttachProperty =
        DependencyProperty.RegisterAttached(
            "Attach",
            typeof(bool),
            typeof(PasswordBoxHelper),
            new PropertyMetadata(false, Attach));
​
    // 私有辅助属性:防止循环更新
    private static readonly DependencyProperty IsUpdatingProperty =
        DependencyProperty.RegisterAttached(
            "IsUpdating", typeof(bool), typeof(PasswordBoxHelper));
​
    // 必须提供静态 Get/Set 方法
    public static string GetPassword(DependencyObject dp)
        => (string)dp.GetValue(PasswordProperty);
​
    public static void SetPassword(DependencyObject dp, string value)
        => dp.SetValue(PasswordProperty, value);
​
    public static bool GetAttach(DependencyObject dp)
        => (bool)dp.GetValue(AttachProperty);
​
    public static void SetAttach(DependencyObject dp, bool value)
        => dp.SetValue(AttachProperty, value);
​
    private static bool GetIsUpdating(DependencyObject dp)
        => (bool)dp.GetValue(IsUpdatingProperty);
​
    private static void SetIsUpdating(DependencyObject dp, bool value)
        => dp.SetValue(IsUpdatingProperty, value);
​
    // 变更回调:同步 PasswordBox 的实际密码
    private static void OnPasswordPropertyChanged(DependencyObject sender,
        DependencyPropertyChangedEventArgs e)
    {
        if (sender is PasswordBox passwordBox)
        {
            passwordBox.PasswordChanged -= PasswordChanged;
            if (!(bool)GetIsUpdating(passwordBox))
                passwordBox.Password = (string)e.NewValue;
            passwordBox.PasswordChanged += PasswordChanged;
        }
    }
​
    private static void Attach(DependencyObject sender,
        DependencyPropertyChangedEventArgs e)
    {
        if (sender is PasswordBox passwordBox)
        {
            if ((bool)e.OldValue)
                passwordBox.PasswordChanged -= PasswordChanged;
            if ((bool)e.NewValue)
                passwordBox.PasswordChanged += PasswordChanged;
        }
    }
​
    private static void PasswordChanged(object sender, RoutedEventArgs e)
    {
        var passwordBox = (PasswordBox)sender;
        SetIsUpdating(passwordBox, true);
        SetPassword(passwordBox, passwordBox.Password);
        SetIsUpdating(passwordBox, false);
    }
}
<!-- XAML 中使用附加属性 -->
<PasswordBox Helper:PasswordBoxHelper.Attach="True"
             Helper:PasswordBoxHelper.Password="{Binding Password, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />

3. 自定义控件中的依赖属性实战(UCPager 分页控件)

cs 复制代码
using System.Text.RegularExpressions;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
​
public partial class UCPager : UserControl
{
    // 当前页码
    public static readonly DependencyProperty PageProperty =
        DependencyProperty.Register("Page", typeof(int), typeof(UCPager),
            new PropertyMetadata(0));
​
    public int Page
    {
        get => (int)GetValue(PageProperty);
        set
        {
            SetValue(PageProperty, value);
            RaisePageChangedEvent();           // 触发路由事件
            tbPageAndTotalPage.Text = $"{Page}/{TotalPage}";
            txtPage.Text = value.ToString();
            DisableButton();
        }
    }
​
    // 每页条数
    public static readonly DependencyProperty PageSizeProperty =
        DependencyProperty.Register("PageSize", typeof(int), typeof(UCPager),
            new PropertyMetadata(10));
​
    public int PageSize
    {
        get => (int)GetValue(PageSizeProperty);
        set => SetValue(PageSizeProperty, value);
    }
​
    // 总页数
    public static readonly DependencyProperty TotalPageProperty =
        DependencyProperty.Register("TotalPage", typeof(int), typeof(UCPager),
            new PropertyMetadata(0));
​
    public int TotalPage
    {
        get => (int)GetValue(TotalPageProperty);
        set
        {
            SetValue(TotalPageProperty, value);
            tbPageAndTotalPage.Text = $"{Page}/{TotalPage}";
            DisableButton();
        }
    }
​
    // 路由事件定义(配合依赖属性使用)
    public static readonly RoutedEvent PageChangedEvent =
        EventManager.RegisterRoutedEvent("PageChanged", RoutingStrategy.Tunnel,
            typeof(RoutedEventHandler), typeof(UCPager));
​
    public event RoutedEventHandler PageChanged
    {
        add => AddHandler(PageChangedEvent, value);
        remove => RemoveHandler(PageChangedEvent, value);
    }
​
    private void RaisePageChangedEvent()
        => RaiseEvent(new RoutedEventArgs(PageChangedEvent));
}
<!-- XAML 中绑定依赖属性 -->
<local:UCPager x:Name="ucPage1" Page="{Binding Page}" TotalPage="{Binding TotalPage}" />

4. 带变更回调的依赖属性(MyDataGrid)

cs 复制代码
using System.Windows;
using System.Windows.Controls;
​
[TemplatePart(Name = PART_Right, Type = typeof(DataGridScrollView))]
[TemplatePart(Name = DG_ScrollViewer, Type = typeof(ScrollViewer))]
public class MyDataGrid : DataGrid
{
    private const string PART_Right = "PART_Right";
    private const string DG_ScrollViewer = "DG_ScrollViewer";
​
    private DataGridScrollView _rightDataGrid;
    private ScrollViewer _rightScrollViewer;
    private ScrollViewer _scrollViewer;
​
    // 右侧冻结列数
    public static readonly DependencyProperty RightFrozenCountProperty =
        DependencyProperty.Register(
            nameof(RightFrozenCount),
            typeof(int),
            typeof(MyDataGrid),
            new PropertyMetadata(0, OnRightFrozenCountChanged));
​
    public int RightFrozenCount
    {
        get => (int)GetValue(RightFrozenCountProperty);
        set => SetValue(RightFrozenCountProperty, value);
    }
​
    // 静态回调方法:属性变更时重新排列列
    private static void OnRightFrozenCountChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (d is MyDataGrid dataGrid)
            dataGrid.HandleRightFrozenCountChanged();
    }
​
    private void HandleRightFrozenCountChanged()
    {
        if (_rightDataGrid == null) return;
​
        if (RightFrozenCount > 0)
        {
            // 将主 DataGrid 的列合并,再将最后 N 列移到右侧冻结区域
            for (int i = 0; i < _rightDataGrid.Columns.Count; i++)
            {
                var column = _rightDataGrid.Columns[i];
                _rightDataGrid.Columns.Remove(column);
                Columns.Add(column);
            }
            for (int i = 0; i < RightFrozenCount; i++)
            {
                var last = Columns[Columns.Count - 1];
                Columns.Remove(last);
                _rightDataGrid.Columns.Insert(0, last);
            }
            _rightDataGrid.SetCurrentValue(VisibilityProperty, Visibility.Visible);
        }
        else
        {
            _rightDataGrid.SetCurrentValue(VisibilityProperty, Visibility.Collapsed);
        }
    }
​
    static MyDataGrid()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(MyDataGrid),
            new FrameworkPropertyMetadata(typeof(MyDataGrid)));
    }
​
    public override void OnApplyTemplate()
    {
        base.OnApplyTemplate();
        _rightDataGrid = GetTemplateChild(PART_Right) as DataGridScrollView;
        _scrollViewer = GetTemplateChild(DG_ScrollViewer) as ScrollViewer;
        HandleRightFrozenCountChanged();
    }
}

5. PropertyMetadata 常用选项

选项 说明 示例
默认值 属性的初始值 new PropertyMetadata(0)
变更回调 值改变时触发的方法 new PropertyMetadata("", OnPropertyChanged)
强制值回调 在赋值前强制修正值 CoerceValueCallback
BindsTwoWayByDefault 默认双向绑定 new FrameworkPropertyMetadata("", FrameworkPropertyMetadataOptions.BindsTwoWayByDefault)
Inherits 属性值沿可视化树继承 FontSizeFontFamily 等内置属性

6. 依赖属性 vs 普通 CLR 属性

特性 依赖属性 普通 CLR 属性
数据绑定 ✅ 支持 XAML {Binding} ❌ 不支持
样式/模板 ✅ 可在 Style/Template 中设置 ❌ 不支持
动画 ✅ 支持 Storyboard 动画 ❌ 不支持
默认值 ✅ 通过 PropertyMetadata 设置 需在构造函数中赋值
属性变更通知 ✅ 内置回调机制 需手动实现 INotifyPropertyChanged
内存开销 ✅ 按需存储,共享默认值 每个实例都占用字段内存
继承 ✅ 支持沿可视化树继承 ❌ 不支持

三、问题排查

错误1:XamlParseException --- 不能在属性上设置 Binding

  • 现象System.Windows.Markup.XamlParseException: "不能在"PasswordBox"类型的"Password"属性上设置"Binding""

  • 原因Password 不是依赖属性,WPF 绑定系统只能绑定到 DependencyProperty

  • 解决 :使用附加属性 PasswordBoxHelperPasswordBox 添加绑定能力(见第二节第2小节)

错误2:依赖属性注册名称与 CLR 包装器不一致

  • 现象:属性赋值无效或绑定失败

  • 原因DependencyProperty.Register 的第一个参数必须与 CLR 包装器属性名完全一致

  • 解决 :确保 Register("MyProp", ...) 对应 public xxx MyProp { get/set }

cs 复制代码
// 错误:名称不匹配
public static readonly DependencyProperty TitleProperty =
    DependencyProperty.Register("title", typeof(string), typeof(MyControl), ...); // 小写 t
​
public string Title  // 大写 T ------ 不匹配!
{
    get => (string)GetValue(TitleProperty);
    set => SetValue(TitleProperty, value);
}
​
// 正确:名称一致
public static readonly DependencyProperty TitleProperty =
    DependencyProperty.Register("Title", typeof(string), typeof(MyControl), ...);

错误3:在 CLR 包装器中添加额外逻辑导致问题

  • 现象 :在 get/set 中添加验证或业务逻辑后,动画或绑定行为异常

  • 原因 :WPF 动画和绑定系统直接调用 GetValue/SetValue,绕过 CLR 包装器

  • 解决 :将额外逻辑放入 PropertyChangedCallback 而非 CLR 包装器中

cs 复制代码
// 推荐:变更逻辑放在回调中,CLR 包装器保持简洁
public static readonly DependencyProperty PageProperty =
    DependencyProperty.Register("Page", typeof(int), typeof(UCPager),
        new PropertyMetadata(0, OnPageChanged));
​
public int Page
{
    get => (int)GetValue(PageProperty);
    set => SetValue(PageProperty, value);  // 保持简洁
}
​
private static void OnPageChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    // 所有响应逻辑在这里
}

注意 :项目中 UCPager.Page 的 CLR 包装器 setter 包含了 UI 更新逻辑(如 DisableButton()),这是因为该控件需要在代码中直接赋值 Page 时同步更新 UI。如果只通过 XAML 绑定赋值,建议改用 PropertyChangedCallback 实现。

错误4:附加属性 Get/Set 方法签名错误

  • 现象:编译错误或 XAML 中无法使用附加属性

  • 原因 :附加属性必须提供 public staticGetXxx / SetXxx 方法,参数为 DependencyObject

  • 解决:严格按照签名规范实现

cs 复制代码
// 必须的签名格式
public static string GetMyProperty(DependencyObject dp)
    => (string)dp.GetValue(MyPropertyProperty);

public static void SetMyProperty(DependencyObject dp, string value)
    => dp.SetValue(MyPropertyProperty, value);
相关推荐
JaydenAI1 小时前
[MAF预定义ChatClient中间件-09]MessageInjectingChatClient-赋予工具消息注入的能力
ai·c#·agent·maf·ichatclient
学计算机的计算基1 小时前
MySQL 锁体系全解:从 MDL 到间隙锁,一次讲透
java·数据库·笔记·python·mysql
Engineer邓祥浩1 小时前
宏观认知(4):AI与社会——吴恩达《AI for Everyone》Week4学习笔记
人工智能·笔记·学习
imDwAaY1 小时前
从非线性分类到多层神经网络 CS188 Note21 学习笔记
人工智能·笔记·python·神经网络·学习·机器学习·分类
凌波粒1 小时前
深度学习入门(鱼书)第4章笔记——神经网络的学习
笔记·深度学习·神经网络
Xin_ye100861 小时前
C# 零基础到精通教程 - WPF 深度专题:3D 图形与视觉增强
开发语言·c#·wpf
zhangfeng11332 小时前
台大李宏毅老师讲解memba和类似linear atttenion 模型,笔记
开发语言·人工智能·笔记
Chris _data3 小时前
并发单词频率统计器 - 从零到完整实现(C# 实战)
开发语言·c#
AOwhisky13 小时前
Ceph系列第六期:Ceph 文件系统(CephFS)精讲
linux·运维·网络·笔记·ceph