1. 程式人生 > 其它 >WPF中的依賴屬性

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的程式碼片段
  • 新增到Hashtable

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>

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>

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. 參考資料: