[Asp.net core 3.1] 經過一個小組件熟悉Blazor服務端組件開發

經過一個小組件,熟悉 Blazor 服務端組件開發。githubcss

1、環境搭建

vs2019 16.4, asp.net core 3.1 新建 Blazor 應用,選擇 asp.net core 3.1。 根文件夾下新增目錄 Components,放置代碼。html

2、組件需求定義

Components 目錄下新建一個接口文件(interface)看成文檔,加個 using using Microsoft.AspNetCore.Components;git

先從直觀的方面入手。github

  • 相似 html 標籤對的組件,樣子相似<xxx propA="aaa" data-propB="123" ...>其餘標籤或內容...</xxx><xxx .../>。接口名:INTag.
  • 須要 Id 和名稱,方便區分和調試。string TagId{get;set;} string TagName{get;set;}.
  • 須要樣式支持。加上string Class{get;set;} string Style{get;set;}
  • 不經常使用的屬性也提供支持,使用字典。IDictionary<string,object> CustomAttributes { get; set; }
  • 應該提供 js 支持。加上using Microsoft.JSInterop; 屬性 IJSRuntime JSRuntime{get;set;}

考慮一下功能方面。面試

  • 既然是標籤對,那就有可能會嵌套,就會產生層級關係或父子關係。由於只是可能,因此咱們新建一個接口,用來提供層級關係處理,IHierarchyComponent。
  • 須要一個 Parent ,類型就定爲 Microsoft.AspNetCore.Components.IComponent.IComponent Parent { get; set; }.
  • 要能添加子控件,void AddChild(IComponent child);,有加就有減,void RemoveChild(IComponent child);
  • 提供一個集合方便遍歷,咱們已經提供了 Add/Remove,讓它只讀就好。 IEnumerable<IComponent> Children { get;}
  • 一旦有了 Children 集合,咱們就須要考慮何時從集合裏移除組件,讓 IHierarchyComponent 實現 IDisposable,保證組件被釋放時解開父子/層級關係。
  • 組件須要處理樣式,僅有 Class 和 Style 可能不夠,一般還會須要 Skin、Theme 處理,增長一個接口記錄一下, public interface ITheme{ string GetClass<TComponent>(TComponent component); }。INTag 增長一個屬性 ITheme Theme { get; set; }

INTag:c#

public interface INTag
    {
        string TagId { get; set; }
        string TagName { get;  }
        string Class { get; set; }
        string Style { get; set; }
        ITheme Theme { get; set; }
        IJSRuntime JSRuntime { get; set; }
        IDictionary<string,object> CustomAttributes { get; set; }
    }

IHierarchyComponent:瀏覽器

public interface IHierarchyComponent:IDisposable
    {
        IComponent Parent { get; set; }
        IEnumerable<IComponent> Children { get;}
        void AddChild(IComponent child);
        void RemoveChild(IComponent child);
    }

IThemeasp.net

public interface ITheme
    {
        string GetClass<TComponent>(TComponent component);
    }

組件的基本信息 INTag 有了,須要的話能夠支持層級關係 IHierarchyComponent,能夠考慮下一些特定功能的處理及類型部分。async

  • Blazor 組件實現相似 <xxx>....</xxx>這種可打開的標籤對,須要提供一個 RenderFragment 或 RenderFragment<TArgs>屬性。RenderFragment 是一個委託函數,帶參的明顯更靈活些,可是參數類型很差肯定,很差肯定的類型用泛型。再加一個接口,INTag< TArgs >:INTag, 一個屬性 RenderFragment<TArgs> ChildContent { get; set; }.
  • 組件的主要目的是爲了呈現咱們的數據,也就是通常說的 xxxModel,Data....,類型不肯定,那就加一個泛型。INTag< TArgs ,TModel>:INTag.
  • RenderFragment 是一個函數,ChildContent 是一個函數屬性,不是方法。在方法內,咱們可使用 this 來訪問組件自身引用,可是函數內部實際上是沒有 this 的。爲了更好的使用組件自身,這裏增長一個泛型用於指代自身,public interface INTag<TTag, TArgs, TModel>:INTag where TTag: INTag<TTag, TArgs, TModel>

