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

通过一个小组件,熟悉 Blazor 服务端组件开发。github:https://github.com/git-net/NBlazors

一、环境搭建

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>:INTagwhere 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.cspublic 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.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/312704.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【STM32】STM32学习笔记-TIM定时中断(13)

00. 目录 文章目录 00. 目录01. TIM简介02. 定时器类型03. 基本定时器04. 通用定时器05. 高级定时器06. 定时中断基本结构07. 预分频器时序08. 计数器时序09. 计数器无预装时序10. 计数器有预装时序11. RCC时钟树12. 附录 01. TIM简介 TIM&#xff08;Timer&#xff09;定时器…

开源.Net Standard版华为物联网北向接口SDK

说明最近用到了华为的物联网平台API&#xff0c;但是官方没有.Net版的SDK&#xff0c;所以就自己封装了一个&#xff0c;开源出来给有需要的朋友&#xff0c;同时也算是为.Net Core的发展做点小贡献~源码地址&#xff1a;https://github.com/iamoldli/HuaWei.IoT.NorthApi.Sdk同…

你知道怎么使用DebugView查看调试信息吗?

简介 DebugView是sysinternals工具集中的一款用来查看调试信息的工具。不管你是内核开发人员还是应用程序开发人员&#xff0c;都会用到这款神器。先简单看看DebugView可以干什么吧。可以查看应用程序输出的调试信息。可以查看驱动程序输出的调试信息。可以查看本地机器的调试信…

使用ASP.NET Core 3.x 构建 RESTful API - 3.3.3 ProblemDetails

当ASP.NET Core 大约在 2.1 版本的时候&#xff0c;它引入了 ProblemDetails。ProblemDetails是基于 RFC7807 这个规范&#xff0c;目的是让 HTTP 响应可以携带错误的详细信息&#xff0c;而不是只返回一个错误的状态码。 在 ASP.NET Core 2.2的时候&#xff0c;如果Controller…

(译)An introduction to Kubernetes

原文&#xff1a;https://www.jeremyjordan.me/kubernetes/&#xff08;博客园团队推荐的&#xff09;这篇博客文章将对Kubernetes进行介绍&#xff0c;以便您了解该工具背后的动机&#xff0c;含义以及使用方式。在后续文章中&#xff0c;我将讨论如何使用更具体的&#xff08…

WeihanLi.Npoi 支持 ShadowProperty 了

WeihanLi.Npoi 支持 ShadowProperty 了Intro在 EF 里有个 ShadowProperty (阴影属性/影子属性)的概念&#xff0c;你可以通过 FluentAPI 的方式来定义一个不在 .NET model 里定义的属性&#xff0c;只能通过 EF 里的 ChangeTracker 来操作这种属性。在导出 Excel 的时候&#x…

RNN循环神经网络概述

RNN与普通神经网络的区别&#xff1a;能够更好的处理序列的信息 RNN结构图 如图所示&#xff0c;St的值不仅取决于输入X的值&#xff0c;还取决于St-1的值。同时&#xff0c;St-1的值还取决于St-2的值&#xff0c;因此S1,S2,…,St-1的值都与St的值直接或间接相关&#xff0c;…

使用ASP.NET Core 3.x 构建 RESTful API - 3.3.1 HTTP状态码

HTTP状态码会告诉API的消费者以下事情&#xff1a; 请求是否执行成功了 如果请求失败了&#xff0c;那么谁为它负责 HTTP的状态码有很多&#xff0c;但是Web API不一定需要支持所有的状态码。HTTP状态码一共分为5个级别&#xff1a; 1xx&#xff0c;属于信息性的状态码。Web AP…

解决import tensorflow时的报错 Passing (type, 1) or ‘1type‘ as a synonym of type is deprecate

问题 在Pycharm中运行import tensorflow as tf时报错。 解决方案 此时点开报错中的dtypes.py文件&#xff0c;对其进行修改。 从# hard-coding of names.这里开始&#xff0c;修改所有以_np_q开头的代码行&#xff0c;同时也要修改最后一行的np_resource。 # hard-coding of …

解决module ‘tensorflow‘ has no attribute ‘optimizers‘报错

