[Asp.net core 3.1] 通過一個小元件熟悉Blazor服務端元件開發
通過一個小元件,熟悉 Blazor 服務端元件開發。github
一、環境搭建
vs2019 16.4, asp.net core 3.1 新建 Blazor 應用,選擇 asp.net core 3.1。 根資料夾下新增目錄 Components,放置程式碼。
二、元件需求定義
Components 目錄下新建一個介面檔案(interface)當作文件,加個 using using Microsoft.AspNetCore.Components;
。
先從直觀的方面入手。
- 類似 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:
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);
}
ITheme
public interface ITheme
{
string GetClass<TComponent>(TComponent component);
}
元件的基本資訊 INTag 有了,需要的話可以支援層級關係 IHierarchyComponent,可以考慮下一些特定功能的處理及型別部分。
- 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 ]
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 元件。
三、元件實現
抽象基類 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>
1、 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>
2、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 增加測試程式碼
7、呈現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.