WPF中的依赖属性

# 1. WPF中的依赖属性

  • 依赖属性是专门基于WPF创建的。在WPF库实现中,依赖属性使用普通的C#属性进行了包装,使用方法与普通的属性是相同的。

1.1 依赖属性提供的属性功能

1.2 依赖属性优先级列表

运行时值分配给依赖项属性时,属性系统使用的明确优先级顺序,由高到低为:

  1. 属性系统强制
  2. 活动动画或具有保留行为的动画
  3. 本地值
  4. TemplatedParent 模板属性值
  5. 隐式样式
  6. 样式触发器
  7. 模板触发器
  8. 样式 setter 值
  9. 默认样式,也称为 主题样式
  10. 继承。 子元素的某些依赖属性从父元素继承其值。 因此,可能不需要在整个应用程序中设置每个元素的属性值。
  11. 依赖项属性元数据中的默认值 依赖属性可以在该属性的属性系统注册过程中设置默认值。 继承依赖属性的派生类可以重写依赖属性元数据 (包括基于每个类型) 的默认值。 对于继承的属性,父元素的默认值优先于子元素的默认值。 因此,如果未设置可继承属性,则将使用根或父元素的默认值,而不是子元素的默认值。

1.3 附加属性

附加属性允许子元素为父元素中定义的属性指定唯一值。 常见方案是一个子元素,它指定其父元素在 UI 中的呈现方式。 例如, DockPanel.Dock是附加属性,因为它在 的子元素上 DockPanel设置,而不是本身 DockPanel

2. 依赖属性的使用

2.1 定义依赖属性

  • 定义一个名叫Name的依赖属性,根据命名约定,依赖属性以属性名称加Property来命名
  • 依赖属性的所有者必须继承自DependencyObject
public class People : DependencyObject
{
    public static readonly DependencyProperty NameProperty;
}

2.2 注册依赖属性

  • 使用DependencyProperty.Register()静态方法对依赖属性进行注册
public class People : DependencyObject
{
    public static readonly DependencyProperty NameProperty =
        DependencyProperty.Register("Name", typeof(string), typeof(People));
}

参数说明:

  • 第一个参数表示要注册的依赖属性的名称
  • 第二个参数表示要注册的依赖属性的类型
  • 第三个参数表示要注册的依赖属性的所有者
  • ......(DependencyProperty.Register()提供了多种重载方式,其他参数参考文档即可,最少需要上面3个参数)

注册方法的定义:

public static DependencyProperty Register(string name, Type propertyType, Type ownerType, PropertyMetadata typeMetadata, ValidateValueCallback validateValueCallback)
{
    
}
  • 第四个参数为依赖属性元数据,具体见下文讲解

  • 第五个参数为一个回调函数

原理说明:

上面定义了一个string类型的依赖属性Name, 在WPF的源代码中, 其实是生成了一个key/value存储在Hashtable里面。

  • 生成key的代码片段

img

  • 添加到Hashtable

img

2.3 添加属性包装器

  • 创建属性包装器时应当只包含对GetValue()SetValue()方法的调用,不应当添加任何验证属性值、引发事件等额外的代码,因为WPF中的其他功能可能会忽略属性封装器,直接调用GetValue()SetValue()方法
public class People : DependencyObject
{

    public string Name
    {
        get { return (string)GetValue(NameProperty); }
        set { SetValue(NameProperty, value); }
    }

    public static readonly DependencyProperty NameProperty =
        DependencyProperty.Register("Name", typeof(string), typeof(People));
}

2.4 依赖属性元数据

  • PropertyMetadata类存储属性系统使用的大多数元数据

  • 在实现新的依赖项属性时,可以通过使用方法的 Register重载来设置其元数据。

定义如下:

public PropertyMetadata(object defaultValue, PropertyChangedCallback propertyChangedCallback, CoerceValueCallback coerceValueCallback)
{
}

参数说明:

  • 更改默认值,这是一个常见方案。
  • 验证回调:更改或添加属性,更改回调
  • 强制回调:可以用来修正属性值