一般出现此类问题的原因是包的更新导致有些用法发生了变化&#xff0c;因此在tensorflow中调用optimizer需要通过tf.keras调用。 将self.opt tf.optimizers.Adam(learning_rateself.lr)中的tf后面加个keras&#xff0c; 变成self.opt tf.keras.optimizers.Adam(learning_rat…

Dapr 运用

前置条件DockerWin10Dapr 部署本文将采用本地部署的方式。安装 Dapr CLI打开 Windows PowerShell 或 cmd &#xff0c;运行以下命令以安装 Dapr CLI&#xff0c;并添加安装路径到系统环境变量中。powershell -Command "iwr -useb https://raw.githubusercontent.com/dapr/…

微软将中止支持 .NET Core 2.2,建议开发者升级至 .NET Core 3.1

Current 版本 .NET Core 2.2 将在 12 月 23 日结束生命周期&#xff0c;开发者应更新到长期支持版本 .NET Core 3.1。.NET Core 2.2 于 2018 年 12 月 4 日发布&#xff0c;作为一个非 LTS 版本(“Current”)&#xff0c;它只在下一个版本发布后的三个月内受支持。.NET Core 3.…

LeetCode贪心 数组拆分I

Given an integer array nums of 2n integers, group these integers into n pairs (a1, b1), (a2, b2), …, (an, bn) such that the sum of min(ai, bi) for all i is maximized. Return the maximized sum. 思路 这道题要使得各最小值相加最小&#xff0c;就要使得的每一组…

使用.NET Core创建Windows服务 - 使用.NET Core工作器方式

原文&#xff1a;Creating Windows Services In .NET Core – Part 3 – The “.NET Core Worker” Way作者&#xff1a;Dotnet Core Tutorials译者&#xff1a;Lamond Lu译文&#xff1a;使用.NET Core创建Windows服务 - 使用.NET Core工作器方式使用.NET Core创建Windows服务…

LeetCode贪心 最长回文串

Given a string s which consists of lowercase or uppercase letters, return the length of the longest palindrome that can be built with those letters. Letters are case sensitive, for example, “Aa” is not considered a palindrome here. 思路 首先学习一个新单…

【译】Visual Studio 2019 中 WPF UWP 的 XAML 开发工具新特性

原文 | Dmitry翻译 | 郑子铭自Visual Studio 2019推出以来&#xff0c;我们为使用WPF或UWP桌面应用程序的XAML开发人员发布了许多新功能。在本周的 Visual Studio 2019 版本 16.4 和 16.5 Preview 1中&#xff0c;我们希望借此机会回顾一下全年的新变化。如果您错过了我们以前的…

numpy创建zeros数组时报错TypeError: Cannot interpret ‘8‘ as a data type

错误代码 xPoint np.zeros(pow(2, k), pow(2, k))改正方法 zeros括号内填数组行列数时&#xff0c;加一对括号。 正确代码 xPoint np.zeros((pow(2, k), pow(2, k)))

我在外包公司做增删改查有前途么?

作者&#xff1a;邹溪源&#xff0c;长沙资深互联网从业者&#xff0c;架构师社区特邀嘉宾&#xff01;起因这是我无意中在筛选简历时&#xff0c;看到一位朋友发布的求职说明中&#xff0c;明确指出&#xff0c;外包勿扰&#xff0c;并给出了他做出这个决定的理由&#xff1a;…

csv.reader读取txt中的文本数据

Python里读取txt文件的方法有很多&#xff0c;但numpy读取字符串比较困难&#xff0c;这时可以考虑使用csv库&#xff0c;读取txt文件中的每一行文本。 代码 data [] with open(*******.txt, rt) as csvfile:reader csv.reader(csvfile, delimiter,)for row in reader:data…

[译]C#8.0中一个使接口更加灵活的新特性-默认接口实现

9月份的时候&#xff0c;微软宣布正式发布C#8.0&#xff0c;作为.NET Core 3.0发行版的一部分。C#8.0的新特性之一就是默认接口实现。在本文中&#xff0c;我们将一起来聊聊默认接口实现。众所周知&#xff0c;对现有应用程序的接口进行更改是一项很危险的操作。如果这个接口又…