經過一個小組件,熟悉 Blazor 服務端組件開發。githubcss
vs2019 16.4, asp.net core 3.1 新建 Blazor 應用,選擇 asp.net core 3.1。 根文件夾下新增目錄 Components,放置代碼。html
Components 目錄下新建一個接口文件(interface)看成文檔,加個 using using Microsoft.AspNetCore.Components;
。git
先從直觀的方面入手。github
<xxx propA="aaa" data-propB="123" ...>其餘標籤或內容...</xxx>
或<xxx .../>
。接口名:INTag.string TagId{get;set;} string TagName{get;set;}
.string Class{get;set;} string Style{get;set;}
。IDictionary<string,object> CustomAttributes { get; set; }
using Microsoft.JSInterop;
屬性 IJSRuntime JSRuntime{get;set;}
。考慮一下功能方面。面試
IComponent Parent { get; set; }
.void AddChild(IComponent child);
,有加就有減,void RemoveChild(IComponent child);
。IEnumerable<IComponent> Children { get;}
。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
<xxx>....</xxx>
這種可打開的標籤對,須要提供一個 RenderFragment 或 RenderFragment<TArgs>
屬性。RenderFragment 是一個委託函數,帶參的明顯更靈活些,可是參數類型很差肯定,很差肯定的類型用泛型。再加一個接口,INTag< TArgs >:INTag
, 一個屬性 RenderFragment<TArgs> ChildContent { get; set; }
.INTag< TArgs ,TModel>:INTag
.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; } }
回顧一下咱們的幾個接口。
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; } }
Parameter(CaptureUnmatchedValues =true)
支持聲明時將組件上沒定義的屬性打包賦值;CascadingParameter
配合 Blazor 內置組件 <CascadingValue Value="xxx" >... <NTag /> ...</CascadingValue>
,捕獲 Value。處理過程和級聯樣式表(css)很相似。泛型其實就是定義在類型上的函數,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 基類時須要考慮各個泛型參數應該是什麼:
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; } }
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 。
咱們的組件中 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 跑起來,結果大體以下:
<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>
看一下結果
<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>
到目前爲止,咱們的 NTag 主要在處理一些基本功能,好比隱式的父子關係、子內容 ChildContent、參數、泛型。。接下來咱們考慮如何把一個 Model 呈現出來。
對於常見的 Model 對象來講,呈現 Model 其實就是把 Model 上的屬性、字段。。。這些成員信息呈現出來,所以咱們須要給 NTag 增長一點能力。
調整下 NTag 代碼,增長一個類型爲 Func<TModel,TArg,object> 的 Getter 屬性,打上【Parameter】標記。
[Parameter]public Func<TModel,object,object> Getter { get; set; }
[Parameter] public string Text { get; set; }
一個小枚舉
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); }
集合的簡單處理只須要循環一下。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,就須要使用列。
新增一個組件用於測試: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 }); }
tbl.Tag.Children
會爲空。<td>@arg.Value</td>
。下面試着簡化一些。以前測試 Model 呈現的代碼中咱們說到能夠 「藉助外部方法推斷 TModel 類型」,當時使用了一個 GetGetter 方法,讓咱們試着在 RenderArg 中增長一個相似方法。
RenderArgs.cs:
public Func<TModel, object, object> GetGetter(Func<TModel, object, object> func) => func;
用法:
<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>