再學Blazor——元件建造者

known發表於2023-11-25

使用 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元件的需求。