INTag[TTag, TArgs, TModel ]ide

public interface INTag<TTag, TArgs, TModel>:INTag
        where TTag: INTag<TTag, TArgs, TModel>
    {
        /// <summary>
        /// 標籤對之間的內容,<see cref="TArgs"/> 爲參數,ChildContent 爲Blazor約定名。
        /// </summary>
        RenderFragment<TArgs> ChildContent { get; set; }
    }

回顧一下咱們的幾個接口。

  • INTag:描述了組件的基本信息,即組件的樣子。
  • IHierarchyComponent 提供了層級處理能力,屬於組件的擴展能力。
  • ITheme 提供了 Theme 接入能力,也屬於組件的擴展能力。
  • INTag<TTag, TArgs, TModel> 提供了打開組件的能力,ChildContent 像一個動態模板同樣,讓咱們能夠在聲明組件時自行決定組件的部份內容和結構。
  • 全部這些接口最主要的目的實際上是爲了產生一個合適的 TArgs, 去調用 ChildContent。
  • 有描述,有能力還有了主要目的,咱們就能夠去實現 NTag 組件。

3、組件實現

抽象基類 AbstractNTag

Components 目錄下新增 一個 c#類,AbstractNTag.cs, using Microsoft.AspNetCore.Components; 藉助 Blazor 提供的 ComponentBase,實現接口。

public    abstract class AbstractNTag<TTag, TArgs, TModel> : ComponentBase, IHierarchyComponent, INTag<TTag, TArgs, TModel>
   where TTag: AbstractNTag<TTag, TArgs, TModel>{

}

調整一下 vs 生成的代碼, IHierarchyComponent 使用字段實現一下。

Children:

List<IComponent> _children = new List<IComponent>();

 public void AddChild(IComponent child)
        {
            this._children.Add(child);

        }
        public void RemoveChild(IComponent child)
        {
            this._children.Remove(child);
        }

Parent,dispose

IComponent _parent;
public IComponent Parent { get=>_parent; set=>_parent=OnParentChange(_parent,value); }
 protected virtual IComponent OnParentChange(IComponent oldValue, IComponent newValue)
        {

            if(oldValue is IHierarchyComponent o) o.RemoveChild(this);
            if(newValue is IHierarchyComponent n) n.AddChild(this);
            return newValue;
        }
public void Dispose()
        {
            this.Parent = null;
        }

增長對瀏覽器 console.log 的支持, razor Attribute...,完整的 AbstractNTag.cs

public    abstract class AbstractNTag<TTag, TArgs, TModel> : ComponentBase, IHierarchyComponent, INTag<TTag, TArgs, TModel>
   where TTag: AbstractNTag<TTag, TArgs, TModel>
{
 List<IComponent> _children = new List<IComponent>();
        IComponent _parent;

        public string TagName => typeof(TTag).Name;
        [Inject]public IJSRuntime JSRuntime { get; set; }
        [Parameter]public RenderFragment<TArgs> ChildContent { get; set; }
        [Parameter] public string TagId { get; set; }

        [Parameter]public string Class { get; set; }
        [Parameter]public string Style { get; set; }
        [Parameter(CaptureUnmatchedValues =true)]public IDictionary<string, object> CustomAttributes { get; set; }

        [CascadingParameter] public IComponent Parent { get=>_parent; set=>_parent=OnParentChange(_parent,value); }
        [CascadingParameter] public ITheme Theme { get; set; }

         public bool TryGetAttribute(string key, out object value)
        {
            value = null;
            return CustomAttributes?.TryGetValue(key, out value) ?? false;
        }
        public IEnumerable<IComponent> Children { get=>_children;}

        protected virtual IComponent OnParentChange(IComponent oldValue, IComponent newValue)
        {
                ConsoleLog($"OnParentChange: {newValue}");
            if(oldValue is IHierarchyComponent o) o.RemoveChild(this);
            if(newValue is IHierarchyComponent n) n.AddChild(this);
            return newValue;
        }

        protected bool FirstRender = false;

        protected override void OnAfterRender(bool firstRender)
        {
            FirstRender = firstRender;
            base.OnAfterRender(firstRender);

        }
        public override Task SetParametersAsync(ParameterView parameters)
        {
            return base.SetParametersAsync(parameters);
        }

          int logid = 0;
        public object ConsoleLog(object msg)
        {
            logid++;
            Task.Run(async ()=> await this.JSRuntime.InvokeVoidAsync("console.log", $"{TagName}[{TagId}_{ logid}:{msg}]"));
            return null;
        }


        public void AddChild(IComponent child)
        {
            this._children.Add(child);

        }
        public void RemoveChild(IComponent child)
        {
            this._children.Remove(child);
        }
        public void Dispose()
        {
            this.Parent = null;
        }
}
  • Inject 用於注入
  • Parameter 支持組件聲明的 Razor 語法中直接賦值,<NTag Class="ssss" .../>;
  • Parameter(CaptureUnmatchedValues =true) 支持聲明時將組件上沒定義的屬性打包賦值;
  • CascadingParameter 配合 Blazor 內置組件 <CascadingValue Value="xxx" >... <NTag /> ...</CascadingValue>,捕獲 Value。處理過程和級聯樣式表(css)很相似。

