一, 组件
支撑 Blazor 的是微软的两大成熟技术, Razor 模板和 SignalR, 两者的交汇点就是组件. 通常, 我们从 ComponentBase 派生的类型, 或者创建的. razor 文件, 就可以称作组件. 基于这两大技术, 组件也就具备了两大功能, 1, 生成 html 片段; 2, 维护组件状态. 这里我们来说一下组件最基本的功能, 生成 HTML 片段.
二, RenderTreeBuilder,RenderFragment
我们知道, 浏览器处理 HTML 文档时会将所有的标签都挂到一颗文档树中, 无论一段 HTML 来自哪里, 总会被这棵树安排的明明白白. 换句话说, 如果有根线的话, 我们可以依靠这棵树把所有的标签都串起来, 而在 Blazor 组件中也有这么一根线, 这根线就是 RenderTreeBuilder, 拿这根线的人就是 Blazor 框架.
备注一下: 以下涉及的代码如果没有特别说明, 都是指写在. cs 文件中, 继承 Microsoft.AspNetCore.Components.ComponentBase 的组件类.
下面用代码看看这根线. 新建一个 Blazor 应用 项目, 新增 一个 c# 类, MyComp 继承 Microsoft.AspNetCore.Components.ComponentBase, 然后 override 一下, 找到如下方法:
- protected override void BuildRenderTree(RenderTreeBuilder builder)
- {
- base.BuildRenderTree(builder);// 加断点
- }
加个断点, 在项目的 Pages\Index.razor 里加上一行.<MyComp />
如果不想代码执行两次, 就在 Pages_Host.cshtml 里修改一下 rendermode
@(await HTML.RenderComponentAsync<App>(RenderMode.Server))
F5 跑起来, 虽然没有任何输出, 但是断点命中了, RenderTreeBuilder 这根线确实串起了我们的组件.
现在让我们看看, RenderTreeBuilder 可以做什么.
- protected override void BuildRenderTree(RenderTreeBuilder builder)
- {
- builder.AddMarkupContent(0, "<span> BuildRenderTree 使用 AddMarkupContent 输出 Html .</span>");
- // base.BuildRenderTree(builder);
- }
再次跑起来, 我们发现页面上多了我们加的 span. 也就是说 HTML 的输出, 靠的是调用 RenderTreeBuilder 上的各种方法加上的. 组件的基本原理也就是这样, 一个 RenderTreeBuilder 进入不同组件的 BuildRenderTree 方法, 方法内 通过 RenderTreeBuilder 上的 add.. open.. 方法把我们想要输出的部分, 挂载到 builder 上, 最终输出到浏览器.
接下来, 我们考察一下 BuildRenderTree 方法, 用委托描述一下, 我们发现这就是一个 Action<RenderTreeBuilder>.
在标题里我们提到了 RenderFragment, 查看一下它的定义.
public delegate void RenderFragment(RenderTreeBuilder builder);// 还是一个 Action<RenderTreeBuilder>, 或者说, BuildRenderTree 就是一个 RenderFragment
我们发现和前面的 BuildRenderTree 在签名上一模一样, 既然 blazor 会使用 RenderTreeBuilder 去调用 BuildRenderTree 方法, 那么 RenderFragment 会不会也被调用?
让我们暂时离开组件 MyComp, 转到 Index.razor 内加一段 code
- @code{
- RenderFragment MyRender=(builder) => builder.AddMarkupContent(0, "<span > 当前输出来自: Index.razor 组件, MyRender 字段. </span>");
- }
在之前我们声明 MyComp 组件之后, 再加一行调用 @MyRender.
完整的 Index.razor
- @page "/"
- <MyComp />
- @MyRender
- @code{
- RenderFragment MyRender = (builder) => builder.AddMarkupContent(0, "<div > 当前输出来自: Index.razor 组件, MyRender 字段. </div>");
- }
两段信息, 如愿输出, 证明 blazor 能够识别出模板中的 RenderFragment , 并自动调用.
既然我们在组件模板中 (Index.razor) 书写 RenderFragment , 当然有其他方式可以不用拼凑字符串.
RenderFragment AnotherRender =@<div > 模板写法的 RenderFragment</div>;
加上调用 @AnotherRender, 跑起来, 三段信息.
至此, 我们对 RenderFragment 有了一个大概的了解, 它是一个函数, 内部打包了我们的输出内容. 在模板中我们可以使用,@xxxrender 将其就地展开输出, 在 c# 环境下我们可以通过 xxxrender(builder)的形式进行调用(比如在 BuildRenderTree 方法内调用). 又因为其本身就是一个委托函数, 因此我们即可以在组件内使用, 也可以自由的在组件之间传递, 完成对输出内容及逻辑的复用.
同时, 为了更好的配合 RenderFragment 使用, Blazor 中还提供了一个工厂委托, RenderFragment , 即 Func<TValue,RenderFragment> 用法一般如下
- // 模板中(Index.razor)
- RenderFragment<object> RenderValue =value=> @<div> render value :@value</div>;
调用 @RenderValue (123) 如果在 c# 代码中, 比如在 BuildRenderTree 方法内, RenderValue (123)(builder).
vs 中 *.razor 在编译时会生成对应的. g.cs 代码, 位置在 obj/debug/netcoreapp3.0/ razor 下, 可以多打开看看.
三, RenderFragment 的一些用法
1,HTML 中, 我们可以在一对标签内添加 内容, 比如 <div>123</div>, 组件默认是不支持此类操作的, 这时我们就需要 RenderFragment 来包装标签内的内容.
让我们回到 MyComp 组件类中, 增加一个属性
- [Parameter] public RenderFragment ChildContent{
- get; set;
- }
- Index.razor
- <MyComp><div> 组件标记内部</div></MyComp>
此时直接运行的话, 组件不会输出内部信息, 需要在 BuildRenderTree 中执行一下
- protected override void BuildRenderTree(RenderTreeBuilder builder)
- {
- ChildContent?.Invoke(builder);
- base.BuildRenderTree(builder);
- }
组件标记内的片段被打包进了 ChildContent, 已经变成了独立的一个片段, 因此需要我们显式的调用一下.
ChildContent 是特殊名称
2, 组件上有多个 RenderFragment
- [Parameter] public RenderFragment Fragment1 { get; set; }
- [Parameter] public RenderFragment Fragment2 { get; set; }
此时调用需要调整一下, 不然框架不知道把内容片段打包进哪个属性里
- <MyComp>
- <Fragment1>
- <div> Fragment1 </div>
- </Fragment1>
- <Fragment1>
- <div> Fragment1.1 </div>
- </Fragment1>
- <Fragment2>
- <div> Fragment2 </div>
- </Fragment2>
- </MyComp>
这里故意重复处理了 Fragment1, 可以看看结果.
3, 带参数的 RenderFragmentcode:
[Parameter] public RenderFragment<MyComp> ChildContent { get; set; }
调用及传参
- <MyComp Context="self"> //<ChildContent>
- @self.GetType()
- </MyComp> //</ChildContent>
4, 打开的组件声明标记内部, 除了可以使用 RenderFragment 参数属性外, 其他的 razor 语法基本都支持, 也包括另外一个组件.
比如
- <MyComp>
- <CompA />
- <CompB> ...... </CompB>
- </MyComp>
或者
- <MyComp>
- <Fragment1>
- <CompA />
- </Fragment1>
- <Fragment2>
- <CompB> ...... </CompB>
- </Fragment2>
- </MyComp>
虽然看上去, 声明标记的代码很相似, 但却有着实质上的不同.
当我们使用 标记声明一个参数属性时, 我们是在生成 RenderFragment, 随后将其赋值给对应的属性.
当我们使用标记声明一个组件时, 我们是在构造一个组件实例, 然后调用它, 将组件输出插入到组件所在位置.
参数属性 (RenderFragment ) 属于组件, 是组件的一个属性, 互相关系是明确的类型《=》成员关系.
组件内部的其他组件标记很多时候只是为了复用一些输出片段, 如果不通过代码进行一些处理的话, 是无法明确知道组件之间关系的.
四, CascadingValue/CascadingParameter
组件多起来之后, 组件之间的数据共享和传递以及组件间的关系就会变的很麻烦, 数量少的时候, 还可以使用 @ref 手工指定, 多起来之后 @ref 明显不是一个好方法. 组件 CascadingValue 和对应的特性 [CascadingParameter] 就是为了解决这一问题而出现.
一个 CascadingValue 内的所有组件 包括子级, 只要组件属性上附加了 [CascadingParameter] 特性, 并且值内容可以兼容, 此属性就会被赋值.
比如给组件定义 属性接收 CascadingValue
- [CascadingParameter] public int Value { get; set; }
- [CascadingParameter] public string SValue { get; set; }
- // 修改下输出
- protected override void BuildRenderTree(RenderTreeBuilder builder)
- {
- builder.AddMarkupContent(0, $"<div>CascadingValue: {Value},{SValue} </div>");// 一个 int, 一个 string
- ChildContent?.Invoke(this)(builder);// 加载下级组件
- base.BuildRenderTree(builder);
- }
在 razor 页中
- <CascadingValue Value="123"> //int
- <MyComp>
- <MyComp></MyComp>
- </MyComp>
- </CascadingValue>
执行后我们就会发现, 两个组件都捕获到了 int 值 123.
现在再加一个 CascadingValue
- <CascadingValue Value="123"> //int
- <CascadingValue Value="@("aaaa")"> //string
- <MyComp>
- <MyComp></MyComp>
- </MyComp>
- </CascadingValue>
- </CascadingValue>
分属两个 CascadingValue 的两个不同类型值, 就被每个组件的两个属性捕获到, 方便, 强大而且自身不产生任何 HTML 输出, 因此使用场景非常广泛. 比如官方 Forms 组件中就是借助 CascadingValue/Parameter 完成 model 的设置, 再比如, 组件默认没有处理父子, 包含关系的接口, 这时就可以简单的定义一个 [CascadingParameter] public ComponentBase Parent{get;set;} 专门接收父级组件, 处理类似 Table/Columns 之类的组件关系.
五, 总结
组件是为其自身的 BuildRenderTree 方法 ( RenderFragment )服务的, 组件上的各种属性方法, 都是为了给 RenderFragment 做环境准备, 因此组件实质上是个 RenderFragment 的包装类. 组件系统则通过一个 RenderTreeBuilder 依次调用各组件, 收集输出内容, 最终交给系统内部完成输出.
1,.Razor 文件会被编译为一个组件类(obj/debug/netcore3.0/razor/...)
2, 组件系统创建 RenderTreeBuilder, 将其交给组件实例
3, 组件实例使用 RenderTreeBuilder, 调用自身 BuildRenderTree.
4, 等待组件状态变化, 再次输出.
来源: https://www.cnblogs.com/cerl/p/11834510.html