閱讀須知:本文為入門介紹、指引文章,所示代碼皆為最簡易(或僅為實現功能)的演示示例版本,不一定切實符合個人(企業)實際開發需求。 一、DbContext生存期 DbContext 的生存期從創建實例時開始,併在釋放實例時結束。 DbContext 實例旨在用於單個工作單元。這意味著 DbContex ...
和 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 = 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 = 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])。