通过一个小组件,熟悉 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.