依赖属性和附加属性

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结尾(不是包装器,包装器的名称不作限制)

  1. 调用DependencyProperty的静态注册方法,得到一个静态的依赖属性,这个的参数有
  • 属性名 依赖属性的包装器叫啥(也就是下面的Get和Set)
  • 属性类型 依赖属性本身是什么类型
  • 依赖属性的拥有类,是那个依赖对象的依赖属性
  • 元数据 ,这里面包含默认值,回调函数,元数据用于控制依赖属性的行为

2.调用一个包装器 GetValue和SetValue,这个是类的普通方法,而GetValue和SetValue是基类 DependencyObject的方法,然后参数是开始注册的依赖属性

  1. 默认值和回调,或者校验,都是通过元数据指定的
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方法作为附加属性的公共访问器。

应用场景
  1. 布局控件中的子元素定位:如在 GridCanvas 中,使用附加属性来定义子元素的布局或定位。例如,Grid.RowCanvas.Left

  2. 为子控件提供特定功能:附加属性允许你将特定功能应用于多个子控件,而无需在每个控件类中单独定义这些功能。

  3. 将信息传递给父控件:附加属性可以用于将特定信息从子控件传递到父控件,使得父控件可以根据这些信息调整布局或行为。

  4. 原本不支持数据绑定的希望支持数据绑定
    WPF当中并不是所有的内容都支持数据绑定, 但是我们希望其支持数据绑定, 这样我们就可以创建基于自己声明的附加属性,添加到元素上, 让其元素的某个原本不支持数据绑定的属性间接形成绑定关系。
    例如:为PassWord定义附加属性与PassWord进行关联。例如DataGrid控件不支持SelectedItems, 但是我们想要实现选中多个条目进行数据绑定, 这个时候也可以声明附加属性的形式让其支持数据绑定。

附加属性的定义

附加属性通常定义在静态类,或在希望拥有该属性的父控件类中定义。定义附加属性时,你需要提供RegisterAttached方法注册属性,同时定义GetSet方法来访问和设置该属性的值。注意附加属性的三个方法都是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. PasswordBoxPassword 属性添加数据绑定支持

PasswordBoxPassword 属性是一个普通的 CLR 属性,不支持数据绑定。可以创建一个附加属性,通过监听 PasswordBox 的值变化事件,将变化后的值更新到绑定源。

实现步骤:

  • 创建一个附加属性 BoundPassword,它将用于绑定。(里面指定回调事件OnBoundPasswordChanged)
  • 在附加属性的 PropertyChangedCallback 中,更新 PasswordBoxPassword 属性。
  • 监听 PasswordBoxPasswordChanged 事件,将新的密码值更新到 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}" />

在这个示例中,PasswordBoxPassword 属性被间接绑定到了 ViewModel 的 UserPassword 属性上。PasswordHelper.BoundPassword 是一个附加属性,它通过监听 PasswordBox.PasswordChanged 事件来实现双向数据绑定。

2. DataGridSelectedItems 属性添加数据绑定支持

WPF 的 DataGrid 控件不直接支持 SelectedItems 属性的数据绑定,但是可以通过附加属性来间接实现这个功能。

实现步骤:

  • 创建一个附加属性 SelectedItemsBinding
  • 监听 DataGridSelectionChanged 事件,将选中的项更新到附加属性上。
  • 如果附加属性的值发生变化,更新 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}" />

在这个例子中,DataGridSelectedItems 属性通过 SelectedItemsBinding 附加属性与 ViewModel 中的 SelectedItems 集合绑定。这允许你在 ViewModel 中对多个选中的项进行处理。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,752评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,100评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,244评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,099评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,210评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,307评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,346评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,133评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,546评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,849评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,019评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,702评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,331评论 3 319
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,030评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,260评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,871评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,898评论 2 351

推荐阅读更多精彩内容