通过一个小组件,熟悉 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:
root Theme:Theme[blue]
t1.Parent:
t1 Theme:Theme[blue]
t1_1.Parent:
t1_1 Theme:Theme[blue]
t1_1_1.Parent:
t1_1_1 Theme:Theme[blue]
t1_1_2.Parent:
t1_1_2 Theme:Theme[blue]

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:
root Theme:Theme[blue]
t1.Parent:NTag`1[root,0]
t1 Theme:Theme[blue]
t1_1.Parent:NTag`1[t1,1]
t1_1 Theme:Theme[blue]
t1_1_1.Parent:NTag`1[t1_1,2]
t1_1_1 Theme:Theme[blue]
t1_1_2.Parent:NTag`1[t1_1,2]
t1_1_2 Theme:Theme[blue]
  • 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.

[Asp.net core 3.1] 通过一个小组件熟悉Blazor服务端组件开发的更多相关文章

  1. 006.Adding a controller to a ASP.NET Core MVC app with Visual Studio -- 【在asp.net core mvc 中添加一个控制器】

    Adding a controller to a ASP.NET Core MVC app with Visual Studio 在asp.net core mvc 中添加一个控制器 2017-2-2 ...

  2. 008.Adding a model to an ASP.NET Core MVC app --【在 asp.net core mvc 中添加一个model (模型)】

    Adding a model to an ASP.NET Core MVC app在 asp.net core mvc 中添加一个model (模型)2017-3-30 8 分钟阅读时长 本文内容1. ...

  3. socket小程序写一个客户端,实现给服务端发送hello World字符串,将客户端发送的数据变成大写后返回

    写一个客户端,实现给服务端发送hello World字符串,将客户端发送的数据变成大写后返回 本机id是192.168.xx.xy 服务端 import socket soc = socket.soc ...

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

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

  5. ASP.NET Core中如何针对一个使用HttpClient对象的类编写单元测试

    原文地址: How to unit test a class that consumes an HttpClient with IHttpClientFactory in ASP.NET Core? ...

  6. ASP.NET CORE使用MailKit的一个故障点分析

    ASP.NET CORE下有需要发邮件的需求,但是原来framework下的 system.net.mail,没有实现smtpclient的功能(当时看是没有,说是准备并入.net core来着),所 ...

  7. ASP.NET Core ActionFilter引发的一个EF异常

    最近在使用ASP.NET Core的时候出现了一个奇怪的问题.在一个Controller上使用了一个ActionFilter之后经常出现EF报错. InvalidOperationException: ...

  8. ASP.NET Core与Redis搭建一个简易分布式缓存

    ​本文主要介绍了缓存的概念,以及如何在服务器内存中存储内容.今天的目标是利用IDistributedCache来做一些分布式缓存,这样我们就可以横向扩展我们的web应用程序. 在本教程中,我将使用Re ...

  9. 使用SignalR ASP.NET Core来简单实现一个后台实时推送数据给Echarts展示图表的功能

    什么是 SignalR ASP.NET Core ASP.NET Core SignalR 是一种开放源代码库,可简化将实时 web 功能添加到应用程序的功能. 实时 web 功能使服务器端代码可以立 ...

随机推荐

  1. HtmlSpanner 使用小结 -- 安卓解析html

    如何利用 HtmlSpanner解析 HTML格式 的字符串: 1. GitHub 下载HtmlSpanner项目 https://github.com/NightWhistler/HtmlSpann ...

  2. 【Leetcode 做题学算法周刊】第三期

    首发于微信公众号<前端成长记>,写于 2019.11.13 背景 本文记录刷题过程中的整个思考过程,以供参考.主要内容涵盖: 题目分析设想 编写代码验证 查阅他人解法 思考总结 目录 35 ...

  3. Uber Go 语言编码规范

    Uber Go 语言编码规范 Uber 是一家美国硅谷的科技公司,也是 Go 语言的早期 adopter.其开源了很多 golang 项目,诸如被 Gopher 圈熟知的 zap.jaeger 等.2 ...

  4. Havok Physics 2012(1)

    目录 Chapter 1. Introduction 1. What is a Physics Engine? Chapter 1. Introduction ​ 欢迎来到Havok Physics ...

  5. saprk性能调优参考

    1.Tuning Spark 文档 原文:http://spark.apache.org/docs/latest/tuning.html 翻译参考:https://www.cnblogs.com/lh ...

  6. 领扣(LeetCode)独特的电子邮箱地址 个人题解

    每封电子邮件都由一个本地名称和一个域名组成,以 @ 符号分隔. 例如,在 alice@leetcode.com中, alice 是本地名称,而 leetcode.com 是域名. 除了小写字母,这些电 ...

  7. C语言|博客作业03

    这个作业属于哪个课程 C程序语言设计 这个作业要求在哪里 https://edu.cnblogs.com/campus/zswxy/CST2019-1/homework/8654 我在这个课程的目标是 ...

  8. ef+Npoi导出百万行excel之踩坑记

            最近在做一个需求是导出较大的excel,本文是记录我在做需求过程中遇到的几个问题和解题方法,给大家分享一下,一来可以帮助同样遇到问题的朋友,二呢,各位大神也许有更好的方法可以指点小弟一 ...

  9. Python第五天 列表练习 元组类型 字典类型 小购物车练习

    # 反转 reverse# l=['lili','asdf','qwer','80000']# l.reverse()# print(l) # ['80000', 'qwer', 'asdf', 'l ...

  10. NPM 源的管理器nrm

    作为一个 NPM 源管理器,nrm允许快速地在如下 NPM 源间切换: 列表项目 npm cnpm strongloop enropean australia nodejitsu taobao Ins ...