使用 RenderTreeBuilder 建立元件是 Blazor 的一種高階方案。前幾篇文中有這樣建立元件的示例 builder.Component<MyComponent>().Build();
,本文主要介紹該高階方案的具體實現,我們採用測試驅動開發(TDD)方法,大致思路如下:
- 從測試示例入手
- 擴充套件一個RenderTreeBuilder類的泛型擴充套件方法,泛型型別為元件型別
- 建立元件建造者類(ComponentBuilder)提供方法來構建元件
- 透過元件的屬性選擇器來設定元件引數
- 構建時能返回元件的物件例項
1. 示例
首頁我們從一個我們預想的高階方案示例入手,然後逐漸分析並實現我們預想的方案。下面是預想的示例程式碼:
class MyComponent : ComponentBase
{
private MyTest test; //MyTest元件的物件例項
//覆寫構建呈現樹方法
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
builder.Component<MyTest>()
.Set(c => c.Title, "Hello") //設定MyTest元件Title引數
.Build(value => test = value); //建造元件並給MyTest例項賦值
}
}
2. 擴充套件方法
下面實現builder.Component<MyTest>()
這行程式碼,這是RenderTreeBuilder
的一個擴充套件方法,該方法返回元件建造者類(ComponentBuilder)。
public static class Extension
{
//泛型T是Blazor元件型別
public static ComponentBuilder<T> Component<T>(this RenderTreeBuilder builder) where T : notnull, IComponent
{
//返回一個元件建造者類物件,將builder傳遞給建造者
//其內部方法需要透過builder來構建元件
return new ComponentBuilder<T>(builder);
}
}
3. 建造者類
接下來實現元件建造者類(ComponentBuilder),該類是手動構建元件的核心程式碼,提供設定元件引數以及構建方法。
public class ComponentBuilder<T> where T : IComponent
{
//手動構建呈現器
private readonly RenderTreeBuilder builder;
//元件引數字典,設定元件引數時,先存入字典,在構建時批次新增
internal readonly Dictionary<string, object> Parameters = new(StringComparer.Ordinal);
//建構函式
internal ComponentBuilder(RenderTreeBuilder builder)
{
this.builder = builder;
}
//新增元件引數方法,name為元件引數名稱,value為元件引數值
//提供Add方法可以新增非元件定義的屬性,例如html屬性
public ComponentBuilder<T> Add(string name, object value)
{
Parameters[name] = value; //將引數存入字典
return this; //返回this物件,可以流式操作
}
//設定元件引數方法,selector為元件引數屬性選擇器表示式,value為元件引數值
//使用選擇器有如下優點:
// - 當元件屬性名稱更改時,可自動替換
// - 透過表示式 c => c. 可以直接調出元件定義的屬性,方便閱讀
// - 可透過TValue直接限定屬性的型別,開發時即可編譯檢查
public ComponentBuilder<T> Set<TValue>(Expression<Func<T, TValue>> selector, TValue value)
{
var property = TypeHelper.Property(selector); //透過屬性選擇器表示式獲取元件引數屬性
return Add(property.Name, value); //新增元件引數
}
//元件構建方法,action為返回元件物件例項的委託,預設為空不返回例項
public void Build(Action<T> action = null)
{
builder.OpenComponent<T>(0); //開始附加元件
if (Parameters.Count > 0)
builder.AddMultipleAttributes(1, Parameters); //批次新增元件引數
if (action != null)
builder.AddComponentReferenceCapture(2, value => action.Invoke((T)value)); //返回元件物件例項
builder.CloseComponent(); //結束附加元件
}
}
4. 屬性選擇器
為什麼要用屬性選擇器,元件建造者類中已經提到,下面介紹如何透過屬性選擇器表示式來獲取元件型別的屬性物件。
public class TypeHelper
{
//透過屬性選擇器表示式來獲取指定型別的屬性
public static PropertyInfo Property<T, TValue>(Expression<Func<T, TValue>> selector)
{
if (selector is null)
throw new ArgumentNullException(nameof(selector));
if (selector.Body is not MemberExpression expression || expression.Member is not PropertyInfo propInfoCandidate)
throw new ArgumentException($"The parameter selector '{selector}' does not resolve to a public property on the type '{typeof(T)}'.", nameof(selector));
var type = typeof(T);
var propertyInfo = propInfoCandidate.DeclaringType != type
? type.GetProperty(propInfoCandidate.Name, propInfoCandidate.PropertyType)
: propInfoCandidate;
if (propertyInfo is null)
throw new ArgumentException($"The parameter selector '{selector}' does not resolve to a public property on the type '{typeof(T)}'.", nameof(selector));
return propertyInfo;
}
}
5. 總結
以上就是元件建造者的完整實現過程,程式碼不長,但這些功能足以完成手動構建Blazor元件的需求。