[WPF]淺析依賴屬性(DependencyProperty)

来源:https://www.cnblogs.com/czwy/archive/2023/09/27/17734460.html
-Advertisement-
Play Games

一、背景 微信小程式手機號授權介面,從23年8月開始實行付費驗證。 文檔地址:https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/getRealtimePhoneNumber.html 新版手機號授權說明如下 ...


在WPF中,引入了依賴屬性這個概念,提到依賴屬性時通常都會說依賴屬性能節省實例對記憶體的開銷。此外依賴屬性還有兩大優勢。

  • 支持多屬性值,依賴屬性系統可以儲存多個值,配合Expression、Style、Animation等可以給我們帶來很強的開發體驗。
  • 加入了屬性變化通知,限制、驗證等功能。方便我們使用少量代碼實現以前不太容易實現的功能。

本文將主要介紹依賴屬性是如何存取數據的以及多屬性值的取值優先順序。

CLR屬性

CLR屬性是private欄位安全訪問的封裝

對象實例的每個private欄位都會占用一定的記憶體,欄位被CLR屬性封裝起來,每個實例看上去都帶有相同的屬性,但並不是每個實例的CLR屬性都會多占一點記憶體。因為CLR屬性是一個語法糖,本質是Get/Set方法,再多的實例方法也只有一個拷貝。

以TextBlock為例,共有107個屬性,但通常使用的最多的屬性是Text,FontSize,FontFamily,Foreground這幾個屬性,大概有100個左右屬性是沒有使用的。若按照CLR屬性分配空間,假設每個屬性都封裝了一個4Byte的欄位,一個5列1000行的列表浪費的空間就是4×100×5×1000≈1.9M。而依賴屬性則是省下這些沒有用到的屬性所需的空間,其關鍵就在於依賴屬性的聲明和使用。

依賴屬性的聲明和使用

依賴屬性的使用很簡單,只需要以下幾個步驟就可以實現:

  1. 讓所在類型直接或間接繼承自DependecyObject。在WPF中,幾乎所有的控制項都間接繼承自DependecyObject
  2. 聲明一個靜態只讀的DependencyProperty類型變數,這個靜態變數所引用的實例並不是通過new操作符創建,而是使用簡單的單例模式通過DependencyProperty.Register創建的,下文會對這個方法進行介紹。
  3. 使用依賴屬性的實例化包裝屬性讀寫依賴屬性。
    按照以上步驟可以寫出如下代碼:
public class ValidationParams:DependencyObject
{
    public object Param1
    {
        get { return (object)GetValue(Param1Property); }
        set { SetValue(Param1Property, value); }
    }

    // Using a DependencyProperty as the backing store for Data.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty Param1Property =
        DependencyProperty.Register("Param1", typeof(object), typeof(ValidationParams), new PropertyMetadata(null));
}

代碼中Param1Property才是真正的依賴屬性,Param1是依賴屬性的包裝器,這裡有一個命名約定,依賴屬性的名稱是對應包裝器名稱+Property組成。在Visual studio中輸入propdp,然後Tab鍵就會自動生成依賴屬性以及包裝器的代碼片段,然後根據實際情況修改相應的參數和類型。

Register方法的第一個參數為string類型,用來指明作為依賴屬性包裝器的CLR屬性;第二個參數指定依賴屬性存儲什麼類型的值,第三個參數指明依賴屬性的宿主是什麼類型,第四個參數是依賴屬性元數據,包含預設值,PropertyChangedCallback,CoerceValueCallback,ValidateValueCallback等委托。

依賴屬性存取值的機制

從修飾符可以看出依賴屬性是一個靜態的只讀變數,要確保不同實例的依賴屬性正確賦值,肯定不能把數據直接保存到這個靜態變數中。這裡其實也是依賴屬性機制的核心。
與依賴屬性存取數據有三個關鍵的類型:DependencyPropertyDependencyObjectEffectiveValueEntry

  • DependencyProperty:依賴屬性實例都是單例,其中DefaultMetadata存儲了依賴屬性的預設值,提供變化通知、限制、檢驗等回調以及子類override依賴屬性的渠道。GlobalIndex用於檢索DependencyProperty的實例。應用程式中註冊的所有DependencyProperty的實例都存放於名為PropertyFromName的Hashtable中。
  • DependencyObject:依賴屬性的宿主對象,_effectiveValues是一個私有的有序數組,用來存儲本對象實例中修改過值得依賴屬性,GetValueSetValue方法用於讀寫依賴屬性的數值。
  • EffectiveValueEntry:存儲依賴屬性真實數值的對象。它可以實現多屬性值,具體來說就是內部可以存放多個值,根據當前的狀態確定對外暴露哪一個值(這裡涉及到多個值選取的優先順序的問題)。
    image

前邊提到依賴屬性實例是使用簡單的單例模式通過DependencyProperty.Register創建的。通過閱讀源碼發現,所有的DependencyProperty.Register方法重載都是對DependencyProperty.RegisterCommon的調用。為了方便介紹,下文只是提取RegisterCommon方法中的關鍵代碼

