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);
相关推荐
唐青枫9 小时前
线程不是越多越快:C#.NET Thread 生命周期、同步与后台工作线程实战
c#·.net
唐青枫1 天前
别只会反射:C#.NET Emit 动态生成代码实战详解
c#·.net
咕白m6251 天前
.NET 环境下 Word 超链接批量提取方案
c#·.net
用户91721561902111 天前
C# 通信协议增量解析:用状态机处理半包和粘包
c#
小码编匠2 天前
C# 工控上位机必备:数据转换工具类与十个核心模块
后端·c#·.net
LinXunFeng3 天前
Obsidian - 使用 Share Note 分享笔记并自部署
前端·笔记·github
唐青枫4 天前
别再乱用 StartNew:C#.NET TaskFactory 任务调度实战详解
c#·.net
Artech5 天前
[MAF预定义的AIContextProvider-03]ChatHistoryMemoryProvider——赋予Agent从经验中学习的能力
ai·c#·agent·memory·maf
Scout-leaf6 天前
C#摸鱼实录——IoC与DI案例详解
c#
咕白m6256 天前
使用 C# 在 Excel 中应用多种字体样式
后端·c#