富贵列车粤语,遵义梦网社区,中国军舰过英吉利海峡
通过一个小组件,熟悉 blazor 服务端组件开发。github
vs2019 16.4, asp.net core 3.1 新建 blazor 应用,选择 asp.net core 3.1。 根文件夹下新增目录 components,放置代码。
components 目录下新建一个接口文件(interface)当作文档,加个 using using microsoft.aspnetcore.components;
。
先从直观的方面入手。
<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:
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,可以考虑下一些特定功能的处理及类型部分。
<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 ]
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>
1、 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>
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>
看一下结果
<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 增加测试代码
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); }
集合的简单处理只需要循环一下。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>
如对本文有疑问,请在下面进行留言讨论,广大热心网友会与你互动!! 点击进行留言回复
Blazor server side 自家的一些开源的, 实用型项目的进度之 CEF客户端
.NET IoC模式依赖反转(DIP)、控制反转(Ioc)、依赖注入(DI)
vue+.netcore可支持业务代码扩展的开发框架 VOL.Vue 2.0版本发布
网友评论