WPF框架拓展了CLR的属性功能(下面称为WPF属性),基于WPF属性实现的属性就叫做依赖属性。WPF属性用起来和标准的CLR的属性一样,所以很多时候用户实际使用都没有意识到使用的是依赖属性。
依赖属性依赖于外部提供,所以叫做依赖属性,依赖属性一般来说本身没有值,而是通过动态计算获取的。但是如果依赖属性没有其他来源值时,支持指定一个默认值。拥有依赖属性的对象叫做依赖对象。
传统的属性本身不占用空间,但是其背后的字段占用空间,比如TextBox有138个属性,如果用普通的属性,那么每个TextBox对象的属性都要占用空间,一旦控件多起来会很占用空间,而这些属性大部分可能都不会使用。
WPF允许对象在创建时不包含存储字段所用的空间,而是保留需要用到数据时能获得默认值,借用其他对象数据,或者实时分配空间的能力
但是如果直接给依赖属性赋值,依然会像属性一样占用空间,只有绑定才能实现依赖。(或者直接赋值资源也是共享一份内存,不管是静态资源还是动态资源)
所有能绑定的属性都是依赖属性,比如Content,也只有依赖属性才能进行绑定。
依赖属性的来源
- 系统属性 比如主题和用户设置
- JustInTime实时计算 比如数据绑定,或者动画
- 可复用的模板 例如Resources和style
- 通过父子能够得到的值
依赖属性还支持:
- 默认值 (默认值通过元数据设置,使用特殊机制,不会占用额外的空间)
- 继承,如子控件可以继承父控件的属性值FontSize
- 回调,监控其他属性变化 ,变化时触发逻辑
(有个回调事件,OnPropertyChanged回调可以通知其他系统(比如绑定或者UI)属性值的变化) - 验证 验证值是否合法
优势:
- 支持数据绑定,这是MVVM的基础,允许UI元素和数据源之间保持同步
- 支持通过引用资源来设置依赖属性
- 支持模板和样式
- 支持动画效果(对该属性进行动画处理)
- 支持元数据重写,从注册依赖属性的类派生时,可以通过替代依赖的元数据来更改控件的行为
- 支持从父属性继承值,比如子控件可以继承父控件的字体大小
- 支持默认值,验证回调
- 仅在使用时分配存储空间,性能较好
- 设置字典以支持全球化
自定义依赖属性
依赖属性通过DependencyProperty
类实现,自定义依赖属性如下面代码所示。当自定义控件或类需要支持数据绑定,或者需要支持样式和模板,或者能够自动响应其他元素的变化时,等等都需要自定义依赖属性
```csharp
public class MyControl : Control
{
public static readonly DependencyProperty MyProperty =
DependencyProperty.Register("My", // 属性名
typeof(string), // 属性类型
typeof(MyControl), // 拥有类
new PropertyMetadata(null)); // 默认值
public string My
{
get { return (string)GetValue(MyProperty); }
set { SetValue(MyProperty, value); }
}
}
```
约定:依赖属性本身的命名要以Property结尾(不是包装器,包装器的名称不作限制)
- 调用DependencyProperty的静态注册方法,得到一个静态的依赖属性,这个的参数有
- 属性名 依赖属性的包装器叫啥(也就是下面的Get和Set)
- 属性类型 依赖属性本身是什么类型
- 依赖属性的拥有类,是那个依赖对象的依赖属性
- 元数据 ,这里面包含默认值,回调函数,元数据用于控制依赖属性的行为
2.调用一个包装器 GetValue和SetValue,这个是类的普通方法,而GetValue和SetValue是基类 DependencyObject的方法,然后参数是开始注册的依赖属性
- 默认值和回调,或者校验,都是通过元数据指定的
public static readonly DependencyProperty MyValueProperty =
DependencyProperty.Register(
"MyValue",
typeof(int),
typeof(MyControl),
new FrameworkPropertyMetadata(
0, // 默认值
OnMyValueChanged, // PropertyChangedCallback
CoerceMyValue)); // CoerceValueCallback
// 校验方法
private static object CoerceMyValue(DependencyObject d, object baseValue)
{
int value = (int)baseValue;
if (value < 0) value = 0;
if (value > 100) value = 100;
return value;
}
// 回调方法
private static void OnMyValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
MyControl control = (MyControl)d;
int newValue = (int)e.NewValue;
control.OnMyValueUpdated(newValue);
}
原理
注册的依赖属性是个静态值,而我们使用的时候,是每个对象上的依赖属性都是分开的,所以很明显,WPF底层做了一些工作,那是怎么实现依赖属性这种需要时分配空间,绑定不额外消耗内存的呢?
Register
当调用 DependencyProperty.Register 方法注册依赖属性时,WPF会进行校验,看你注册的是否合法,比如命名是否有重复,都通过之后,WPF 底层会将依赖属性的信息存储在一个全局的 哈希表中。
Key(键):是 DependencyProperty 的全局唯一标识,通常由属性的名字(Name)和所属类型(OwnerType)组成。这保证了即使不同的类有相同名字的依赖属性,它们依然可以唯一区分。
Value(值):包含依赖属性的元数据信息,例如默认值、回调、属性变化通知等。这些信息会用于计算属性值的优先级、触发回调等。
这个哈希表的目的是为了能在 WPF 系统中快速找到依赖属性及其相关元数据,从而支持高效的属性查询和操作。
通俗点说,注册是告诉WPF,我有个依赖属性类型,这个属性的名字,特点是啥,哪个类会用等等信息,让WPF帮你管理,不涉及具体对象的内存分配
SetValue
SetValue也会进行校验,看看传入的值满不满足依赖属性的类型要求,是否在合理范围内,是否和当前值相同,然后将值存起来,之后如果有回调函数会触发回调
之前提到,依赖属性支持设置默认值,是放在元数据里面的,如果设置的值就是默认值,那么WPF不会真正存储它的值,因为即使不存储,GetValue也会返回默认值,所以不会占用额外的空间
那么真正的依赖属性存储在哪呢,WPF使用了一个叫做EffectiveValueEntry 的数据结构来存储依赖属性的值,为了优化性能,内部使用了一个稀疏数组(或者类似数组结构)来存储每个依赖对象的属性值
- 每个依赖对象都有一个稀疏数组:当某个依赖对象的依赖属性发生变化时,WPF 系统会在这个数组中为该属性的值分配一个位置。
- 数组索引与依赖属性标识符相关:依赖属性在注册时获得一个唯一的全局标识符(Global Index),这个索引是一个整数,它会作为稀疏数组的索引位置。数组的每一个位置存储着对应的依赖属性的 EffectiveValueEntry。
- EffectiveValueEntry 结构:EffectiveValueEntry 还存储属性值的来源(如本地值、样式、动画、数据绑定等)的计算结果,并根据优先级选取实际使用哪个值。它是一个多层结构,记录了某个依赖属性的实际值、是否有动画绑定,或者该值是否来源于样式等。
具体流程是
- SetValue 首先会找到依赖属性的全局索引,通过这个索引可以定位到依赖对象的稀疏数组中的具体位置。
- 如果属性值发生了变化(与当前存储的值不同),WPF 会更新稀疏数组中对应索引处的值,并且根据属性的 PropertyMetadata 来决定是否触发回调和通知机制。
GetValue
理解了Set,Get就好理解了
- 首先通过依赖属性的全局索引找到依赖对象的稀疏数组中的位置,取出该位置存储的 EffectiveValueEntry。
- WPF 系统会根据不同的值来源(如本地设置、绑定、样式等)的优先级,从 EffectiveValueEntry 中确定最终的属性值。
- 如果稀疏数组中没有存储值(即该属性从未被设置过),则返回 PropertyMetadata 中的默认值。
触发器的条件通常是基于控件的依赖属性值。触发器会监视这些依赖属性,当这些属性的值发生变化时,触发器会重新评估其条件,并应用相应的样式或行为。触发器依赖于依赖属性的变更通知机制。当依赖属性的值发生变化时,WPF 的属性系统会通知相关的触发器,以便它们可以根据新的属性值进行更新。
附加属性
附加属性就是对于一个对象而言, 本来它不具备这个属性, 但是由于附加给这个对象, 然后才有了这个属性,这种我们称之为附加属性。
附加属性可以全局访问,最大的作用是在子元素上设置父元素需要的值,其设计目的是允许一个对象为另一个对象(通常是它的子对象)设置属性值,即允许某个类在自己不拥有这个属性的情况下,仍然能够设置该属性的值。
目前国内网络上主流的说法是,附加属性是一种特殊形式的依赖属性
但是微软的官方文档里是这么说的
附加属性是 XAML 概念,依赖属性是 WPF 概念。 在 WPF 中,WPF 类型上大多数与 UI 相关的附加属性都作为依赖属性实现。 作为依赖属性实现的 WPF 附加属性支持依赖属性概念,例如包含元数据中的默认值的属性元数据。
所以严格来说,有部分附加属性并不是依赖属性实现的,但是我们正常使用当依赖属性用反正没有啥问题
- 依赖属性:通常是为类自身定义并由类自身管理的。
- 附加属性:设计的目的是允许其他类(如父类或容器)为某个对象设置属性值,而这个属性并不属于该对象本身。附加属性通过静态的 Get 和 Set 方法来实现对依赖对象的操作。
附加属性通常用于在布局控件中为子控件指定特定的布局行为。例如,在 Grid 中使用的 Grid.Row 和 Grid.Column 就是典型的附加属性。
DockPanel.Dock是也一个附加属性,因为它在 DockPanel 的子元素上设置,而不是在 DockPanel
本身设置。 DockPanel
类定义名为 DockProperty的静态 DependencyProperty 字段,然后提供 GetDock和 SetDock方法作为附加属性的公共访问器。
应用场景
布局控件中的子元素定位:如在
Grid
或Canvas
中,使用附加属性来定义子元素的布局或定位。例如,Grid.Row
和Canvas.Left
。为子控件提供特定功能:附加属性允许你将特定功能应用于多个子控件,而无需在每个控件类中单独定义这些功能。
将信息传递给父控件:附加属性可以用于将特定信息从子控件传递到父控件,使得父控件可以根据这些信息调整布局或行为。
原本不支持数据绑定的希望支持数据绑定
WPF当中并不是所有的内容都支持数据绑定, 但是我们希望其支持数据绑定, 这样我们就可以创建基于自己声明的附加属性,添加到元素上, 让其元素的某个原本不支持数据绑定的属性间接形成绑定关系。
例如:为PassWord定义附加属性与PassWord进行关联。例如DataGrid控件不支持SelectedItems, 但是我们想要实现选中多个条目进行数据绑定, 这个时候也可以声明附加属性的形式让其支持数据绑定。
附加属性的定义
附加属性通常定义在静态类,或在希望拥有该属性的父控件类中定义。定义附加属性时,你需要提供RegisterAttached
方法注册属性,同时定义Get
和Set
方法来访问和设置该属性的值。注意附加属性的三个方法都是Static的。
其中,Get和Set的参数里有个DependencyObject类型,代表你要从哪个对象获取附加属性的值,因为附加属性并不是定义在对象自身的属性,而是通过 DependencyObject 的依赖属性系统存储的,所以 Get 方法的参数需要传递一个 DependencyObject。可以通过这个参数来获取对象的附加属性值。
public class CustomPanel : Panel
{
// 注册附加属性
public static readonly DependencyProperty IsHighlightedProperty =
DependencyProperty.RegisterAttached(
"IsHighlighted",
typeof(bool),
typeof(CustomPanel),
new PropertyMetadata(false));
// 为附加属性提供Get和Set方法
public static bool GetIsHighlighted(UIElement element)
{
return (bool)element.GetValue(IsHighlightedProperty);
}
public static void SetIsHighlighted(UIElement element, bool value)
{
element.SetValue(IsHighlightedProperty, value);
}
}
1. 为 PasswordBox
的 Password
属性添加数据绑定支持
PasswordBox
的 Password
属性是一个普通的 CLR 属性,不支持数据绑定。可以创建一个附加属性,通过监听 PasswordBox
的值变化事件,将变化后的值更新到绑定源。
实现步骤:
- 创建一个附加属性
BoundPassword
,它将用于绑定。(里面指定回调事件OnBoundPasswordChanged) - 在附加属性的
PropertyChangedCallback
中,更新PasswordBox
的Password
属性。 - 监听
PasswordBox
的PasswordChanged
事件,将新的密码值更新到BoundPassword
。
public static class PasswordHelper
{
public static readonly DependencyProperty BoundPasswordProperty =
DependencyProperty.RegisterAttached("BoundPassword", typeof(string), typeof(PasswordHelper),
new PropertyMetadata(string.Empty, OnBoundPasswordChanged));
public static string GetBoundPassword(DependencyObject d)
{
return (string)d.GetValue(BoundPasswordProperty);
}
public static void SetBoundPassword(DependencyObject d, string value)
{
d.SetValue(BoundPasswordProperty, value);
}
//依赖属性变动时触发作用
//1. 给绑定的目标上添加事件,如果需要绑定到目标属性变动了,那么这个值也变动
//2. 改变对应控件上的属性
private static void OnBoundPasswordChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is PasswordBox passwordBox)
{
passwordBox.PasswordChanged -= OnPasswordBoxPasswordChanged;
passwordBox.Password = (string)e.NewValue;
passwordBox.PasswordChanged += OnPasswordBoxPasswordChanged;
}
}
//对应控件上的属性变化之后,依赖属性跟着变化
private static void OnPasswordBoxPasswordChanged(object sender, RoutedEventArgs e)
{
if (sender is PasswordBox passwordBox)
{
SetBoundPassword(passwordBox, passwordBox.Password);
}
}
}
<PasswordBox local:PasswordHelper.BoundPassword="{Binding UserPassword, Mode=TwoWay}" />
在这个示例中,PasswordBox
的 Password
属性被间接绑定到了 ViewModel 的 UserPassword
属性上。PasswordHelper.BoundPassword
是一个附加属性,它通过监听 PasswordBox.PasswordChanged
事件来实现双向数据绑定。
2. 为 DataGrid
的 SelectedItems
属性添加数据绑定支持
WPF 的 DataGrid
控件不直接支持 SelectedItems
属性的数据绑定,但是可以通过附加属性来间接实现这个功能。
实现步骤:
- 创建一个附加属性
SelectedItemsBinding
。 - 监听
DataGrid
的SelectionChanged
事件,将选中的项更新到附加属性上。 - 如果附加属性的值发生变化,更新
DataGrid
的选择状态。
public static class DataGridHelper
{
public static readonly DependencyProperty SelectedItemsBindingProperty =
DependencyProperty.RegisterAttached("SelectedItemsBinding", typeof(IList), typeof(DataGridHelper),
new PropertyMetadata(null, OnSelectedItemsBindingChanged));
public static void SetSelectedItemsBinding(DependencyObject element, IList value)
{
element.SetValue(SelectedItemsBindingProperty, value);
}
public static IList GetSelectedItemsBinding(DependencyObject element)
{
return (IList)element.GetValue(SelectedItemsBindingProperty);
}
private static void OnSelectedItemsBindingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is DataGrid dataGrid)
{
dataGrid.SelectionChanged -= OnDataGridSelectionChanged;
if (e.NewValue is IList newList)
{
dataGrid.SelectedItems.Clear();
foreach (var item in newList)
{
dataGrid.SelectedItems.Add(item);
}
}
dataGrid.SelectionChanged += OnDataGridSelectionChanged;
}
}
private static void OnDataGridSelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (sender is DataGrid dataGrid)
{
IList boundList = GetSelectedItemsBinding(dataGrid);
if (boundList != null)
{
boundList.Clear();
foreach (var item in dataGrid.SelectedItems)
{
boundList.Add(item);
}
}
}
}
}
<DataGrid local:DataGridHelper.SelectedItemsBinding="{Binding SelectedItems, Mode=TwoWay}" />
在这个例子中,DataGrid
的 SelectedItems
属性通过 SelectedItemsBinding
附加属性与 ViewModel 中的 SelectedItems
集合绑定。这允许你在 ViewModel 中对多个选中的项进行处理。