示例说明:

  • 设置了Name依赖属性的默认值为“元数据”
public class People : DependencyObject
{

    public string Name
    {
        get { return (string)GetValue(NameProperty); }
        set { SetValue(NameProperty, value); }
    }

    public static PropertyMetadata metadata = new PropertyMetadata("元数据", propertyChangedCallback, coerceValueCallback);

    private static void propertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {

    }

    private static object coerceValueCallback(DependencyObject d, object baseValue)
    {
        return null;
    }

    public static readonly DependencyProperty NameProperty =
        DependencyProperty.Register("Name", typeof(string), typeof(People), metadata);


}

3. 依赖属性的继承

  • 属性值继承是依赖属性值从父元素传播到包含属性的元素树中的子元素的机制

  • AllowDrop在基类上 UIElement 实现,因此,派生自 UIElement 的每个控件上也存在该依赖项属性。 WPF 启用依赖项属性的值继承,使用户可以轻松地在父元素上设置属性值一次,并使该属性值传播到元素树中的子代元素。

3.1 定义一个可继承的依赖属性

public class UCStackPanel : StackPanel
{

    public DateTime NowDate
    {
        get { return (DateTime)GetValue(NowDateProperty); }
        set { SetValue(NowDateProperty, value); }
    }

    public static readonly DependencyProperty NowDateProperty =
        DependencyProperty.Register("NowDate", typeof(DateTime), typeof(UCStackPanel), new FrameworkPropertyMetadata(DateTime.MinValue,FrameworkPropertyMetadataOptions.Inherits));
}

public class UCButton : Button
{
    public DateTime NowDate
    {
        get { return (DateTime)GetValue(NowDateProperty); }
        set { SetValue(NowDateProperty, value); }
    }

    public static readonly DependencyProperty NowDateProperty =
        UCStackPanel.NowDateProperty.AddOwner(typeof(UCButton), new FrameworkPropertyMetadata(DateTime.MinValue, FrameworkPropertyMetadataOptions.Inherits));

}
<GroupBox Header="依赖属性继承" FontSize="25" Margin="20 20">
    <local:UCStackPanel NowDate="{x:Static sys:DateTime.Now}">
        <local:UCButton Content="{Binding RelativeSource={x:Static RelativeSource.Self}, Path=NowDate}"/>
    </local:UCStackPanel>
</GroupBox>

image-20220506204303500

4. 只读依赖属性

创建只读依赖项属性的过程与创建读/写依赖项属性的方式很类似,其中包括:

  • 注册只读属性时,调用RegisterReadOnly而不是 Register
  • 实现 CLR 属性包装时,请确保它没有公共 set 访问器。
  • RegisterReadOnly 返回 DependencyPropertyKey 而不是DependencyPropertyDependencyPropertyKey将存储在非公共类成员中。
public class UCLabel : Label
{
    public UCLabel():base()
    {
        SetValue(AgePropertyKey, 108);
    }

    public int Age
    {
        get { return (int)GetValue(AgePropertyKey.DependencyProperty); }
    }

    public static readonly DependencyPropertyKey AgePropertyKey =
        DependencyProperty.RegisterReadOnly("Age", typeof(int), typeof(UCLabel), new PropertyMetadata(88));

}
<GroupBox Header="只读依赖属性" FontSize="25" Margin="20 20">
    <local:UCLabel Content="{Binding RelativeSource={x:Static RelativeSource.Self}, Path=Age}"/>
</GroupBox>

image-20220506212826286

5. 附加属性

  • 附加属性允许子元素为父元素中定义的属性指定唯一值,常见方案是一个子元素,它指定其父元素在 UI 中的呈现方式

  • DockPanel.Dock 是附加属性,因为它在 的子元素上DockPanel设置,而不是本身 DockPanel

  • 附加属性是 XAML 概念

  • 依赖属性是 WPF 概念

  • 遵循 WPF 属性命名约定,通过命名标识符字段来区分字段和它们表示的属性 <property name>Property

  • 提供静态 Get<property name>Set<property name> 访问器方法,使属性系统能够访问附加属性。

