欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

[Asp.net core 3.1] 通过一个小组件熟悉Blazor服务端组件开发

程序员文章站 2023-04-04 14:23:18
通过一个小组件,熟悉 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.