通過一個小組件,熟悉 Blazor 服務端組件開發。 "github" 一、環境搭建 vs2019 16.4, asp.net core 3.1 新建 Blazor 應用,選擇 asp.net core 3.1。 根文件夾下新增目錄 Components,放置代碼。 二、組件需求定義 Compone ...
通過一個小組件,熟悉 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.