public class PassWordExtension
{
    public static string GetPassWord(DependencyObject obj)
    {
        return (string)obj.GetValue(PassWordProperty);
    }

    public static void SetPassWord(DependencyObject obj, string value)
    {
        obj.SetValue(PassWordProperty, value);
    }

    public static readonly DependencyProperty PassWordProperty =
        DependencyProperty.RegisterAttached("PassWord", typeof(string), typeof(PassWordExtension), new PropertyMetadata(string.Empty));

}

6. 集合类型依赖属性

  • 如果属性值是引用类型,应在注册依赖属性的类的构造函数中设置默认值。
  • 依赖属性元数据不应包含默认的引用类型值,因为该值将分配给类的所有实例,从而创建单一实例类。

只读依赖属性:

public class People : DependencyObject
{
    private static readonly DependencyPropertyKey InfosPropertyKey =
        DependencyProperty.RegisterReadOnly(
        name: "Infos",
        propertyType: typeof(List<int>),
        ownerType: typeof(People),
        typeMetadata: new FrameworkPropertyMetadata()
        //typeMetadata: new FrameworkPropertyMetadata(new List<int>())
    );

    public People() => SetValue(InfosPropertyKey, new List<int>());

    public List<int> Infos =>
        (List<int>)GetValue(InfosPropertyKey.DependencyProperty);
}
People p1 = new People();
People p2 = new People();
p1.Infos.Add(1);
p2.Infos.Add(10);

MessageBox.Show($"p1 contains {p1.Infos.Count}\r\n" +
                $"p2 contains {p2.Infos.Count}");

p1 contains 1
p2 contains1

读写依赖属性:

public class People : DependencyObject
{
    public static readonly DependencyProperty InfosProperty =
        DependencyProperty.Register(
          name: "Infos",
          propertyType: typeof(List<int>),
          ownerType: typeof(Aquarium)
        );

    public People() => SetValue(InfosProperty, new List<int>());

    public List<FrameworkElement> Infos
    {
        get => (List<int>)GetValue(InfosProperty);
        set => SetValue(InfosProperty, value);
    }
}

FreezableCollection 依赖项属性:

  • 集合类型依赖属性不会自动报告其子属性中的更改。 因此,如果要绑定到集合,则绑定可能不会报告更改,使某些数据绑定方案失效。 但是,如果将 用于 FreezableCollection 依赖属性类型,则正确报告对集合元素属性的更改,并且绑定将正常工作。

  • 若要在依赖对象集合中启用子属性绑定 FreezableCollection,请使用集合类型 ,具有任何派生类 DependencyObject 的类型约束。

下面的示例声明一个类 Aquarium ,该类包含 FreezableCollection 类型约束为 的 FrameworkElement。 传递给RegisterReadOnly(String, Type, Type, PropertyMetadata)方法的PropertyMetadata中不包含默认集合值,而是使用 类构造函数将默认集合值设置为新的 FreezableCollection

public class Aquarium : DependencyObject
{
    // Register a dependency property with the specified property name,
    // property type, and owner type.
    private static readonly DependencyPropertyKey s_aquariumContentsPropertyKey =
        DependencyProperty.RegisterReadOnly(
          name: "AquariumContents",
          propertyType: typeof(FreezableCollection<FrameworkElement>),
          ownerType: typeof(Aquarium),
          typeMetadata: new FrameworkPropertyMetadata()
        );

    // Store the dependency property identifier as a static member of the class.
    public static readonly DependencyProperty AquariumContentsProperty =
        s_aquariumContentsPropertyKey.DependencyProperty;

    // Set the default collection value in a class constructor.
    public Aquarium() => SetValue(s_aquariumContentsPropertyKey, new FreezableCollection<FrameworkElement>());