具體類 NTag

泛型其實就是定義在類型上的函數,TTag,TArgs,TModel 就是 入參,獲得的類型就是返回值。所以處理泛型定義的過程,就很相似函數逐漸消參的過程。好比:

func(a,b,c)
  肯定a以後,func(b,c)=>func(1,b,c);
  肯定b以後,func(c)=>func(1,2,c);
  最終: func()=>func(1,2,3);
  執行 func 能夠獲得一個明確的結果。

一樣的,咱們繼承 NTag 基類時須要考慮各個泛型參數應該是什麼:

  • TTag:這個很容易肯定,誰繼承了基類就是誰。
  • TModel: 這個不到最後使用咱們是沒法肯定的,須要保留。
  • TArgs: 前面說過,組件的主要目的是爲了給 ChildContent 提供參數.從這一目的出發,TTag 和 TModel 的用途之一就是給TArgs提供類型支持,或者說 TArgs 應該包含 TTag 和 TModel。又由於 ChildContent 只有一個參數,所以 TArgs 應該有必定的擴展性,不妨給他一個屬性作擴展。 綜合一下,TArgs 的大概模樣就有了,來個 struct。
public struct RenderArgs<TTag,TModel>
    {
        public TTag Tag;
        public TModel Model;
        public object Arg;

        public RenderArgs(TTag tag, TModel model, object arg  ) {
            this.Tag = tag;
            this.Model = model;
            this.Arg = arg;

        }
    }
  • RenderArgs 屬於經常使用輔助類型,所以不須要給 TArgs 指定約束。

Components 目錄下新增 Razor 組件,NTag.razor;aspnetcore3.1 組件支持分部類,新增一個 NTag.razor.cs;

NTag.razor.cs 就是標準的 c#類寫法

public partial  class NTag< TModel> :AbstractNTag<NTag<TModel>,RenderArgs<NTag<TModel>,TModel>,TModel>
    {
        [Parameter]public TModel Model { get; set; }

        public RenderArgs<NTag<TModel>, TModel> Args(object arg=null)
        {

            return new RenderArgs<NTag<TModel>, TModel>(this, this.Model, arg);
        }
    }

重寫一下 NTag 的 ToString,方便測試

public override string ToString()
        {
            return $"{this.TagName}<{typeof(TModel).Name}>[{this.TagId},{Model}]";
        }

NTag.razor

@typeparam TModel
@inherits AbstractNTag<NTag<TModel>,RenderArgs<NTag<TModel>,TModel>,TModel>//保持和NTag.razor.cs一致
   @if (this.ChildContent == null)
        {
            <div>@this.ToString()</div>//默認輸出,用於測試
        }
        else
        {
            @this.ChildContent(this.Args());
        }
@code {

}

簡單測試一下, 數據就用項目模板自帶的 Data 打開項目根目錄,找到_Imports.razor,把 using 加進去

@using xxxx.Data
@using xxxx.Components

新增 Razor 組件【Test.razor】

