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

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

  1. ASP.NET Core 3.x 学习笔记(7)——Blazor

    ASP.NET Core 3.x 学习笔记(7)--Blazor ASP.NET Core 3.x 学习笔记(7)--Blazor 编程模式对比 Blazor 客户端宿主模型 Mono 服务器端宿主模 ...

  2. 使用ASP.NET Core、Ocelot、MongoDB和JWT的微服务

    目录 介绍 开发环境 技术 体系结构 源代码 微服务 API网关 客户端应用 单元测试 使用健康检查进行监视 如何运行应用程序 如何部署应用程序 进一步阅读 本文显示了一个使用ASP.NET Core ...

  3. 一个简单的完成端口(服务端/客户端)类

    一个简单的完成端口(服务端/客户端)类 作者:spinoza 翻译:麦子芽儿, POWERCPP(后面部分内容) 下载源代码 原文网址:http://www.codeproject.com/KB/IP ...

  4. 校园网跑腿小程序源码 服务端+客户端+小程序

    介绍: 校园网跑腿小程序源码 需要准备 1.小程序 2.服务器(推荐配置2h4g3m) 3.域名(需要备案) 搭建教程 使用服务器搭建宝塔 安装pm2管理器 新建项目上传服务器接口 修改/pub/co ...

  5. 使用docker部署一个直接可用的puppet服务端

    思路: 在一个docker环境,直接拉下来笔者的镜像,直接启动一个可用的容器即可. 此镜像提供一个直接可用的 puppet服务端(foreman/activemq/mcollective-client ...

  6. java服务器向客户端发消息_java一个简单的客户端向服务端发送消息

    java一个简单的客户端向服务端发送消息 客户端代码: package com.chenghu.tcpip; import java.io.IOException; import java.io.Ou ...

  7. 微信小程序(PHP服务端)之仿淘票票,制作电影购票程序

    微信小程序(PHP服务端)之仿淘票票,制作购票程序 前言 一.业务流程 二.效果图 总结 前言 这学期对PHP进行了学习,就编程而言,和常用的java开发思路都大同小异,但是PHP的部署是真的方便,这 ...

  8. python批量下载文件只有1kb_详解如何用python实现一个简单下载器的服务端和客户端...

    话不多说,先看代码: 客户端: import socket def main(): #creat: download_client=socket.socket(socket.AF_INET,socke ...

  9. ASP.NET Core 模型验证的一个小小坑

    今天在我们的一个项目中遇到一个 asp.net core 模型验证(model validation)的小问题.当模型属性的类型是 bool ,而提交上来的该属性值是 null ,asp.net co ...

最新文章

  1. 2018湖北计算机准考证打印,2018年3月湖北计算机等级考试准考证打印入口
  2. 网站核心关键词一定要控制在五个之内更方便集中优化
  3. 001-SDK框架之Unity游戏调用SDK
  4. 部署Dotnet Core应用到Kubernetes(二)
  5. 离线配置xml的文档类型定义文件(xml语法规则) dtd
  6. 【李宏毅2020 ML/DL】P97-98 More about Meta Learning
  7. kotlin枚举_Kotlin枚举班
  8. 《我也能做CTO之程序员职业规划》之二:做CTO的苹果定律
  9. TOMCAT下载及配置
  10. 蔡高厅高等数学18-函数在一点处的连续、函数在区间内的连续、两类间断点的判断
  11. RHadoop的技术性文章
  12. 2011最犀利语录大全
  13. Neural Turing Machines-NTM系列
  14. android换肤的实现方案,Android 换肤的思路
  15. linux suse11 sp3安装,SUSE Linux Enterprise Server 11 SP3安装教程详解
  16. html之响应式(自适应)网页设计
  17. 苹果7p最佳系统版本_告别虚拟机和双系统,移动硬盘+Win To Go,苹果笔记本的最佳选择...
  18. Unity | 动画那些事儿
  19. 什么软件可以把真人照片卡通化、动漫化?
  20. Teamviewer过期,获取免费版

热门文章

  1. Google:推荐几款好用的Chrome浏览器插件
  2. 计算机英语课程背景,专家讲座第十五讲:信息化背景下高质量大学英语课程建设与教学设计...
  3. c语言编手机蓝牙软件的代码,51单片机C语言的简易蓝牙锁代码
  4. 使用LiveClick升级您的实时书签
  5. mysql查询优化以及面试小结
  6. JavaScript校验网址
  7. 丢失日志文件的风险与对策
  8. ASP.NET MVC 上传大文件时404
  9. SmartDraw_2012_Enterprise_R20.0.1.0的安装使用
  10. 系统安全防护之UNIX下***检测方法