「Asp.net core 3.1」 通过一个小组件熟悉Blazor服务端组件开发
yuyutoo 2024-10-26 16:11 7 浏览 0 评论
通过一个小组件,熟悉 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(#34;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", #34;{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 #34;{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=#34;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.
原文地址:https://www.cnblogs.com/cerl/p/12030355.html
相关推荐
- Python操作Word文档神器:python-docx库从入门到精通
-
Python操作Word文档神器:python-docx库从入门到精通动动小手,点击关注...
- Python 函数调用从入门到精通:超详细定义解析与实战指南 附案例
-
一、函数基础:定义与调用的核心逻辑定义:函数是将重复或相关的代码块封装成可复用的单元,通过函数名和参数实现特定功能。它是Python模块化编程的基础,能提高代码复用性和可读性。定义语法:...
- 等这么长时间Python背记手册终于来了,入门到精通(视频400集)
-
本文毫无套路!真诚分享!前言:无论是学习任何一门语言,基础知识一定要扎实,基础功非常的重要,找一个有丰富编程经验的老师或者师兄带着你会少走很多弯路,你的进步速度也会快很多,无论我们学习的目的是什么,...
- 图解Python编程:从入门到精通系列教程(附全套速查表)
-
引言本系列教程展开讲解Python编程语言,Python是一门开源免费、通用型的脚本编程语言,它上手简单,功能强大,它也是互联网最热门的编程语言之一。Python生态丰富,库(模块)极其丰富,这使...
- Python入门教程(非常详细)从零基础入门到精通,看完这一篇就够
-
本书是Python经典实例解析,采用基于实例的方法编写,每个实例都会解决具体的问题和难题。主要内容有:数字、字符串和元组,语句与语法,函数定义,列表、集、字典,用户输入和输出等内置数据结构,类和对象,...
- Python函数全解析:从入门到精通,一文搞定!
-
1.为什么要用函数?函数的作用:封装代码,提高复用性,减少重复,提高可读性。...
- Python中的单例模式:从入门到精通
-
Python中的单例模式:从入门到精通引言单例模式是一种常用的软件设计模式,它保证了一个类只有一个实例,并提供一个全局访问点。这种模式通常用于那些需要频繁创建和销毁的对象,比如日志对象、线程池、缓存等...
- 【Python王者归来】手把手教你,Python从入门到精通!
-
用800个程序实例、5万行代码手把手教你,Python从入门到精通!...
- Python从零基础入门到精通:一个月就够了
-
如果想从零基础到入门,能够全职学习(自学),那么一个月足够了。...
- Python 从入门到精通:一个月就够了
-
要知道,一个月是一段很长的时间。如果每天坚持用6-7小时来做一件事,你会有意想不到的收获。作为初学者,第一个月的月目标应该是这样的:熟悉基本概念(变量,条件,列表,循环,函数)练习超过30个编...
- Python零基础到精通,这8个入门技巧让你少走弯路,7天速通编程!
-
Python学习就像玩积木,从最基础的块开始,一步步搭建出复杂的作品。我记得刚开始学Python时也是一头雾水,走了不少弯路。现在回头看,其实掌握几个核心概念,就能快速入门这门编程语言。来聊聊怎么用最...
- 神仙级python入门教程(非常详细),从0到精通,从看这篇开始!
-
python入门虽然简单,很多新手依然卡在基础安装阶段,大部分教程对一些基础内容都是一带而过,好多新手朋友,对一些基础知识常常一知半解,需要在网上查询很久。...
- Python类从入门到精通,一篇就够!
-
一、Python类是什么?大家在生活中应该都见过汽车吧,每一辆真实存在、能在路上跑的汽车,都可以看作是一个“对象”。那这些汽车是怎么生产出来的呢?其实,在生产之前,汽车公司都会先设计一个详细的蓝图...
- 学习Python从入门到精通:30天足够了,这才是python基础的天花板
-
当年2w买的全套python教程用不着了,现在送给有缘人,不要钱,一个月教你从入门到精通1、本套视频共487集,本套视频共分4季...
- 30天Python 入门到精通(3天学会python)
-
以下是一个为期30天的Python入门到精通学习课程,专为零基础新手设计。课程从基础语法开始,逐步深入到面向对象编程、数据处理,最后实现运行简单的大语言模型(如基于HuggingFace...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- mybatis plus (70)
- scheduledtask (71)
- css滚动条 (60)
- java学生成绩管理系统 (59)
- 结构体数组 (69)
- databasemetadata (64)
- javastatic (68)
- jsp实用教程 (53)
- fontawesome (57)
- widget开发 (57)
- vb net教程 (62)
- hibernate 教程 (63)
- case语句 (57)
- svn连接 (74)
- directoryindex (69)
- session timeout (58)
- textbox换行 (67)
- extension_dir (64)
- linearlayout (58)
- vba高级教程 (75)
- iframe用法 (58)
- sqlparameter (59)
- trim函数 (59)
- flex布局 (63)
- contextloaderlistener (56)