    // Declare a public get accessor.
    public FreezableCollection<FrameworkElement> AquariumContents =>
        (FreezableCollection<FrameworkElement>)GetValue(AquariumContentsProperty);
}

7. 属性回调(监控依赖属性)

  • 对依赖属性的改变进行监听
  • 使用RegisterAttachedRegister方法时,传入一个带回调函数(propertyChangedCallback)的元数据
public static readonly DependencyProperty PassWordProperty =
            DependencyProperty.RegisterAttached("PassWord", typeof(string), typeof(PassWordExtension), new PropertyMetadata(string.Empty, propertyChangedCallback));

private static void propertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    PasswordBox? passwordBox = d as PasswordBox;
    if (passwordBox != null)
    {
        passwordBox.Password = e.NewValue?.ToString();
    }
}
<PasswordBox local:PassWordExtension.PassWord="{Binding PassWord, UpdateSourceTrigger=PropertyChanged}" PasswordChar="*" FontSize="25"/>

参数说明:

  • DependencyObject d表示哪个依赖对象使用了此依赖属性,这里是PasswordBox
  • DependencyPropertyChangedEventArgs e中存储了需要的参数,例如:老的值、新的值等

8. 属性验证

WPF .NET (依赖项属性回调和)

8.1 验证回调

  • 在注册依赖属性时传入ValidateValueCallback类型的回调
  • ValidateValueCallback返回一个bool值,返回false时会触发异常
  • ValidateValueCallback不能访问设置属性的实际对象,意味着不能检查其它属性值(一次只能访问一个属性)
public static DependencyProperty RegisterAttached(string name, Type propertyType, Type ownerType, PropertyMetadata defaultMetadata, ValidateValueCallback validateValueCallback)

示例说明:

public static readonly DependencyProperty PassWordProperty =
    DependencyProperty.RegisterAttached("PassWord", typeof(string), typeof(PassWordExtension), new PropertyMetadata(string.Empty, propertyChangedCallback), validateValueCallback);

private static bool validateValueCallback(object value)
{
    return true;
}
<PasswordBox local:PassWordExtension.PassWord="{Binding PassWord, UpdateSourceTrigger=PropertyChanged}" PasswordChar="*" FontSize="25"/>

8.2 强制回调

  • 使用RegisterAttachedRegister方法时,传入一个带回调函数(coerceValueCallback)的元数据
  • 可以通过回调函数coerceValueCallback对属性值进行调整就叫强制回调,也叫属性强制
  • coerceValueCallback传递两个参数,该数值将要应用到的对象以及准备使用的数值
  • 可以通过强制回调coerceValueCallback处理相互关联的属性,例如ScrollBar中的MaximunMinimumValue属性,使Minimum属性必须小于Maximun属性,Value属性必须位于两者之间等等
public static DependencyProperty RegisterAttached(string name, Type propertyType, Type ownerType, PropertyMetadata defaultMetadata)
public PropertyMetadata(object defaultValue, PropertyChangedCallback propertyChangedCallback, CoerceValueCallback coerceValueCallback)

9. 使用场景

  • 依赖属性: 当您需要单独创建控件时, 并且希望控件的某个部分能够支持数据绑定时, 你则可以使用到依赖属性。
  • 附加属性: 这种情况很多, 正因为WPF当中并不是所有的内容都支持数据绑定, 但是我们希望其支持数据绑定, 这样我们就可以创建基于自己声明的附加属性,添加到元素上, 让其元素的某个原本不支持数据绑定的属性间接形成绑定关系。例如:为PassWord定义附加属性与PassWord进行关联。例如DataGrid控件不支持SelectedItems, 但是我们想要实现选中多个条目进行数据绑定, 这个时候也可以声明附加属性的形式让其支持数据绑定。

10. 使用案例

  • 以密码框PasswordBox为例,PasswordBoxPassword属性不是依赖属性,不支持MVVM绑定,需要自定义依赖属性来间接支持

使用方法一:

  • 自定义一个帮助类