未打開的NTag,輸出NTag.ToString():
<NTag TModel="object" />
打開的NTag:
<NTag Model="TestData" Context="args" >
        <div>NTag內容 @args.Model.Summary; </div>
</NTag>

<NTag Model="@(new {Name="匿名對象" })" Context="args">
    <div>匿名Model,使用參數輸出【Name】屬性: @args.Model.Name</div>
</NTag>

@code{
WeatherForecast TestData = new WeatherForecast { TemperatureC = 222, Summary = "aaa" };
}

轉到 Pages/Index.razor, 增長一行<Test />,F5 。

應用級聯參數 CascadingValue/CascadingParameter

咱們的組件中 Theme 和 Parent 被標記爲【CascadingParameter】,所以須要經過 CascadingValue 把值傳遞過來。

首先,修改一下測試組件,使用嵌套 NTag,描述一個樹結構,Model 值指定爲樹的 Level。

<NTag Model="0" TagId="root" Context="root">
        <div>root.Parent:@root.Tag.Parent  </div>
        <div>root Theme:@root.Tag.Theme</div>

        <NTag TagId="t1" Model="1" Context="t1">
            <div>t1.Parent:@t1.Tag.Parent  </div>
            <div>t1 Theme:@t1.Tag.Theme</div>
            <NTag TagId="t1_1" Model="2" Context="t1_1">
                <div>t1_1.Parent:@t1_1.Tag.Parent  </div>
                <div>t1_1 Theme:@t1_1.Tag.Theme </div>
                <NTag TagId="t1_1_1" Model="3" Context="t1_1_1">
                    <div>t1_1_1.Parent:@t1_1_1.Tag.Parent </div>
                    <div>t1_1_1 Theme:@t1_1_1.Tag.Theme </div>
                </NTag>
                <NTag TagId="t1_1_2" Model="3" Context="t1_1_2">
                    <div>t1_1_2.Parent:@t1_1_2.Tag.Parent</div>
                    <div>t1_1_2 Theme:@t1_1_2.Tag.Theme </div>
                </NTag>
            </NTag>
        </NTag>

    </NTag>

一、 Theme:Theme 的特色是共享,不管組件在什麼位置,都應該共享同一個 Theme。這類場景,只須要簡單的在組件外套一個 CascadingValue。

<CascadingValue Value="Theme.Default">
<NTag  TagId="root" ......
</CascadingValue>

F5 跑起來,結果大體以下:

root.Parent:
<div>root Theme:Theme[blue]</div> 
        <div>t1.Parent:  </div> 
        <div>t1 Theme:Theme[blue]</div> 
            <div>t1_1.Parent:  </div>
            <div>t1_1 Theme:Theme[blue] </div>
                <div>t1_1_1.Parent: </div>
                <div>t1_1_1 Theme:Theme[blue] </div>
                <div>t1_1_2.Parent:</div>
                <div>t1_1_2 Theme:Theme[blue] </div>

二、Parent:Parent 和 Theme 不一樣,咱們但願他和咱們組件的聲明結構保持一致,這就須要咱們在每一個 NTag 內部增長一個 CascadingValue,直接寫在 Test 組件裏過於囉嗦了,讓咱們調整一下 NTag 代碼。打開 NTag.razor,修改一下,Test.razor 不動。

<CascadingValue Value="this">
        @if (this.ChildContent == null)
        {
            <div>@this.ToString()</div>//默認輸出,用於測試
        }
        else
        {
            @this.ChildContent(this.Args());
        }
     </CascadingValue>

看一下結果

root.Parent:
<div>root Theme:Theme[blue]</div>  
        <div> t1.Parent:NTag`1[root,0]  </div> 
        <div>t1 Theme:Theme[blue]</div>  
            <div> t1_1.Parent:NTag`1[t1,1]  </div> 
            <div> t1_1 Theme:Theme[blue] </div>  
                <div> t1_1_1.Parent:NTag`1[t1_1,2] </div> 
                <div> t1_1_1 Theme:Theme[blue] </div>  
                <div> t1_1_2.Parent:NTag`1[t1_1,2]</div> 
                <div> t1_1_2 Theme:Theme[blue] </div>
  • CascadingValue/CascadingParameter 除了能夠經過類型匹配以外還能夠指定 Name。

