和 UWP 與 WPF 不同的是在 MAUI 里面,使用可系結物件 BindableObject 替換了依賴物件的概念,我閱讀了 MAUI 的源代碼發現其實只是命名變更了,里面的機制和設計思想都是差不多的,在 MAUI 里面提供 BindableObject 用來支持可系結屬性機制和附加屬性機制,本文將告訴大家在 MAUI 里面是如何在可系結物件里面提供可系結屬性和附加屬性的存盤的機制
在 WPF 里面,依賴屬性的提出的一部分原因是為了省記憶體,在 MAUI 里面,我猜測省記憶體是可系結物件提出的一個原因,由于一個界面控制元件,例如按鈕等,有著非常龐大數量的屬性,假設每個控制元件里面的所有屬性都是需要獨立的物件不能共用,那么在復雜界面上,將會因為大量的控制元件的大量屬性占用大量的記憶體,可系結物件里面可以實作在屬性沒有被賦值時,將可以使用默認值,而對于大部分控制元件來說,很多不常用的屬性都是使用默認值即可,可系結物件需要解決的是讓可系結屬性可以代替普通的 CLR 屬性,對可系結屬性進行賦值時,可以值和可系結物件關聯,從而可以讀取出來,既然名字叫可系結物件,那自然也要實作系結的支持,系結的支持的核心就是通知,需要支持在屬性值變更的時候進行通知,接下來將通過閱讀源代碼了解在 MAUI 里是如何實作
打開 MAUI 的 BindableObject 的源代碼,可以看到在 BindableObject 里有 _properties 欄位,定義如下
public abstract class BindableObject : INotifyPropertyChanged, IDynamicResourceHandler
{
readonly Dictionary<BindableProperty, BindablePropertyContext> _properties = new Dictionary<BindableProperty, BindablePropertyContext>(4);
}
沒錯,這就是在 MAUI 里面的可系結物件的存盤核心實作,在 MAUI 的可系結物件里面通過 _properties 字典存放可系結屬性的值內容,字典的 Key 是 BindableProperty 可系結屬性,字典的 Value 是 BindablePropertyContext 可系結屬性背景關系,初始化字典默認占用 4 個空間,默認初始化空間是為了優化而已,沒有什么特別用途,通過此字典定義可以了解到存盤的核心實作就是將可系結屬性和對應的值存入到物件的字典里,例如給某個可系結物件的某個叫 Xxx 的可系結屬性進行賦值,那將會對 _properties 字典更新 Xxx 屬性的值內容
在 MAUI 的實作是,在可系結物件里面,使用 SetValueCore 方法進行屬性更新賦值,我刪掉了不關鍵的邏輯的代碼如下
internal void SetValueCore(BindableProperty property, object value, SetValueFlags attributes, SetValuePrivateFlags privateAttributes)
{
// 獲取或創建可系結屬性背景關系資訊
BindablePropertyContext context = GetOrCreateContext(property);
SetValueActual(property, context, value, currentlyApplying, attributes, silent);
}
BindablePropertyContext GetOrCreateContext(BindableProperty property) => GetContext(property) ?? CreateAndAddContext(property);
internal BindablePropertyContext GetContext(BindableProperty property) => _properties.TryGetValue(property, out var result) ? result : null;
BindablePropertyContext CreateAndAddContext(BindableProperty property)
{
var context = new BindablePropertyContext { ... };
_properties.Add(property, context);
return context;
}
void SetValueActual(BindableProperty property, BindablePropertyContext context, object value, bool currentlyApplying, SetValueFlags attributes, bool silent = false)
{
// 觸發物件變更前事件
context.Value = https://www.cnblogs.com/lindexi/p/value;
// 觸發物件已變更事件
}
可以看到賦值的第一步就是呼叫 GetOrCreateContext 方法,嘗試去拿到背景關系資訊,如果拿不到就創建,這里的用到的 BindablePropertyContext 背景關系資訊是存盤可系結屬性的關鍵,在 BindablePropertyContext 里面存放了很多欄位,定義如下
public abstract class BindableObject : INotifyPropertyChanged, IDynamicResourceHandler
{
internal class BindablePropertyContext
{
public BindableContextAttributes Attributes;
public BindingBase Binding;
public Queue<SetValueArgs> DelayedSetters;
public BindableProperty Property;
public object Value;
public bool StyleValueSet;
public object StyleValue;
}
}
可以看到 BindablePropertyContext 是一個內部型別,也不對外開放,在 BindablePropertyContext 里面重要的就是 Value 欄位,表示存盤的實際值內容,其次為了更好的支持系結,也添加了 Binding 欄位
在獲取到 BindablePropertyContext 背景關系之后,即可進行賦值,賦值是呼叫 SetValueActual 方法進行賦值,賦值前后分別觸發事件用來通知,觸發通知事件最重要的功能是讓系結可以有重繪的時機,如此即可完成賦值程序
通知事件是分別觸發可系結的物件的通知事件和對應的可系結屬性的通知事件,如下面代碼
void SetValueActual(BindableProperty property, BindablePropertyContext context, object value, bool currentlyApplying, SetValueFlags attributes, bool silent = false)
{
// 觸發物件變更前事件
property.PropertyChanging?.Invoke(this, original, value);
OnPropertyChanging(property.PropertyName);
context.Value = https://www.cnblogs.com/lindexi/p/value;
// 觸發物件已變更事件
OnPropertyChanged(property.PropertyName);
property.PropertyChanged?.Invoke(this, original, value);
}
通過以上代碼可以看到,可系結物件給可系結屬性賦值的時候,就是先獲取或創建可系結屬性背景關系,將賦值的引數值給到 可系結屬性背景關系 的 Value 欄位,如此完成賦值程序
由于賦值的引數值被放入到 可系結屬性背景關系 的 Value 欄位,而 可系結屬性背景關系 又放入到 _properties 字典里,相當于間接將 賦值的引數值 放入到 _properties 字典里,自然在獲取值程序里,也需要從字典里面讀取,在 MAUI 里面讀取可系結屬性是通過 GetValue 方法實作,代碼如下
public object GetValue(BindableProperty property)
{
if (property == null)
throw new ArgumentNullException(nameof(property));
var context = property.DefaultValueCreator != null ? GetOrCreateContext(property) : GetContext(property);
return context == null ? property.DefaultValue : context.Value;
}
以上代碼的判斷 BindableProperty 的 DefaultValueCreator 屬性邏輯是 MAUI 特有的邏輯,和 WPF 與 UWP 不相同,咱下文再聊,回到獲取屬性的方法上,是通過先獲取物件的可系結背景關系資訊,如果能獲取到可系結背景關系,證明此可系結物件的這個可系結屬性曾經被賦值過,需要用賦值更新的內容,如果拿到的可系結屬性背景關系是空,那就使用可系結屬性定義的默認值即可
在 MAUI 里面,通過 BindableProperty 的 DefaultValueCreator 屬性簡化了可系結屬性的定義,和讓可系結屬性更加強大,使用 MAUI 的可系結屬性和可系結物件對比 WPF 的依賴屬性和依賴物件的實作,可以看到 MAUI 的實作實在簡潔很多,在 MAUI 里的 BindableProperty 的 DefaultValueCreator 屬性是一個委托,定義如下
public sealed class BindableProperty
{
public delegate object CreateDefaultValueDelegate(BindableObject bindable);
internal CreateDefaultValueDelegate DefaultValueCreator { get; }
}
可以看到 BindableProperty 的 DefaultValueCreator 屬性的委托是支持給傳入的可系結物件進行處理,對可系結物件回傳特定的默認值,這里值得說明的是,通過委托是可以特例給可系結物件不同的默認值的,但不代表著一定是不同的可系結物件都一定需要不同的默認值物件,這里只是一個委托,讓委托回傳相同的物件是完全可以的,這個委托更多的是使用在判斷可系結物件型別,根據可系結型別物件或者狀態,回傳不同的默認值,或者是回傳一個需要運行時動態計算值,而不是一個可以寫固定在代碼里面的引數
例如對于 FontSize 的可系結屬性的定義里,就采用讓不同的控制元件回傳不同的默認字體大小,定義如下
public static readonly BindableProperty FontSizeProperty =
BindableProperty.Create("FontSize", typeof(double), typeof(IFontElement), 0d,
propertyChanged: OnFontSizeChanged,
defaultValueCreator: FontSizeDefaultValueCreator);
static object FontSizeDefaultValueCreator(BindableObject bindable)
=> ((IFontElement)bindable).FontSizeDefaultValueCreator();
也就是說對于不同的可系結物件,獲取到的默認的字體大小是根據對應的可系結物件的 FontSizeDefaultValueCreator 方法實作決定,不同的可系結物件可以有不同的實作,從而實作了讓默認值關聯上具體的可系結物件型別,這個創新的設計,可以省掉在 WPF 里面的大量默認依賴屬性值重寫的邏輯代碼,省掉了這部分代碼,也可以大量減少的機制,從而減少更多的代碼
例如 Span 和 Editor 控制元件對字體大小默認值有不同的實作
public class Span : GestureElement, IFontElement
{
double IFontElement.FontSizeDefaultValueCreator() =>
double.NaN;
}
public partial class Button : View, IFontElement
{
double IFontElement.FontSizeDefaultValueCreator() =>
this.GetDefaultFontSize();
}
同樣,對于某些可系結屬性來說,需要給每個可系結物件的物件不同的默認值物件,例如 Grid 里面的 RowDefinitions 屬性,大家都知道,在 Grid 里面的 RowDefinitions 是一個集合,如果集合也是一個共享的默認值,那自然會存在默認值污染,如果默認值是一個空值,那么將會讓 Grid 邏輯里面存在大量的判斷空邏輯,或者需要其他額外的初始化邏輯,在 MAUI 里面,通過 DefaultValueCreator 委托,實作了每個 Grid 物件使用獨立的默認值物件,代碼如下
public class Grid : Layout, IGridLayout
{
public static readonly BindableProperty RowDefinitionsProperty = BindableProperty.Create("RowDefinitions",
typeof(RowDefinitionCollection), typeof(Grid), null, validateValue: (bindable, value) => value != null,
propertyChanged: UpdateSizeChangedHandlers, defaultValueCreator: bindable =>
{
// 每個 Grid 物件使用獨立的,新創建的默認值物件
var rowDef = new RowDefinitionCollection();
rowDef.ItemSizeChanged += ((Grid)bindable).DefinitionsChanged;
return rowDef;
});
}
在 MAUI 里面除了可系結屬性之外,還有一個特殊的屬性型別,附加屬性,附加屬性可以定義在任意的型別里面,通過附加屬性,給某個現有的型別附加上屬性,功能上和 WPF 或 UWP 的附加屬性功能是相同的,可系結屬性和附加屬性都是相同的 BindableProperty 型別,只是在創建的時候,呼叫的靜態創建方法不同而已,對于可系結屬性來說,呼叫的是 BindableProperty.Create 方法創建,對于附加屬性來說,呼叫 BindableProperty.CreateAttached 創建,在 MAUI 里面,通過閱讀代碼,我認為分開兩個方法更多的是為了兼容 WPF 或 UWP 的寫法,沒有非常本質的差別,引數也差不多,如下面代碼
internal static BindableProperty Create(string propertyName, Type returnType, [DynamicallyAccessedMembers(DeclaringTypeMembers)] Type declaringType, object defaultValue, BindingMode defaultBindingMode, ValidateValueDelegate validateValue,
BindingPropertyChangedDelegate propertyChanged, BindingPropertyChangingDelegate propertyChanging, CoerceValueDelegate coerceValue, BindablePropertyBindingChanging bindingChanging,
CreateDefaultValueDelegate defaultValueCreator = null)
{
return new BindableProperty(propertyName, returnType, declaringType, defaultValue, defaultBindingMode, validateValue, propertyChanged, propertyChanging, coerceValue, bindingChanging,
defaultValueCreator: defaultValueCreator);
}
internal static BindableProperty CreateAttached(string propertyName, Type returnType, [DynamicallyAccessedMembers(DeclaringTypeMembers)] Type declaringType, object defaultValue, BindingMode defaultBindingMode, ValidateValueDelegate validateValue,
BindingPropertyChangedDelegate propertyChanged, BindingPropertyChangingDelegate propertyChanging, CoerceValueDelegate coerceValue, BindablePropertyBindingChanging bindingChanging,
bool isReadOnly, CreateDefaultValueDelegate defaultValueCreator = null)
{
return new BindableProperty(propertyName, returnType, declaringType, defaultValue, defaultBindingMode, validateValue, propertyChanged, propertyChanging, coerceValue, bindingChanging, isReadOnly,
defaultValueCreator);
}
如此可以看到可系結屬性和附加屬性從引數上是似乎相同的,由于附加屬性也是一個可系結屬性型別,同理可以了解到附加屬性的存盤也和可系結物件的可系結屬性的存盤是相同的,如此也能解答一個問題,在 MAUI 的附加屬性,附加到物件上,附加屬性的引數值是如何跟隨物件的生命周期的問題,由于附加屬性也是一個可系結屬性,同樣將引數值存在可系結物件的 _properties 字典里面,在物件會 GC 回收時,自然 _properties 欄位也被回收,那放在字典里面的引數值也自然被減去參考,當引數值的沒有被參考時,也就自然被回收
在 MAUI 里面,可系結物件基型別的意義就是提供了可系結屬性的機制,存盤可系結屬性的方式就是通過 _properties 字典存放,通過字典存放的內容是被賦值更改的屬性,沒有賦值更改的屬性是沒有被放入到字典里面,獲取在字典里面沒有存放的屬性時,將會通過對應的可系結屬性獲取到默認值,默認值的獲取有兩個方式,一個是可系結屬性的固定的默認值屬性,另一個是通過可系結屬性的默認值創建委托創建默認值,在 MAUI 里的可系結屬性的默認值創建委托是一個創新,可以寫出讓不同的可系結物件使用不同的默認值的功能,也可以寫出根據不同的可系結物件型別回傳不同的默認值,通過委托的方式靈活實作復雜的功能
更多的 MAUI 相關博客,還請參閱我的 博客導航
博客園博客只做備份,博客發布就不再更新,如果想看最新博客,請到 https://blog.lindexi.com/

本作品采用知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議進行許可,歡迎轉載、使用、重新發布,但務必保留文章署名[林德熙](http://blog.csdn.net/lindexi_gd)(包含鏈接:http://blog.csdn.net/lindexi_gd ),不得用于商業目的,基于本文修改后的作品務必以相同的許可發布,如有任何疑問,請與我[聯系](mailto:[email protected]),
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/507078.html
標籤:.NET Core
