一、核心概念
| 术语 | 说明 |
|---|---|
| 依赖属性(Dependency Property) | 通过 DependencyProperty.Register 注册的属性,存储在 WPF 属性系统中而非对象字段里 |
| 附加属性(Attached Property) | 通过 DependencyProperty.RegisterAttached 注册,可附加到任意 DependencyObject 上 |
| CLR 属性包装器 | 对依赖属性的 GetValue/SetValue 封装,提供与普通属性一致的访问语法 |
DependencyObject |
所有可拥有依赖属性的 WPF 对象的基类(如 UIElement、FrameworkElement) |
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 |
属性值沿可视化树继承 | FontSize、FontFamily 等内置属性 |
6. 依赖属性 vs 普通 CLR 属性
| 特性 | 依赖属性 | 普通 CLR 属性 |
|---|---|---|
| 数据绑定 | ✅ 支持 XAML {Binding} |
❌ 不支持 |
| 样式/模板 | ✅ 可在 Style/Template 中设置 | ❌ 不支持 |
| 动画 | ✅ 支持 Storyboard 动画 | ❌ 不支持 |
| 默认值 | ✅ 通过 PropertyMetadata 设置 |
需在构造函数中赋值 |
| 属性变更通知 | ✅ 内置回调机制 | 需手动实现 INotifyPropertyChanged |
| 内存开销 | ✅ 按需存储,共享默认值 | 每个实例都占用字段内存 |
| 继承 | ✅ 支持沿可视化树继承 | ❌ 不支持 |
三、问题排查
错误1:XamlParseException --- 不能在属性上设置 Binding
-
现象 :
System.Windows.Markup.XamlParseException: "不能在"PasswordBox"类型的"Password"属性上设置"Binding"" -
原因 :
Password不是依赖属性,WPF 绑定系统只能绑定到DependencyProperty -
解决 :使用附加属性
PasswordBoxHelper为PasswordBox添加绑定能力(见第二节第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 static的GetXxx/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);