呈現 Model

到目前爲止,咱們的 NTag 主要在處理一些基本功能,好比隱式的父子關係、子內容 ChildContent、參數、泛型。。接下來咱們考慮如何把一個 Model 呈現出來。

對於常見的 Model 對象來講,呈現 Model 其實就是把 Model 上的屬性、字段。。。這些成員信息呈現出來,所以咱們須要給 NTag 增長一點能力。

  • 描述成員最直接的想法就是 lambda,model=>model.xxxx,此時咱們只須要 Model 就足夠了;
  • UI 呈現時僅有成員還不夠,一般會有格式化需求,好比:{0:xxxx}; 或者帶有先後綴: "¥{xxxx}元整",甚至就是一個常量。。。。此類信息一般應記錄在組件上,所以咱們須要組件自身。
  • 呈現時有時還會用到一些環境變量,好比序號/行號這種,所以須要引入一個參數。
  • 以上需求能夠很容易的推導出一個函數類型:Func<TTag, TModel,object,object> ;考慮 TTag 就是組件自身,這裏能夠簡化一下:Func<TModel,object,object>。 主要目的是從 model 上取值,兼顧格式化及環境變量處理,返回結果會直接用於頁面呈現輸出。

調整下 NTag 代碼,增長一個類型爲 Func<TModel,TArg,object> 的 Getter 屬性,打上【Parameter】標記。

[Parameter]public Func<TModel,object,object> Getter { get; set; }
  • 此處也可以使用表達式(Expression<Func<TModel,object,object>>),須要增長一些處理。
  • 呈現時一般還須要一些文字信息,好比 lable,text 之類, 支持一下;
[Parameter] public string Text { get; set; }
  • UI 呈現的需求難以肯定,一般還會有對狀態的處理, 這裏提供一些輔助功能就能夠。

一個小枚舉

public enum NVisibility
    {
        Default,
        Markup,
        Hidden
    }

狀態屬性和 render 方法,NTag.razor.cs

[Parameter] public NVisibility TextVisibility { get; set; } = NVisibility.Default;
        [Parameter] public bool ShowContent { get; set; } = true;

 public RenderFragment RenderText()
        {
            if (TextVisibility == NVisibility.Hidden|| string.IsNullOrEmpty(this.Text)) return null;
            if (TextVisibility == NVisibility.Markup) return (b) => b.AddContent(0, (MarkupString)Text);
            return (b) => b.AddContent(0, Text);

        }
        public RenderFragment RenderContent(RenderArgs<NTag<TModel>, TModel> args)
        {
           return   this.ChildContent?.Invoke(args) ;
        }
        public RenderFragment RenderContent(object arg=null)
        {
            return this.RenderContent(this.Args(arg));
        }

NTag.razor

<CascadingValue Value="this">
        @RenderText()
        @if (this.ShowContent)
        {
            var render = RenderContent();
            if (render == null)
            {
                <div>@this</div>//測試用
            }
            else
            {
                @render//render 是個函數,使用@才能輸出,若是不考慮測試代碼,能夠直接 @RenderContent()
            }

        }
    </CascadingValue>

Test.razor 增長測試代碼

七、呈現Model
<br />
value:@@arg.Tag.Getter(arg.Model,null)
<br />
<NTag Text="日期" Model="TestData" Getter="(m,arg)=>m.Date" Context="arg">
    <input type="datetime" value="@arg.Tag.Getter(arg.Model,null)" />
</NTag>
<br />
Text中使用Markup:value:@@((DateTime)arg.Tag.Getter(arg.Model, null))
<br />
<label>
    <NTag Text="<span style='color:red;'>日期</span>" TextVisibility="NVisibility.Markup" Model="TestData" Getter="(m,a)=>m.Date" Context="arg">
        <input type="datetime" value="@((DateTime)arg.Tag.Getter(arg.Model,null))" />
    </NTag>
