「Asp.net core 3.1」通过一个小组件熟悉Blazor服务端组件开发
通过一个小组件,熟悉 Blazor 服务端组件开发。github
一、环境搭建
vs2021 16.4, asp.net core 3.1 新建 Blazor 应用,选择 asp.net core 3.1。 根文件夹下新增目录 Components,放置代码。
二、组件需求定义
Components 目录下新建一个接口文件(interface)当作文档,加个 using using
Microsoft.AspNetCoreponents;。
先从直观的方面入手。
类似 html 标签对的组件,样子类似其他标签或内容...或。接口名: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.AspNetCoreponents.IComponent.IComponent Parent { get; set; }.要能添加子控件,void AddChild(IComponent child);,有加就有减,void RemoveChild(IComponent child);。提供一个集合方便遍历,我们已经提供了 Add/Remove,让它只读就好。 IEnumerable Children { get;}。一旦有了 Children 集合,我们就需要考虑什么时候从集合里移除组件,让 IHierarchyComponent 实现 IDisposable,保证组件被释放时解开父子/层级关系。组件需要处理样式,仅有 Class 和 Style 可能不够,通常还会需要 Skin、Theme 处理,增加一个接口记录一下, public interface ITheme{ string GetClass(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 Children { get;}
void AddChild(IComponent child);
void RemoveChild(IComponent child);
}
ITheme
public interface ITheme
{
string GetClass(TComponent component);
}
组件的基本信息 INTag 有了,需要的话可以支持层级关系 IHierarchyComponent,可以考虑下一些特定功能的处理及类型部分。
Blazor 组件实现类似 ....这种可打开的标签对,需要提供一个 RenderFragment 或 RenderFragment属性。RenderFragment 是一个委托函数,带参的明显更灵活些,但是参数类型不好确定,不好确定的类型用泛型。再加一个接口,INTag< TArgs >:INTag, 一个属性 RenderFragment ChildContent { get; set; }.组件的主要目的是为了呈现我们的数据,也就是一般说的 xxxModel,Data....,类型不确定,那就加一个泛型。INTag< TArgs ,TModel>:INTagderFragment 是一个函数,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>
{
///
/// 标签对之间的内容, 为参数,ChildContent 为Blazor约定名。
///
RenderFragment ChildContent { get; set; }
}
回顾一下我们的几个接口。
INTag:描述了组件的基本信息,即组件的样子。IHierarchyComponent 提供了层级处理能力,属于组件的扩展能力。ITheme 提供了 Theme 接入能力,也属于组件的扩展能力。INTag<TTag, TArgs, TModel> 提供了打开组件的能力,ChildContent 像一个动态模板一样,让我们可以在声明组件时自行决定组件的部分内容和结构。所有这些接口最主要的目的其实是为了产生一个合适的 TArgs, 去调用 ChildContent。有描述,有能力还有了主要目的,我们就可以去实现 NTag 组件。三、组件实现抽象基类 AbstractNTag
Components 目录下新增 一个 c#类,AbstractNTag.cs, using
Microsoft.AspNetCoreponents; 借助 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 _children=new List();
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 _children=new List();
IComponent _parent;
public string TagName=> typeof(TTag).Name;
[Inject]public IJSRuntime JSRuntime { get; set; }
[Parameter]public RenderFragment 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 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(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 语法中直接赋值,;Parameter(CaptureUnmatchedValues=true) 支持声明时将组件上没定义的属性打包赋值;CascadingParameter 配合 Blazor 内置组件 ... ...,捕获 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,RenderArgs<NTag,TModel>,TModel>
{
[Parameter]public TModel Model { get; set; }
public RenderArgs<NTag, TModel> Args(object arg=null)
{
return new RenderArgs<NTag, 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,RenderArgs<NTag,TModel>,TModel>//保持和NTag.razor.cs一致
@if (this.ChildContent==null)
{
//默认输出,用于测试
}
else
{
@this.ChildContent(this.Args());
}
@code {
}
简单测试一下, 数据就用项目模板自带的 Data 打开项目根目录,找到_Imports.razor,把 using 加进去
@using xxxx.Data
@using xxxxponents
新增 Razor 组件【Test.razor】
未打开的NTag,输出NTag.ToString():
打开的NTag:
@code{
WeatherForecast TestData=new WeatherForecast { TemperatureC=222, Summary="aaa" };
}
转到 Pages/Index.razor, 增加一行,F5 。
应用级联参数 CascadingValue/CascadingParameter
我们的组件中 Theme 和 Parent 被标记为【CascadingParameter】,因此需要通过 CascadingValue 把值传递过来。
首先,修改一下测试组件,使用嵌套 NTag,描述一个树结构,Model 值指定为树的 Level。
1、 Theme:Theme 的特点是共享,无论组件在什么位置,都应该共享同一个 Theme。这类场景,只需要简单的在组件外套一个 CascadingValue。
F5 跑起来,结果大致如下:
root.Parent:
2、Parent:Parent 和 Theme 不同,我们希望他和我们组件的声明结构保持一致,这就需要我们在每个 NTag 内部增加一个 CascadingValue,直接写在 Test 组件里过于啰嗦了,让我们调整一下 NTag 代码。打开 NTag.razor,修改一下,Test.razor 不动。
@if (this.ChildContent==null)
{
//默认输出,用于测试
}
else
{
@this.ChildContent(this.Args());
}
看一下结果
root.Parent:
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> args)
{
return this.ChildContent?.Invoke(args) ;
}
public RenderFragment RenderContent(object arg=null)
{
return thisderContent(this.Args(arg));
}
NTag.razor
@RenderText()
@if (thisContent)
{
var render=RenderContent();
if (render==null)
{
//测试用
}
else
{
@render//render 是个函数,使用@才能输出,如果不考虑测试代码,可以直接 @RenderContent()
}
}
Test.razor 增加测试代码
7、呈现Model
value:@@arg.Tag.Getter(arg.Model,null)
Text中使用Markup:value:@@((DateTime)arg.Tag.Getter(arg.Model, null))
也可以直接使用childcontent:value:@@arg.Model.Date
getter 格式化:@@((m,a)=>m.Date.ToString("yyyy-MM-dd"))
m.Date.ToString("yyyy-MM-dd"))" Context="arg">
使用customAttributes ,借助外部方法推断TModel类型
@code {
WeatherForecast TestData=new WeatherForecast { TemperatureC=222, Date=DateTime.Now, Summary="test summary" };
Func<T, object, object> GetGetter(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> Args(object arg=null)
{
return new RenderArgs<NTag, TModel>(this, this.Model, arg,this.Getter);
}集合,Table 行列
集合的简单处理只需要循环一下。Test.razor
@foreach (var o in this.Datas)
{
- @arg.Value
}
@code {
IEnumerable Datas=Enumerable.Range(0, 10)
.Select(i=> new WeatherForecast { Summary=i + "" });
}
复杂一点的时候,比如 Table,就需要使用列。
列有 header:可以使用 NTag.Text;列要有单元格模板:NTag.ChildContent;行就是所有列模板的呈现集合,行数据即是集合数据源的一项。具体到 table 上,thead 定义列,tbody 生成行。
新增一个组件用于测试:TestTable.razor,试着用 NTag 呈现一个 table。
#"
TextVisibility="NVisibility.Markup"
ShowContent="false"
TModel="WeatherForecast"
Getter="(m, a)=>a"
Context="arg">
@arg.Value
Summary"
TextVisibility="NVisibility.Markup"
ShowContent="false"
TModel="WeatherForecast"
Getter="(m, a)=> m.Summary"
Context="arg">
@arg.Value
Date"
TextVisibility="NVisibility.Markup"
ShowContent="false"
TModel="WeatherForecast"
Getter="(m, a)=> m.Date"
Context="arg">
@arg.Value
@{ var cols=tbl.Tag.Children;
var i=0;
tbl.Tag.ConsoleLog(cols.Count());
}
@foreach (var o in Source)
{
@foreach (var col in cols)
{
if (col is NTag tag)
{
@tagderContent(tag.Args(o,i ))
}
}
i++;
}
@code {
IEnumerable 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,单元格@arg.Value。下面试着简化一些。
之前测试 Model 呈现的代码中我们说到可以 “借助外部方法推断 TModel 类型”,当时使用了一个 GetGetter 方法,让我们试着在 RenderArg 中增加一个类似方法。
RenderArgs.cs:
public Func<TModel, object, object> GetGetter(Func<TModel, object, object> func)=> func;GetGetter 极简单,不需要任何逻辑,直接返回参数。原理是 RenderArgs 可用时,TModel 必然是确定的。
用法:
#"
TextVisibility="NVisibility.Markup"
ShowContent="false"
Getter="(m, a)=>a"
Context="arg">
@arg.Value
作为列的 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>();
NTag defaultTag=null;
foreach (var child in children)
{
if (defaultTag==null && child.ChildContent !=null) defaultTag=child;
var render=(child.ChildContent==null ? defaultTag : child);
renderderContent(child.Args(model, arg))(builder);
}
};
}
TestTable.razor
#"
TextVisibility="NVisibility.Markup"
ShowContent="false"
Getter="tbl.GetGetter((m,a)=>a)"
Context="arg">
@arg.Value
Summary"
TextVisibility="NVisibility.Markup"
ShowContent="false"
Getter="tbl.GetGetter((m, a)=> m.Summary)"/>
Date"
TextVisibility="NVisibility.Markup"
ShowContent="false"
Getter="tbl.GetGetter((m, a)=> m.Date)"
/>
@{
var i=0;
foreach (var o in Source)
{
@tbl.TagderChildren(o, i++)
}
}
结束文中通过 NTag 演示一些组件开发常用技术,因此功能略多了些。TArgs 可以视作 js 组件中的 option.

浙公网安备 33010602011771号