public class PasswordHelper
{
    public static readonly DependencyProperty PasswordProperty =
        DependencyProperty.RegisterAttached("Password", typeof(string), typeof(PasswordHelper), new FrameworkPropertyMetadata("", new PropertyChangedCallback(OnPropertyChanged)));
    public static string GetPassword(DependencyObject d)
    {
        return d.GetValue(PasswordProperty).ToString();
    }
    public static void SetPassword(DependencyObject d, string value)
    {
        d.SetValue(PasswordProperty, value);
    }

    public static readonly DependencyProperty AttachProperty =
        DependencyProperty.RegisterAttached("Attach", typeof(bool), typeof(PasswordHelper), new FrameworkPropertyMetadata(default(bool), new PropertyChangedCallback(OnAttached)));
    public static bool GetAttach(DependencyObject d)
    {
        return (bool)d.GetValue(AttachProperty);
    }
    public static void SetAttach(DependencyObject d, bool value)
    {
        d.SetValue(AttachProperty, value);
    }

    static bool _isUpdating = false;
    private static void OnPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        PasswordBox password = d as PasswordBox;
        password.PasswordChanged -= Password_PasswordChanged;
        if (!_isUpdating)
            password.Password = e.NewValue?.ToString();
        password.PasswordChanged += Password_PasswordChanged;
    }

    private static void OnAttached(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        PasswordBox password = d as PasswordBox;
        password.PasswordChanged += Password_PasswordChanged;
    }

    private static void Password_PasswordChanged(object sender, RoutedEventArgs e)
    {
        PasswordBox passwordBox = sender as PasswordBox;
        _isUpdating = true;
        SetPassword(passwordBox, passwordBox.Password);
        _isUpdating = false;
    }
}
<PasswordBox local:PasswordHelper.Password="{Binding PassWord, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" 
                             local:PasswordHelper.Attach="True" PasswordChar="*" FontSize="25"/>

使用方法二:

  • 使用行为来进行支持
/// <summary>
/// 增加Password扩展属性
/// </summary>
public static class PasswordBoxHelper
{
    public static string GetPassword(DependencyObject obj)
    {
        return (string)obj.GetValue(PasswordProperty);
    }

    public static void SetPassword(DependencyObject obj, string value)
    {
        obj.SetValue(PasswordProperty, value);
    }

    public static readonly DependencyProperty PasswordProperty =
        DependencyProperty.RegisterAttached("Password", typeof(string), typeof(PasswordBoxHelper), new PropertyMetadata("", OnPasswordPropertyChanged));

    private static void OnPasswordPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
    {
        PasswordBox box = sender as PasswordBox;
        string password = (string)e.NewValue;
        if (box != null && box.Password != password)
        {
            box.Password = password;
        }
    }
}

/// <summary>
/// 接收PasswordBox的密码修改事件
/// </summary>
public class PasswordBoxBehavior : Behavior<PasswordBox>
{

    protected override void OnAttached()
    {
        base.OnAttached();
        AssociatedObject.PasswordChanged += AssociatedObject_PasswordChanged;
    }

    private void AssociatedObject_PasswordChanged(object sender, RoutedEventArgs e)
    {
        PasswordBox passwordBox = sender as PasswordBox;
        string password = PasswordBoxHelper.GetPassword(passwordBox);

        if (passwordBox != null && passwordBox.Password != password)
            PasswordBoxHelper.SetPassword(passwordBox, passwordBox.Password);
    }

    protected override void OnDetaching()
    {
        base.OnDetaching();
        AssociatedObject.PasswordChanged -= AssociatedObject_PasswordChanged;
    }
}
<PasswordBox local:PasswordBoxHelper.Password="{Binding PassWord, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" 
             PasswordChar="*" FontSize="25">
    <i:Interaction.Behaviors>
        <local:PasswordBoxBehavior/>
    </i:Interaction.Behaviors>
</PasswordBox>

11. 参考资料:

posted @ 2022-05-15 19:08  Alex枫  阅读(968)  评论(0编辑  收藏  举报