</label>
<br />
也能夠直接使用childcontent:value:@@arg.Model.Date
<div>
    <NTag Model="TestData" Getter="(m,a)=>m.Date" Context="arg">
        <label> <span style='color:red;'>日期</span> <input type="datetime" value="@arg.Model.Date" /></label>
    </NTag>
</div>
getter 格式化:@@((m,a)=>m.Date.ToString("yyyy-MM-dd"))
<div>
    <NTag Model="TestData" Getter="@((m,a)=>m.Date.ToString("yyyy-MM-dd"))" Context="arg">
        <label> <span style='color:red;'>日期</span> <input type="datetime" value="@arg.Tag.Getter(arg.Model,null)" /></label>
    </NTag>
</div>
使用customAttributes ,藉助外部方法推斷TModel類型
<div>
    <NTag type="datetime"  Getter="@GetGetter(TestData,(m,a)=>m.Date)" Context="arg">
        <label> <span style='color:red;'>日期</span> <input @attributes="arg.Tag.CustomAttributes"  value="@arg.Tag.Getter(arg.Model,null)" /></label>
    </NTag>
</div>

@code {
    WeatherForecast TestData = new WeatherForecast { TemperatureC = 222, Date = DateTime.Now, Summary = "test summary" };

    Func<T, object, object> GetGetter<T>(T model, Func<T, object, object> func) {
        return (m, a) => func(model, a);
    }
}

考察一下測試代碼,咱們發現 用做取值的 arg.Tag.Getter(arg.Model,null) 明顯有些囉嗦了,調整一下 RenderArgs,讓它能夠直接取值。

public struct RenderArgs<TTag,TModel>
    {
        public TTag Tag;
        public TModel Model;
        public object Arg;
        Func<TModel, object, object> _valueGetter;
        public object Value => _valueGetter?.Invoke(Model, Arg);
        public RenderArgs(TTag tag, TModel model, object arg  , Func<TModel, object, object> valueGetter=null) {
            this.Tag = tag;
            this.Model = model;
            this.Arg = arg;
            _valueGetter = valueGetter;
        }
    }
//NTag.razor.cs
 public RenderArgs<NTag<TModel>, TModel> Args(object arg = null)
        {

            return new RenderArgs<NTag<TModel>, TModel>(this, this.Model, arg,this.Getter);
        }

集合,Table 行列

集合的簡單處理只須要循環一下。Test.razor

<ul>
    @foreach (var o in this.Datas)
    {
        <NTag Model="o" Getter="(m,a)=>m.Summary" Context="arg">
            <li @key="o">@arg.Value</li>
        </NTag>
    }
</ul>
@code {

    IEnumerable<WeatherForecast> Datas = Enumerable.Range(0, 10)
        .Select(i => new WeatherForecast { Summary = i + "" });

}

複雜一點的時候,好比 Table,就須要使用列。

  • 列有 header:可使用 NTag.Text;
  • 列要有單元格模板:NTag.ChildContent;
  • 行就是全部列模板的呈現集合,行數據便是集合數據源的一項。
  • 具體到 table 上,thead 定義列,tbody 生成行。

新增一個組件用於測試:TestTable.razor,試着用 NTag 呈現一個 table。

<NTag TagId="table" TModel="WeatherForecast" Context="tbl">
    <table>
        <thead>
            <tr>
                <NTag Text="<th>#</th>"
                      TextVisibility="NVisibility.Markup"
                      ShowContent="false"
                      TModel="WeatherForecast"
                      Getter="(m, a) =>a"
                      Context="arg">
                    <td>@arg.Value</td>
                </NTag>
                <NTag Text="<th>Summary</th>"
                      TextVisibility="NVisibility.Markup"
                      ShowContent="false"
                      TModel="WeatherForecast"
                      Getter="(m, a) => m.Summary"
                      Context="arg">
                    <td>@arg.Value</td>
                </NTag>
                <NTag Text="<th>Date</th>"
                      TextVisibility="NVisibility.Markup"
                      ShowContent="false"
                      TModel="WeatherForecast"
                      Getter="(m, a) => m.Date"
                      Context="arg">
                    <td>@arg.Value</td>
                </NTag>
            </tr>
        </thead>
        <tbody>
            <CascadingValue Value="default(object)">
                @{ var cols = tbl.Tag.Children;
                    var i = 0;
                    tbl.Tag.ConsoleLog(cols.Count());
                }
                @foreach (var o in Source)
                {
                    <tr @key="o">
                        @foreach (var col in cols)
                        {
                            if (col is NTag<WeatherForecast> tag)
                            {
                                @tag.RenderContent(tag.Args(o,i ))
                            }
                        }
                    </tr>
                    i++;
                }
            </CascadingValue>

        </tbody>
    </table>