private static DependencyProperty RegisterCommon(string name, Type propertyType, Type ownerType, PropertyMetadata defaultMetadata, ValidateValueCallback validateValueCallback)
{
    FromNameKey key = new FromNameKey(name, ownerType);
    .....略去校驗以及預設元數據代碼

    // Create property
    DependencyProperty dp = new DependencyProperty(name, propertyType, ownerType, defaultMetadata, validateValueCallback);

    // Map owner type to this property
    // Build key
    lock (Synchronized)
    {
        PropertyFromName[key] = dp;
    }

    return dp;
}

代碼的大致意思是生成一個FromNameKey類型的key,然後構造一個DependencyProperty實例dp,並存放到名為PropertyFromName的Hashtable中,最後返回這個實例dp
FromNameKeyDependencyProperty中的內部私有類,其代碼如下:

private class FromNameKey
{
    public FromNameKey(string name, Type ownerType)
    {
        _name = name;
        _ownerType = ownerType;
        _hashCode = _name.GetHashCode() ^ _ownerType.GetHashCode();
    }

    public override int GetHashCode()
    {
        return _hashCode;
    }
    ...略去部分代碼
    private string _name;
    private Type _ownerType;
    private int _hashCode;
}

這裡特地介紹這個類是因為FromNameKey對象是依賴屬性實例的key,它的hashcode是由Register的第一個參數(依賴屬性包裝器屬性名稱字元串)的hashcode和第三個參數(依賴屬性宿主類型)的hashcode做異或運算得來的,這樣設計確保了每個DependecyObject類型中不同名稱的依賴屬性的實例是唯一的。

接下來就是使用(讀寫)依賴屬性了,前邊提到DependecyObject中提供了GetValueSetValue方法用於讀寫依賴屬性。先看下GetValue方法,代碼如下:

public object GetValue(DependencyProperty dp)
{
    // Do not allow foreign threads access.
    // (This is a noop if this object is not assigned to a Dispatcher.)
    //
    this.VerifyAccess();

    ArgumentNullException.ThrowIfNull(dp);

    // Call Forwarded
    return GetValueEntry(
            LookupEntry(dp.GlobalIndex),
            dp,
            null,
            RequestFlags.FullyResolved).Value;
}

方法前幾行是線程安全性和參數有效性檢測,最後一行是獲取依賴屬性的值。LookupEntry是根據DependencyProperty實例的GlobalIndex_effectiveValues數組中查找依賴屬性的有效值EffectiveValueEntry,找到後返回其索引對象EntryIndexEntryIndex主要包含IndexFound兩個屬性,Index表示查找到的索引值,Found表示是否找到目標元素。

GetValueEntry根據LookupEntry方法返回的EntryIndex實例查找有效值EffectiveValueEntry。如果entryIndex.Found為true,則根據Index返回_effectiveValues中的元素,否則new一個EffectiveValueEntry實例。

SetValue方法也是先通過GetValueEntry查找有效值對象,找到則修改舊數據,反之則new一個EffectiveValueEntry實例賦值,並添加到_effectiveValues中。

至此,我們也大致瞭解了依賴屬性存取值的秘密。DependencyProperty並不保存實際數值,而是通過其GlobalIndex屬性來檢索屬性值。每一個DependencyObject對象實例都有一個EffectiveValueEntry數組,保存著已賦值的依賴屬性的數據,當要讀取某個依賴屬性的值時,會在這個數組中去檢索,如果沒有檢索到,會從DependencyProperty保存的DefaultMetadata中讀取預設值(這裡只是簡單的描述這個過程,真實情況還涉及到元素的style、Theme、父節點的值等)。

依賴屬性值的優先順序

前邊提到依賴屬性支持多屬性值,WPF中可以通過多種方法為一個依賴項屬性賦值,如通過樣式、模板、觸發器、動畫等為依賴項屬性賦值的同時,控制項本身的聲明也為屬性進行了賦值。在這種情況下,WPF只能選擇其中的一種賦值作為該屬性的取值,這就涉及到取值的優先順序問題。
從上一小節的圖中可以看到EffectiveValueEntry中有兩個屬性:ModifiedValueBaseValueSourceInternalModifiedValue用於跟蹤依賴屬性的值是否被修改以及被修改的狀態。BaseValueSourceInternal是一個枚舉,它用於表示依賴屬性的值是從哪裡獲取的。在與ModifiedValue一起使用,可以確定最終呈現的屬性值。
EffectiveValueEntryGetFlattenedEntry方法中以下代碼及註釋可以看出強制值>動畫值>表達式值這樣得優先順序

internal EffectiveValueEntry GetFlattenedEntry(RequestFlags requests)
{
    ......略去部分代碼

    // Note that the modified values have an order of precedence
    // 1. Coerced Value (including Current value)
    // 2. Animated Value
    // 3. Expression Value
    // Also note that we support any arbitrary combinations of these
    // modifiers and will yet the precedence metioned above.
    if (IsCoerced)
    {
        ......略去部分代碼
    }
    else if (IsAnimated)
    {
        ......略去部分代碼
    }
    else
    {
        ......略去部分代碼
    }
    return entry;
}

