一、背景 微信小程式手機號授權介面,從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。而依賴屬性則是省下這些沒有用到的屬性所需的空間,其關鍵就在於依賴屬性的聲明和使用。
依賴屬性的聲明和使用
依賴屬性的使用很簡單,只需要以下幾個步驟就可以實現:
- 讓所在類型直接或間接繼承自
DependecyObject
。在WPF中,幾乎所有的控制項都間接繼承自DependecyObject
。 - 聲明一個靜態只讀的
DependencyProperty
類型變數,這個靜態變數所引用的實例並不是通過new操作符創建,而是使用簡單的單例模式通過DependencyProperty.Register
創建的,下文會對這個方法進行介紹。 - 使用依賴屬性的實例化包裝屬性讀寫依賴屬性。
按照以上步驟可以寫出如下代碼:
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等委托。
依賴屬性存取值的機制
從修飾符可以看出依賴屬性是一個靜態的只讀變數,要確保不同實例的依賴屬性正確賦值,肯定不能把數據直接保存到這個靜態變數中。這裡其實也是依賴屬性機制的核心。
與依賴屬性存取數據有三個關鍵的類型:DependencyProperty
、DependencyObject
、EffectiveValueEntry
。
DependencyProperty
:依賴屬性實例都是單例,其中DefaultMetadata
存儲了依賴屬性的預設值,提供變化通知、限制、檢驗等回調以及子類override依賴屬性的渠道。GlobalIndex
用於檢索DependencyProperty
的實例。應用程式中註冊的所有DependencyProperty
的實例都存放於名為PropertyFromName
的Hashtable中。DependencyObject
:依賴屬性的宿主對象,_effectiveValues
是一個私有的有序數組,用來存儲本對象實例中修改過值得依賴屬性,GetValue
、SetValue
方法用於讀寫依賴屬性的數值。EffectiveValueEntry
:存儲依賴屬性真實數值的對象。它可以實現多屬性值,具體來說就是內部可以存放多個值,根據當前的狀態確定對外暴露哪一個值(這裡涉及到多個值選取的優先順序的問題)。
前邊提到依賴屬性實例是使用簡單的單例模式通過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
。
FromNameKey
是DependencyProperty
中的內部私有類,其代碼如下:
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
中提供了GetValue
、SetValue
方法用於讀寫依賴屬性。先看下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
,找到後返回其索引對象EntryIndex
。EntryIndex
主要包含Index
和Found
兩個屬性,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
中有兩個屬性:ModifiedValue
和BaseValueSourceInternal
,ModifiedValue
用於跟蹤依賴屬性的值是否被修改以及被修改的狀態。BaseValueSourceInternal
是一個枚舉,它用於表示依賴屬性的值是從哪裡獲取的。在與ModifiedValue
一起使用,可以確定最終呈現的屬性值。
EffectiveValueEntry
中GetFlattenedEntry
方法中以下代碼及註釋可以看出強制值>動畫值>表達式值這樣得優先順序
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,
}
綜合起來依賴屬性取值優先順序列表如下:
- 強制:在
CoerceValueCallback
對依賴屬性約束的強制值。 - 活動動畫或具有Hold行為的動畫。
- 本地值:通過CLR包裝器調用
SetValue
設置的值,或者XAML中直接對元素本身設置值(包括binding
、StaticResource
、DynamicResource
) - TemplatedParent模板的觸發器
- TemplatedParent模板中設置的值
- 隱式樣式
- 樣式觸發器
- 模板觸發器
- 樣式
- 主題樣式的觸發器
- 主題樣式
- 繼承。這裡的繼承Inherited是xaml樹中的父元素,要區別於面向對象語言子類繼承(derived,譯為派生更合適)與父類
- 依賴屬性元數據中的預設值
WPF對依賴屬性的優先順序支持分別使用了ModifiedValue
和BaseValueSourceInternal
,大概是因為約束強制值和動畫值是臨時性修改,希望在更改結束後能夠恢復依賴屬性原有值。而對於樣式、模板、觸發器、主題這些來說相對固定,不需要像動畫那樣結束後恢複原來的值。
總結
依賴屬性是WPF中一個非常核心的概念,涉及的知識點也非常多。像RegisterReadOnly
、PropertyMetadata
、OverrideMetadata
、AddOwner
都能展開很多內容。要想真正掌握依賴屬性,這些都是需要熟悉的。