</NTag>

@code {

    IEnumerable<WeatherForecast> Source = Enumerable.Range(0, 10)
        .Select(i => new WeatherForecast { Date=DateTime.Now,Summary=$"data_{i}", TemperatureC=i });

}
  • 服務端模板處理時,代碼會先於輸出執行,直觀的說,就是組件在執行時會有層級順序。因此咱們在 tbody 中增長了一個 CascadingValue,推遲一下代碼的執行時機。不然,tbl.Tag.Children會爲空。
  • thead 中的 NTag 做爲列定義使用,與最外的 NTag(table)正好造成父子關係。
  • 觀察下 NTag,咱們發現有些定義重複了,好比 TModel,單元格<td>@arg.Value</td>。下面試着簡化一些。

以前測試 Model 呈現的代碼中咱們說到能夠 「藉助外部方法推斷 TModel 類型」,當時使用了一個 GetGetter 方法,讓咱們試着在 RenderArg 中增長一個相似方法。

RenderArgs.cs:

public Func<TModel, object, object> GetGetter(Func<TModel, object, object> func) => func;
  • GetGetter 極簡單,不須要任何邏輯,直接返回參數。原理是 RenderArgs 可用時,TModel 必然是肯定的。

用法:

<NTag Text="<th>#<th>"
                      TextVisibility="NVisibility.Markup"
                      ShowContent="false"
                      Getter="(m, a) =>a"
                      Context="arg">
                    <td>@arg.Value</td>

做爲列的 NTag,每列的 ChildContent 實際上是同樣的,變化的只有 RenderArgs,所以只須要定義一個就足夠了。

NTag.razor.cs 增長一個方法,對於 ChildContent 爲 null 的組件咱們使用一個默認組件來 render。

public RenderFragment RenderChildren(TModel model, object arg=null)
        {
            return (builder) =>
            {
                var children = this.Children.OfType<NTag<TModel>>();
                NTag<TModel> defaultTag = null;
                foreach (var child in children)
                {
                    if (defaultTag == null && child.ChildContent != null) defaultTag = child;
                    var render = (child.ChildContent == null ? defaultTag : child);
                    render.RenderContent(child.Args(model, arg))(builder);
                }
            };

        }

TestTable.razor

<NTag TagId="table" TModel="WeatherForecast" Context="tbl">
    <table>
        <thead>
            <tr>
                <NTag Text="<th >#</th>"
                      TextVisibility="NVisibility.Markup"
                      ShowContent="false"
                      Getter="tbl.GetGetter((m,a)=>a)"
                      Context="arg">
                    <td>@arg.Value</td>
                </NTag>
                <NTag Text="<th>Summary</th>"
                      TextVisibility="NVisibility.Markup"
                      ShowContent="false"
                      Getter="tbl.GetGetter((m, a) => m.Summary)"/>
                <NTag Text="<th>Date</th>"
                      TextVisibility="NVisibility.Markup"
                      ShowContent="false"
                      Getter="tbl.GetGetter((m, a) => m.Date)"
                      />
            </tr>
        </thead>
        <tbody>
            <CascadingValue Value="default(object)">
                @{
                    var i = 0;
                    foreach (var o in Source)
                    {
                    <tr @key="o">
                        @tbl.Tag.RenderChildren(o, i++)
                    </tr>

                    }
                    }
            </CascadingValue>

        </tbody>
    </table>
</NTag>

結束

  • 文中經過 NTag 演示一些組件開發經常使用技術,所以功能略多了些。
  • TArgs 能夠視做 js 組件中的 option.
相關文章
相關標籤/搜索