其中表達式值包含樣式、模板、觸發器、主題、控制項本身對屬性賦值或者綁定表達式。其優先順序則是在BaseValueSourceInternal中定義的。枚舉元素排列順序與取值優先順序順序剛好相反。

// Note that these enum values are arranged in the reverse order of
// precendence for these sources. Local value has highest
// precedence and Default value has the least. Note that we do not
// store default values in the _effectiveValues cache unless it is
// being coerced/animated.
[FriendAccessAllowed] // Built into Base, also used by Core & Framework.
internal enum BaseValueSourceInternal : short
{
    Unknown                 = 0,
    Default                 = 1,
    Inherited               = 2,
    ThemeStyle              = 3,
    ThemeStyleTrigger       = 4,
    Style                   = 5,
    TemplateTrigger         = 6,
    StyleTrigger            = 7,
    ImplicitReference       = 8,
    ParentTemplate          = 9,
    ParentTemplateTrigger   = 10,
    Local                   = 11,
}

綜合起來依賴屬性取值優先順序列表如下:

  1. 強制:在CoerceValueCallback對依賴屬性約束的強制值。
  2. 活動動畫或具有Hold行為的動畫。
  3. 本地值:通過CLR包裝器調用SetValue設置的值,或者XAML中直接對元素本身設置值(包括bindingStaticResourceDynamicResource
  4. TemplatedParent模板的觸發器
  5. TemplatedParent模板中設置的值
  6. 隱式樣式
  7. 樣式觸發器
  8. 模板觸發器
  9. 樣式
  10. 主題樣式的觸發器
  11. 主題樣式
  12. 繼承。這裡的繼承Inherited是xaml樹中的父元素,要區別於面向對象語言子類繼承(derived,譯為派生更合適)與父類
  13. 依賴屬性元數據中的預設值

WPF對依賴屬性的優先順序支持分別使用了ModifiedValueBaseValueSourceInternal,大概是因為約束強制值和動畫值是臨時性修改,希望在更改結束後能夠恢復依賴屬性原有值。而對於樣式、模板、觸發器、主題這些來說相對固定,不需要像動畫那樣結束後恢複原來的值。

總結

依賴屬性是WPF中一個非常核心的概念,涉及的知識點也非常多。像RegisterReadOnlyPropertyMetadataOverrideMetadataAddOwner都能展開很多內容。要想真正掌握依賴屬性,這些都是需要熟悉的。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 不知道大家平時開發響應式前端代碼是如何調試的?是不是也跟我一樣,通過瀏覽器的開發者工具來切換不同的界面尺寸來看驗證效果呢? 可能是因為習慣了,平時就不停的切換不同尺寸來看效果。直到TJ君看到今天要推薦的這個免費工具,我才發現之前的調試方式好傻... 使用體驗 下麵,我們還一起來看看今天要推薦的這款名 ...
  • 3.1、環境搭建 創建名為spring_mvc_demo的新module,過程參考2.1節 3.1.1、創建SpringMVC的配置文件 <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.o ...
  • 共兩個依賴的需提前安裝的第三方庫:requests和bs4庫 cmd命令行輸入安裝requests庫:pip3 install -i https://pypi.douban.com/simple requests 安裝bs4庫:pip3 install -i https://pypi.douban. ...
  • Matplotlib 庫是一個用於數據可視化和繪圖的 Python 庫。 它提供了大量的函數和類,可以幫助用戶輕鬆地創建各種類型的圖表,包括直方圖、箱形圖、散點圖、餅圖、條形圖和密度圖等。 本系列具體內容包括: 畫布 畫布是其他所有的元素的載體,可以說是最重要,也是最容易被忽視的元素。 繪製圖形之前 ...
  • Context本質 golang標準庫里Context實際上是一個介面(即一種編程規範、 一種約定)。 type Context interface { Deadline() (deadline time.Time, ok bool) Done() <-chan struct{} Err() err ...
  • Merkle 樹(Merkle Tree)是一種樹狀數據結構,通常用於驗證大規模數據集的完整性和一致性。它的名字來源於其發明者 Ralph Merkle。Merkle 樹在密碼學、分散式系統和區塊鏈等領域得到廣泛應用,尤其在區塊鏈中,它用於驗證交易和區塊的完整性,確保數據不被篡改。 下麵是 Merk ...
  • 歡迎訪問我的GitHub 這裡分類和彙總了欣宸的全部原創(含配套源碼):https://github.com/zq2599/blog_demos 關於《Strimzi Kafka Bridge(橋接)實戰》 在strimzi技術體系中,橋接(bridge)是很要的功能,內容也很豐富,因此將橋接相關的 ...
  • 0. 數據說明 本項目所用數據集包含了一個家庭6個月的用電數據,收集於2007年1月至2007年6月。 這些數據包括有功功率、無功功率、電壓、電流強度、分項計量1(廚房)、分項計量2(洗衣房)和分項計量3(電熱水器和空調)等信息。該數據集共有260,640個測量值,可以為瞭解家庭用電情況提供重要的見 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...