我是大标题
我学习Blazor的顺序是基于Blazor University,然后实际内容不完全基于它,因为它的例子还是基于.NET Core 3.1做的,距离现在很遥远了。
截至本文撰写的时间,2025年,最新的.NET是.NET9了都,可能10也快了。我发现有些它上面说的例子其实现在都不一定能运行了,我结合Deep Seek和其他几家人工智能辅助的情况下,进行Blazor的学习,基于原来的教程,并补充了一些我好奇的部分,结合人工智能给我的教学,掌握blazor的核心知识。
我是冗长你不爱看的目录
组件与布局
首先什么是布局,就是页面的模板
创建自己的模板需要
- 使用razor语法,@inherits LayoutComponentBase
然后一般的会基于一个已有的布局来做,如果这样的话就需要再写一句
@layout [其他布局]
例如这样
基于MainLayout创建一个叫Admin的布局。
然后如何使用自定义的布局到具体的页面呢
- 大批量的,通过_Imports.razor,在指定文件夹下的所有布局都会按照_Imports.razor来处理,就是这个razor文件放在哪个文件夹下,哪个文件夹下的razor都会按照这个_Imports.razor来渲染
- 小量的,直接就写@layout [你的布局]
如图所示,这样写Admin下面的razor都会按_Import的布局来渲染了
组件如果需要交互的话,需要在最前面加@rendermode InteractiveServer
这里我默认都是用服务器模式的Blazor
单向绑定和双向绑定
单向绑定
单向绑定就是C#->前端的意思
只需要把需要绑定的对象写一个[Parameter]属性,然后修改为public属性,在前端区域就可以使用@符号进行访问了
比如这样
访问HTML元素的DOM事件
对于像是button这类存在DOM的HTML元素,可以通过@onxxxx来访问其DOM事件,之后就可以和C#进行联动了,或者直接通过lambda表达式。某些事件还具备属性,只需要通过@onxxxx:yyyy进行更细致的控制。
联动C#区域代码
原地直接lambda表达式
访问某些事件更细化的属性功能【譬如这里的stopPropagation】
这个的作用就是让事件停止冒泡传递了,就停在button这一级了。
单向绑定的总结[来自AI]
单向绑定,这样写外部初始化这个Razor组件的时候就必须要传入一个参数,然后传入的参数就会投射到前端区域
在Blazor中,组件如何解释我们传递的值取决于我们正在设置的[Parameter]的类型,以及我们是否通过使用@符号明确表达了我们的意图。
关于[Parameter]属性:
在Blazor组件中,当你想要从父组件接收数据时,你会在子组件的代码中定义一个带有[Parameter]特性的属性。这个特性告诉Blazor这是一个可以从外部传递进来的值。
关于值的传递和解释:
当你向组件传递一个简单类型的值(如整数、字符串、布尔值)时,Blazor会根据属性的类型自动进行值的转换和绑定。例如,如果你传递了一个字符串给一个类型为string的[Parameter]属性,Blazor会直接将这个字符串值赋给该属性。
对于复杂类型的值(如对象、委托Func、事件处理程序等),Blazor同样会根据属性的类型进行绑定,但这里可能需要更多的考虑,比如对象的生命周期管理、委托的调用上下文等。
关于@符号的使用:
在Blazor的Razor语法中,@符号通常用于标记代码表达式。当你在HTML标记中嵌入C#代码时,你需要使用@符号来告诉Razor引擎这是一个代码块或表达式。
但是,在传递参数给组件时,@符号的使用并不是直接相关的。更常见的是,你会在父组件的Razor标记中直接使用属性名来传递值,而不需要额外的@符号(除非你是在传递一个代码表达式的结果作为值)。例如:。
有一点需要注意:如果你在传递参数时使用了绑定表达式(例如,绑定到一个组件的状态或父组件的状态),那么你可能需要使用@bind指令或@bind-Value(取决于属性的命名)来实现双向绑定。但即使在这种情况下,@符号也是用于标记绑定表达式本身,而不是用于标记参数属性。总结来说,Blazor如何解释传递给组件的值主要取决于[Parameter]属性的类型以及你传递值的方式(直接值、绑定表达式等)。@符号在Razor语法中用于标记代码表达式,但在传递参数时通常不是直接相关的,除非你在值中使用了代码表达式。
Blazor 在传递参数时,会根据目标参数的类型推断传递的值。
对于布尔、数字等非字符串类型,Blazor 会将字符串字面量推断为相应的表达式。
对于字符串类型,Blazor 会直接传递字符串值,而不会进行推断。
这种推断机制使得代码更简洁,同时确保类型安全。
双向绑定
双向绑定就是实现数据在前端区域和C#区域双向传递
通过@bind和@bind-value来实现
特性 | @bind | @bind-value |
---|---|---|
语法简洁性 | 更简洁,适合大多数场景 | 更显式,适合复杂场景 |
默认绑定属性 | 自动绑定到 value 属性 | 显式绑定到 value 属性 |
默认事件 | 默认绑定到 onchange 事件 | 需要显式指定事件(如 oninput) |
适用场景 | 普通 HTML 元素或简单组件 | 自定义组件或需要显式控制的场景 |
灵活性 | 较低,适合简单绑定 | 较高,适合需要精细控制的场景 |
这里我们看下面这个例子,涵盖了双向绑定的各种情况
简单说就是懒的话,直接@bind,想精细点就@bind-value,毕竟牛逼
组件之间的参数传递
组件之间传递参数,可以通过将需要传递的组件通过[Parameter]暴露出去给父级组件来访问或者通过CascadingParameter来级联传递。
这里重点说CascadingParameter
它有两种模式
- 通过“名字”来让子组件索引自己要的信息
- 通过类型自动检索自己要的信息
先说“名字”索引
父组件想传递两个信息到子组件
传递的时候就这样套娃写,CascadingValue,然后指定传递的变量,然后给要传递的变量取一个“名字”,然后最后子组件就写在最内层就完了【我这里子组件叫CascadingChild】
子组件是这样的
在子组件里头需要用一个变量来接传递进来的参数
写一个属性CascadingParameter,然后指定好取的名字就可以接收到上层传递进来的参数了
基于类型的参数传递
父组件
我这里整了个复杂的类对象来传递,让子组件自动推断,类似于上面用“名字”来索引
子组件
最后再说下按“名字”传递的特殊情况-覆写
框架在按照名字进行索引的时候,会出现级联传递的名字一样的情况,这种情况下就会发生在传递过程中的覆写问题,框架并不禁止这种行为,可以在过程中组件将上一级传递的值修改,然后再往下传递。
父组件
子组件
这里就出现了子组件往更下一级的孙组件传递同样名字的参数,这个时候框架不禁止这种行为,可以自行修改变量的值,再往下传递。
孙组件
Blazor所支持的指令
类别 | 关键字/用法 |
---|---|
控制流 | @if、@else、@switch、@for、@foreach、@while |
代码块 | @{ … } |
表达式 | @变量、@(表达式) |
HTML 辅助方法 | @Html.Raw、@Html.ActionLink、@Html.Partial |
注释 | @* … *@ |
模型和视图数据 | @model、@using、@inherits |
布局和部分视图 | @section、@RenderBody、@RenderSection |
函数和属性 | @functions |
Razor Pages | @page |
异步编程 | @await |
依赖注入 | @inject |
标签助手 | @addTagHelper、 |
URL 和路径 | @Url.Action、@Url.Content |
表单和验证 | @Html.BeginForm、@Html.ValidationSummary、@Html.ValidationMessageFor |
组件渲染 | @(await Html.RenderComponentAsync) |
动态属性 | <div class=“@(isActive ? “active” : “inactive”)”> |
全局指令 | @namespace、@attribute |
转义字符 | @@ |
自定义指令 | 通过自定义 Razor 引擎或标签助手实现 |
控制流语句
@if、@else、@else if:条件判断。
@switch、@case、@default:多条件分支。
@for、@foreach、@while:循环语句。
代码块
@{ … }:定义多行C#代码块。
表达式
@变量:直接输出变量。
@(表达式):输出表达式的结果。
HTML辅助方法
@Html.Raw:输出未编码的HTML。
@Html.ActionLink:生成超链接。
@Html.Partial、@Html.RenderPartial:渲染部分视图。
注释
@* … *@:Razor注释。
模型和视图数据
@model:定义视图的强类型模型。
@using:引入命名空间。
@inherits:指定视图继承的基类。
布局和部分视图
@section:定义布局中的占位符内容。
@RenderBody():在布局页面中渲染主体内容。
@RenderSection:在布局页面中渲染特定部分。
函数和属性
@functions:定义视图中的函数或属性。
Razor Pages
@page:定义Razor页面的路由。
三元运算符
@(条件 ? “True” : “False”):条件化输出。
Lambda表达式
@{ Func<int, string> 函数名 = (参数) => “返回值”; }:定义和使用Lambda表达式。
其他
@await:用于异步操作。
Blazor的属性与属性展开Attribute Splatting
属性这个和前端部分联系比较紧密。
对于HTML组件来说,他们一般会有一些“键-值对”构成了属性描述
譬如举一个例子,可能某一个按钮有这些属性
属性名 | 值 |
---|---|
class | btn btn-primary |
style | color:red; |
disable | true |
data-custom | 123 |
按照既有的理解,开发人员就直接在前端部分写HTML标签写这些玩意进去了。但是在Blazor框架下,这个可以通过C#的字典来定义,然后传递给前端HTML部分,这个就叫做属性了。就很方便可以动态在C#代码区修改属性
譬如我定义一个键值对属性字典
我把它给一个按钮附上
这样渲染的时候,框架就会把我希望的属性渲染给这个按钮
就是通过这个@attributes 来实现
这里就会引申出另一个问题,如果我HTML对象就本身存在了一些既有的键值对属性了,怎么处理呢,就要引出CaptureUnmatchedValues了。
来自AI的解读
如何捕获了未在组件中显式定义的属性CaptureUnmatchedValues
譬如我这里有一个组件CaptureUnmatchedValues
通过CaptureUnmatchedValues属性,配合一个键值对字典变量来接收外部传递进来的“属性”,接收没有显式定义的属性
外部我是这样写的
我在外部既有已经写了一个字体大小的属性,和内部显式定义的属性是重复的,这种时候就会按内部显式定义的属性来渲染,虽然这个组件也接收了来自外部的属性。
如果我把显式定义删掉
这样写,运行的效果这个按钮的字体大小就是10px了
来自AI的总结
Blazor的变量生命期
这里我让AI总结了下微软文档中关于【ASP.NET Core Razor组件生命周期】这篇文档的内容
就是每个组件自带的这些方法的执行顺序要小心,有很多坑
这部分就是理论性比较强,就没有去具体做些例子来试了,只能说是日后遇到用到的情况具体问题具体分析了。
Blazor的多线程与InvokeAsync
我先问了问AI,Blazor里的多线程和普通C#的多线程的区别
Blazor的多线程
- UI线程的限制:
在Blazor中,特别是Blazor WebAssembly应用程序中,所有的UI更新和事件处理都是在浏览器的UI线程上执行的。这意味着,如果开发者在Blazor中启动了一个异步操作(如使用Task.Run或await Task.Delay等),并且这个操作需要在UI线程上继续执行(如更新UI元素),那么就需要确保这个操作在完成后能够正确地返回到UI线程上。
为了实现这一点,Blazor通常会使用ConfigureAwait(true)来确保异步操作在完成后能够返回到捕获的同步上下文(即UI线程)上。
然而,这也可能导致性能问题,因为频繁的上下文切换会增加额外的开销。 - 线程安全性的考虑:
在Blazor中,由于UI线程的限制,开发者需要更加谨慎地处理线程安全问题。例如,当多个线程尝试同时访问或修改共享资源时,就需要使用适当的同步机制(如锁、信号量等)来避免竞态条件和死锁等问题。此外,由于Blazor运行在浏览器中,因此还需要考虑跨线程通信的问题。例如,如果需要在非UI线程上执行某些操作,并且这些操作的结果需要更新到UI上,那么就需要使用适当的方法(如通过消息传递或事件机制)来将结果传递回UI线程。 - Blazor Server的特殊性:
值得注意的是,Blazor Server的运行环境与Blazor WebAssembly有所不同。
在Blazor Server中,应用程序实际上是在服务器上运行的,而UI更新则是通过SignalR连接实时回传给客户端的。这意味着在Blazor Server中,多线程的使用可能更加复杂。
因为开发者需要处理服务器和客户端之间的通信、状态同步等问题。同时,由于服务器上的代码可以访问完整的.NET Core运行时支持,因此也可以使用更高级的多线程和并行编程技术来提高性能。
普通C#程序
C#提供了多种实现多线程的方式,如使用Thread类、ThreadPool类、Task类以及Parallel类等。这些类和方法允许开发者创建和控制线程,以实现并行执行多个任务。在普通C#程序中,多线程的访问和操作通常不会受到特定的框架或运行环境的限制。开发者可以自由地在线程之间共享数据、进行同步和通信等操作。然而,这也需要开发者自行处理线程安全问题,如避免竞态条件、死锁等问题。
这里先通过两种不同的初始化-同步初始化vs异步初始化,来体验下初探下多线程,然后再说一说InvokeAsync。
同步初始化
这里重写了OnInitialized方法,里面获取当前线程的管理线程ID。
然后外层父组件是通过一个for循环调用了5次这个组件
运行的效果就是这5个组件均会输出同一个管理线程的ID
然后我再问了AI关于这个的理解。
输出结果取决于以下因素:
情况 1:纯同步调用
如果父组件自身是完全同步的(例如没有 async 方法或 await):
所有子组件的 OnInitialized 会由同一个线程连续执行
输出的 IdThread 相同
这是因为同步代码会阻塞当前线程,直到所有子组件初始化完成。
情况 2:异步父组件
如果父组件包含异步逻辑(如 OnInitializedAsync):
可能在初始化过程中发生线程切换
子组件的 IdThread 可能不同(但未必一定会不同)
异步初始化
做一个异步初始化的代码,试一下,外层也是类似的for循环调用5次
出现了一些不同的线程ID了
输出的线程 ID 大概率会不同,因为异步操作可能导致线程切换。
ConfigureAwait(continueOnCapturedContext: bool)方法
这里再引申出另一个有关系的知识点-ConfigureAwait
实际上在异步初始化这里,我们的初始化是可以设置一个参数来控制线程控制权后面的归属的。
就是这个ConfigureAwait(continueOnCapturedContext: bool)方法。
用于指定在等待异步操作完成后,是否应该尝试将控制权返回给捕获的同步上下文(如果存在的话)。在这个上下文中,“捕获的同步上下文”通常指的是最初启动异步操作的上下文,比如ASP.NET Core的请求上下文或Blazor的UI线程
设置为true | 设置为false |
---|---|
框架默认的行为是true,await 操作完成后,控制权将尝试返回给捕获的同步上下文。在Blazor中,这意味着如果异步操作是在UI线程上启动的,那么后续的操作也会尝试在UI线程上执行,以确保对UI元素的访问是线程安全的 | await 操作完成后,控制权不会返回给捕获的同步上下文,而是继续在当前可用的线程池线程上执行。这可以提高性能,因为它避免了不必要的上下文切换,但你必须小心确保不在错误的线程上访问UI元素 |
推荐的做法,保证UI访问的线程安全 | 不涉及UI的后台操作,使用false可以提高性能和响应 |
1. 需要操作 UI 组件(如更新 @currentCount)2. 访问 HttpContext(在 ASP.NET Core 中)3.使用 Blazor 的 JS 互操作(IJSRuntime.InvokeAsync) | 1. 通用类库代码(不依赖具体上下文)2. 纯后台任务(如日志记录、数据处理)3. 长时间运行的 CPU 密集型操作(避免阻塞 UI 线程) |
这里我们做一个测试
打印异步之前的线程ID,和不同设置下异步之后的线程ID
当使用await关键字时,默认情况下,它会捕获当前的同步上下文(在Blazor Server中,这通常是ASP.NET Core的同步上下文),并在异步操作完成后尝试回到这个上下文。这是为了确保像UI更新这样的操作能在正确的上下文中执行。在异步操作完成后,应该尝试回到原来的同步上下文。在Blazor Server中,这意味着回到处理该SignalR消息的线程或与之相关的线程。由于线程池的工作方式,这个“原来的线程”可能并不是实际开始执行异步操作的那个线程。因此,你看到的线程ID不同,是因为在await之后,代码可能是在线程池中的另一个线程上执行的,但这个线程被调度回来执行后续的代码,以确保它运行在正确的同步上下文中。所以线程ID会不一样。
如果是false的情况下
await之后它去到另一个线程ID了,没有回到之前的线程ID
InvokeAsync
先听听AI怎么说:
由于Blazor使用单线程的渲染模型,这意味着UI更新通常在主线程上执行。
这种设计简化了开发,但也可能导致性能问题,特别是在处理复杂或耗时的操作时。
某些场景我们会希望再后台线程处理一些操作,以避免阻塞UI线程,这时候就需要上多线程了。
这个InvokeAsync方法就是Blazor提供的一个用于在UI线程上执行代码,当你在后台线程操作时,如果要更新UI,就必须用InvokeAsync将更新操作调度到UI线程上执行,以避免线程冲突,直白点就是把涉及UI的更新部分更新给UI。
通过几个例子来看看
第1个例子
这个例子大概就是做了一个按钮,当单击按钮之前,任务状态显示时未开始,之后单击按钮,显示任务进行中,然后开始异步延时了。延时完了之后就通过InvokeAsync执行更新UI的显示,然后调用框架通知重写渲染UI。
单击之后,就组件自己更新,其他的东西不受影响
时间到了,完成异步操作,然后UI更新
第2个例子
这个例子就复杂一些,更新的是一个复杂的列表数据。原理其实和第一个差不多。就是这个复杂点。
单击之前
单击之后,数据就刷到列表里渲染出来了。
第3个例子
这个例子相比前面两个又要复杂些了。这个是一个定时刷新UI,显示实时时间的例子。
通过重写OnInitialized方法,初始化了一个定时器,并且实现Dispose接口,处理定时器在组件释放的时候的释放问题,一定要做释放,不然内存会有泄露的风险。定时器回调那里写法比较独特,我们来听听AI的说法:
_ = 是一种常见的写法,用于明确表示我们忽略这个返回值。_ 是一个合法的变量名,通常用于表示“我不关心这个值”。所以,_ = InvokeAsync(…) 的意思是:调用 InvokeAsync,但不关心它的返回值。将返回值赋值给 _,表示我们忽略它。
在 C# 中,如果一个方法返回 Task,而你直接调用它而不使用 await 或赋值给某个变量,编译器会发出警告,提示你“这个异步操作没有被等待”。
例如,如果你直接写:
InvokeAsync(() =>
{
currentTime = newTime;
StateHasChanged();
});
编译器会警告:
由于此调用未被等待,因此在调用完成之前将继续执行当前方法。请考虑将 await 运算符应用于调用结果。
为了避免这个警告,我们可以使用 _ = 来明确表示我们忽略返回值。
第4个例子
最后一个例子是一个并行任务的例子。每一个任务是均是一个单独异步任务,点击运行之后会等待所有任务都完成之后才算执行完。点击运行之后,最终的效果是任务123都显示完成。
AI替大家总结下:
Blazor的单线程渲染模型简化了开发,但在处理复杂操作时可能需要使用多线程。
InvokeAsync方法允许开发者在后台线程中执行操作,并将UI更新安全地调度到UI线程上执行。
通过合理使用InvokeAsync,可以提升Blazor应用的性能和响应性。
Blazor的虚拟DOM功能
AI先给大家说一说概念性的东西
虚拟 DOM 是一种在内存中表示 HTML 页面结构的技术。它是一个轻量级的 JavaScript 对象(或其他语言的对象),用来描述页面上应该渲染的 HTML 元素及其结构
它的作用是当页面需要更新的时候,框架不会直接操作DOM,而是操作内存里面的虚拟DOM,然后框架比较新旧DOM的差异,然后更新差异部分,而不是整个页面全部重新渲染一次.
这样做的好处就是可以减少直接操作真实DOM的次数,提高性能,提高UI的响应
在Blazor里面,它是通过BuildRenderTree来实现的,但是在目前的NET9版本里,实际上是不推荐用户自己去重写BuildRenderTree这个方法的,框架推荐用户通过Razor语言来实现动态渲染组件或者是通过RenderFragment来做
RenderFragment
这个按我的理解就是通过C#的代码来写DOM操作的语句
这里我通过几个例子来说明下
例子1-基础语法
基础写法就是这样写的,譬如我想写一个div,然后它的内容是hello world,我就要先写div,然后"hello world",然后结束。
在前端区域,如果要使用它,只需要@这个变量就完了,非常方便
例子2-内联前端写法
简单说就是C#内联直接写对应的前端代码
效果和上面一样
例子3-支持泛型参数的写法
这个区别于前面的写法,就是强制指定了参数的类型,然后调用的时候要这样写
例子4-支持属性的写法
有时候我们需要属性,譬如style,这个时候我们就可以通过这个AddAttribute加上对应的属性
这里引申出一个特殊的组件写法
例子5-动态代码定义样式的组件写法
这里我们以列表为例,一个泛型的动态列表,列表项的样式是动态的,可以通过外部代码来定义列表中元素的样式,内容。
这种要泛型的组件,需要写一句@typeparam XXXX,表示参数存在泛型,待会在父级需要告诉组件,对应的泛型是啥。
然后由于我们这个例子是一个列表,所以传入的对象需要是一个实现了IEnumerable的东西.然后在列表遍历的时候,需要传入对应的泛型参数给到ItemTemplate,由它来指定渲染的效果。
然后在父级要这样调用
告诉组件,泛型的类型,Items对应的对象是啥,然后呈现的样式模板是啥
然后实际运行起来的效果就是:
例子6-动态代码定义样式的组件写法-进阶版本
反正都可以指定传入参数的类型,就不局限于普通类型,也可以是复杂类型
这里我给了一个类变量
然后对应的也给了数据和模板
完了之后,原理其实和上面例子5是类似的,效果如下:
例子7-Tab页面的例子
这个例子是Blazor University的例子,比较绕,就是基于这个RenderFragment的复杂例子了。
就是一个这样的控件,当你点击某个Tab按钮之后,它会切换到另一个“页面”上,但是其他页面上的组件不会受到影响.
这个组件是由两个部分组成的:
- TabControl—[父组件]
- TabPage—[子组件]
调用者
<TabControl>
<TabTextTemplate>
<img src="/images/tab.png" />
</TabTextTemplate>
<ChildContent>
@* 当 TabControl 渲染每个 Tab 按钮时,会调用 @TabTextTemplate(tab),将当前 tab 对象传递给模板,并渲染 <img src="/images/tab.png" /> *@
<TabPage Text="Tab 1">
<h1>The first tab</h1>
</TabPage>
<TabPage Text="Tab 2">
<h1>The second tab</h1>
</TabPage>
<TabPage Text="Tab 3">
<h1>The third tab</h1>
</TabPage>
</ChildContent>
</TabControl>
TabControl
@rendermode InteractiveServer
@* 使用 CascadingValue 组件将 TabControl 自身(或 Tabs 集合)传递给子组件 *@
@* this 应该就是TabControl的引用*@
<CascadingValue Value="this">
<div class="btn-group" role="group">
@foreach (TabPage tab in Pages)
{
<button type="button" class="btn @GetButtonClass(tab)"
@onclick=@( ()=>ActivatePage(tab) )>
@tab.Text
@if (TabTextTemplate != null)
{
@* 如果提供了TabTextTemplate,则使用模板来渲染按钮内容,否则直接使用TabPage的Text属性 *@
@* 调用模板并传入当前 TabPage 对象作为参数,然后渲染模板定义的内容 *@
@*tab 是 TabControl 组件中 @foreach 循环的当前 TabPage 对象*@
@* TabTextTemplate(tab) 会将 tab 作为参数传递给模板,并返回一个 RenderFragment,最终渲染出模板定义的内容。 *@
@TabTextTemplate(tab)
}
else
{
@tab.Text
}
</button>
}
</div>
@ChildContent
</CascadingValue>
@code {
/// <summary>
/// 自动捕获所有嵌套的子组件
/// </summary>
[Parameter]
public RenderFragment ChildContent{ get; set; }
//它是一个模板,允许用户自定义每个 Tab 按钮的显示内容
//表示一个可以接收参数(这里是 TabPage 对象)并返回渲染内容的模板
//在 index.razor 中,TabTextTemplate 被定义为一个 <img> 标签
[Parameter]
public RenderFragment<TabPage> TabTextTemplate { get; set; }
public TabPage ActivePage{ get; set; }
List<TabPage> Pages = new List<TabPage>();
internal void AddPage(TabPage page)
{
Pages.Add(page);
if(Pages.Count == 1)
{
ActivePage = page;
}
StateHasChanged();
}
string GetButtonClass(TabPage page)
{
return page == ActivePage ? "btn-primary" : "btn-secondary";
}
void ActivatePage(TabPage page)
{
ActivePage = page;
}
}
在父组件这层,对外需要传入的参数有:
- RenderFragment ChildContent
- RenderFragment TabTextTemplate
这个TabTextTemplate是用来自定义Tab按钮显示内容的,譬如我们在外层调用者那里传入了img标签对应的图片
这个ChildContent是用于捕获嵌套在TabControl中的所有子组件,譬如我们在外层调用者里面写的那三个TabPage
其他变量的作用:
Pages用于TabControl所有包含的TabPage
ActivatePage用来得到当前活动的TabPage
AddPage,用于将TabPage添加到Pages列表里,然后添加第一个TabPage时将其设置为活动页面。
前端部分:
: 使用CascadingValue将TabControl实例传递给所有子组件(即TabPage组件),以便子组件可以访问父组件的属性和方法。这里的this应该就是指代this指针的意思.
@foreach (TabPage tab in Pages): 遍历所有的TabPage,并为每个TabPage生成一个按钮。
: 每个按钮对应一个TabPage,点击按钮时会调用ActivatePage方法激活对应的TabPage。GetButtonClass方法用来判定当前Page是不是激活的哪个,然后决定是不是要修改button所属的class.
@if (TabTextTemplate != null): 如果提供了TabTextTemplate,则使用模板来渲染按钮内容,否则直接使用TabPage的Text属性。
@ChildContent: 渲染TabControl的内容部分,即所有的TabPage。
我在补充下我不太理解的
TabPage
@rendermode InteractiveServer
@* 如果当前对应的父组件是当前子组件,就呈现出定制Tab页面的内容 *@
@if(Parent.ActivePage == this)
{
@ChildContent
}
@code {
/// <summary>
/// 接收自父组件传递的引用,传入进来的值是对应父组件的引用
/// </summary>
[CascadingParameter]
public TabControl Parent{ get; set; }
/// <summary>
/// 用于定制Tab页的内容,对应在顶层调用里填的那段HTML
/// </summary>
[Parameter]
public RenderFragment ChildContent{ get; set; }
[Parameter]
public string Text { get; set; }
/// <summary>
/// 初始化组件的时候
/// </summary>
/// <exception cref="ArgumentNullException"></exception>
protected override void OnInitialized()
{
if(Parent == null)
{
throw new ArgumentNullException(nameof(Parent), "TabPage must exist within a TabControl");
}
base.OnInitialized();
//父组件添加当前Page作为子页面
Parent.AddPage(this);
}
}
Parent: 通过级联参数传递得到父组件变量的引用
@if(Parent.ActivePage == this): 只有在当前TabPage是活动页面时,才会渲染ChildContent。
@ChildContent: 渲染TabPage的内容部分。
Text: 用于设置Tab按钮的文本。
OnInitialized: 在组件初始化时,将当前TabPage添加到父组件TabControl的Pages列表中。
顶层调用者,定义了TabControl和多个TabPage的结构
TabControl.razor 是管理多个TabPage的组件,负责渲染Tab按钮并控制哪个TabPage是活动的。
TabPage.razor 是单个选项卡页面的组件,只有在它处于活动状态时才会显示其内容。
这里我还想再补充下,关于RenderFragment的理解
在 TabControl 组件的代码中,有一个 TabTextTemplate 参数,类型为 RenderFragment
public RenderFragment TabTextTemplate { get; set; }
这表示 TabTextTemplate 是一个可以接受 TabPage 对象作为输入,并返回 UI 内容的模板。
Blazor 会自动将 标签内的内容转换为一个 RenderFragment 的委托
怎么理解这个委托呢
RenderFragment 的定义
它是一个 泛型委托,可以接受一个类型为 T 的参数,并返回要渲染的 UI 内容。用代码表示就是:
// 伪代码简化表示
public delegate RenderFragment RenderFragment(T input);
输入:一个 T 类型的参数(比如 TabPage 对象)
输出:RenderFragment(一段 UI 内容)
实际Blazor框架会将对应到例子上的写法转换上下面这样的写法
TabTextTemplate = (TabPage context) => __builder =>
{
<img src="/images/tab.png" />
};
传入了TabPage这个变量,到HTML标签中,但是实际上没有用到
RenderFragment 就像「一个能生成 UI 的模板工厂」
想象你有一个 制作汉堡的模具(模板),这个模具需要你 提供食材(参数 T),然后它会 自动压出特定形状的汉堡(生成 UI)。RenderFragment 就是这个模具,而 T 就是你给它的食材类型。
为什么需要这种设计?
关注点分离:业务逻辑(数据)和 UI 表现分离
复用性:同一套数据可以用不同的模板渲染
动态性:根据运行时数据决定如何渲染
类型安全:编译时检查模板中使用的属性是否存在
这种模式在需要高度定制 UI 的组件(如表格、列表、导航菜单)中非常常见,它完美结合了 C# 的强类型特性和 HTML 的声明式语法。
Blazor的@key关键字
Key关键字是用来让列表类的数据出现"唯一性"的一个关键字,因为某些场景下,我们是希望列表中的数据存在唯一性的,处理的时候也是。
这里我们看两个例子
例子1-泛型模板列表
调用者
<DataList TItem="Person" Data=@People>
<ItemTemplate>
<li @key=context>
@context.Name @context.Age @context.Sex
</li>
</ItemTemplate>
</DataList>
DataList是一个泛型列表的组件,然后它会渲染出Person类对象列表中的信息.
注意这里出现了@key关键字,这里的context就代指People类对象列表中的对象.然后列表项展示Name,Age和Sex
DataList
@typeparam TItem
@if (ListTemplate == null)
{
<ul>
@* ?? Array.Empty<TItem>() 避免data是null的时候造成影响*@
@foreach (TItem item in Data ?? Array.Empty<TItem>())
{
@ItemTemplate(item)
}
</ul>
}
else
{
@* @: 看作是一个“转义符号”,它告诉 Blazor:“接下来的内容不要解析为 Razor 代码 *@
@ListTemplate(
// 在你的代码中,@: 的作用是确保 @{ ... } 中的内容被正确传递给 ListTemplate,而不会被误解析为 Razor 代码
@:@{
foreach (TItem item in Data ?? Array.Empty<TItem>())
{
@ItemTemplate(item)
}
}
)
}
@code {
[Parameter]
public IEnumerable<TItem> Data{ get; set; }
[Parameter]
public RenderFragment<TItem> ItemTemplate{ get; set; }
[Parameter]
//这表示 ListTemplate 是一个接受 RenderFragment 类型参数的模板
public RenderFragment<RenderFragment> ListTemplate{ get; set; }
}
和前面所说的泛型组件类似,也是有一个@typeparam TItem来代指泛型。
[Parameter] public IEnumerable Data { get; set; }:这是一个参数属性,用于接收要展示的数据集合。它必须是IEnumerable类型,即任何实现了IEnumerable接口的集合类型,其中T是数据项的类型。
[Parameter] public RenderFragment ItemTemplate { get; set; }:这是一个渲染片段参数,用于定义如何渲染每个数据项。RenderFragment是一个委托类型,它接受一个TItem类型的参数并返回一个RenderFragment,后者定义了如何渲染UI。
[Parameter] public RenderFragment ListTemplate { get; set; }:这是另一个渲染片段参数,但它更加灵活。它接受一个RenderFragment类型的参数(这里实际上是接受一个代码块),这个RenderFragment本身又可以包含更多的渲染逻辑。这允许开发者完全自定义整个列表的渲染方式,包括列表的HTML结构。
比较精妙的逻辑在其渲染的那部分
组件内部首先检查ListTemplate是否为null:
如果ListTemplate为null,则使用默认的<ul>标签来渲染数据项列表。数据项通过foreach循环遍历Data集合(如果Data为null,则使用Array.Empty<TItem>()来避免空引用异常),并对每个数据项应用ItemTemplate渲染片段。
如果ListTemplate不为null,则使用ListTemplate来渲染列表。这里使用了@:指令来“转义”接下来的代码块,确保它不会被解析为Razor代码,而是作为一个完整的RenderFragment传递给ListTemplate。在ListTemplate内部,再次遍历Data集合并对每个数据项应用ItemTemplate渲染片段。
这里再说说转义:
在Blazor中,@: 符号被用作一个“转义”符号,它的作用是告诉Blazor编译器接下来的内容不应该被当作Razor代码来解析。这在一些特定的场景下非常有用,尤其是当你想要将一个代码块或者一段文本直接传递给一个组件或者渲染片段(RenderFragment)时。
ListTemplate是一个接受RenderFragment类型参数的模板。这里的嵌套RenderFragment结构可能有点令人困惑,但基本思想是ListTemplate可以定义一个自定义的列表渲染逻辑,这个逻辑内部又可以包含另一个渲染片段,用于渲染列表项
直接在ListTemplate内部写Razor代码会遇到一个问题:Blazor编译器会尝试解析这些代码作为Razor标记,而不是将它们作为一个整体传递给ListTemplate。这就是@:符号发挥作用的地方
当你使用@:时,你实际上是在说:“接下来的这部分内容应该被视为一个普通的文本块或者代码块,不要尝试解析其中的Razor语法。
其实这样写就比较难读懂其实,最好还是用一个局部函数来写,比较容易看得懂,原地写lambda表达式总觉得怪怪的
@code {
private RenderFragment GetListItemsRenderFragment()
{
return builder =>
{
foreach (var item in Data ?? Array.Empty<TItem>())
{
builder.AddContent(0, ItemTemplate(item));
}
};
}
}
<!-- 在ListTemplate中使用这个函数 -->
@ListTemplate(GetListItemsRenderFragment())
然后回到key关键字,它的作用最主要的还是当列表有变化的时候,框架可以正确的管理好每一个列表中的列表项.
例子2
<DataList Data=@People TItem="Person">
<ListTemplate Context="listofPeople" >
<table boarder=1 cellpadding=4>
<thead>
<tr>
<th>
Name
</th>
<th>
Age
</th>
<th>
Sex
</th>
</tr>
</thead>
<tbody>@listofPeople</tbody>
</table>
</ListTemplate>
@* Context这里是来指定传入的参数的名称的不是变量的名称 *@
@* ItemTemplate 定义了每个 Person 对象的渲染方式 *@
<ItemTemplate Context="person">
@* 在 ItemTemplate 内部,可以通过 @person 访问当前的 Person 对象。 *@
<tr @key=@person>
@* key 是 Blazor 中的一个优化机制,用于标识列表项的唯一性 *@
@* 这里,@person 作为 key,确保每个 Person 对象的渲染是独立的。 *@
<td>@person.Name</td>
<td>@person.Age</td>
<td>@person.Sex</td>
</tr>
</ItemTemplate>
</DataList>
其实和上面那个例子差不多,但是有点不一样的是这里出现了Context属性,这个是用来指定传入参数的名称而不是对应数据变量的名称,就当是一个别名,渲染出来的效果是下面这样
Blazor的路由功能
路由的意思就是允许用户通过URL访问不同的页面或者组件
路由是通过@page指令来实现的
路由是通过URL实现的,URL就可以向页面传递参数。
这个参数可以设定为可选传入,或者必须传入,还可以限制传入参数类型,也可以让一个页面支持多种不同的参数类型传入。
具体写法
@page "/counter"
普通的路由
@page "/user/{UserId}"
包含一个必须要填的参数 UserId
@page "/user/{UserId:int?}
包含一个可选要填的整型数参数 UserId
@page "/user/{UserId?}/{Action?}"
包含多个可选参数的路由
@page "/user/{UserId:int}
包含一个必选要填的整型数参数 UserId
包含多个路由
@page "/user"
@page "/user/{UserId}"
SupplyParameterFromQuery功能,通过设置一个别名来做查询
@page "/search"
<h3>QueryPage</h3>
<p>Query:@Query</p>
@code {
[Parameter]
[SupplyParameterFromQuery(Name ="q")]
public string Query{ get; set; }
}
实际运行效果就是这样
Blazor 支持多种内置的路由约束,例如:
int:参数必须是整数。
bool:参数必须是布尔值(true 或 false)。
datetime:参数必须是日期时间格式。
guid:参数必须是 GUID 格式。
long:参数必须是长整型。
decimal:参数必须是十进制数。
float:参数必须是浮点数。
double:参数必须是双精度浮点数。
抛开内置的约束,自己还可以自定义约束,就是实现RouteConstraint接口
譬如,自定义一个限定偶数的约束
namespace LearnBlazor.Components.LearnRouteConstraint
{
/// <summary>
/// 自定义路由约束-限定偶数[注意,虽然是偶数,但是传入的还是字符串]
/// </summary>
public class EvenNumberRouteConstraint : IRouteConstraint
{
public bool Match(HttpContext? httpContext, IRouter? route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection)
{
if(values.TryGetValue(routeKey,out var value) &&
value is string stringValue)
{
if (int.TryParse(stringValue, out int intValue))
{
return intValue % 2 == 0;
}
}
return false;
}
}
}
写完之后,需要在Program.cs注册下自定义的约束到框架里
//添加自定义路由约束
builder.Services.AddRouting(options =>
{
options.ConstraintMap.Add("even", typeof(EvenNumberRouteConstraint));
});
注册完就可以用了,就直接打注册时录入的名字,譬如这里是even
用的时候
@page “/user/{Number:even}”
如何把界面添加到导航里
路由弄好了之后,如何让框架知道有这个页面,通过NavLink组件来实现。
NavLink 是 Blazor 中用于创建导航链接的组件,类似于 HTML 的 标签。
它的特殊之处在于可以根据当前 URL 自动为匹配的链接添加一个 CSS 类(默认是 active),以便用户知道当前所在的页面。
这里就要引出一个匹配的问题,就是URL有可能有相似的地方,是完全匹配还是怎么样
NavLinkMatch 属性的作用:
NavLinkMatch 是一个枚举类型,用于控制 NavLink 如何匹配当前 URL。
它有两个可选值:
- NavLinkMatch.All:要求 URL 完全匹配 href 属性值。
- NavLinkMatch.Prefix(默认值):只要当前 URL 以 href 属性值开头,就会匹配。
NavLinkMatch.All 的使用场景:
当需要精确匹配 URL 时使用。例如,如果 href 是 /counter,则只有当 URL 正好是 /counter 时,链接才会被激活
<NavLink href="/counter" Match="NavLinkMatch.All">Counter</NavLink>
NavLinkMatch.Prefix 的使用场景
这是默认行为,适用于大多数情况。例如,如果 href 是 /,则任何以 / 开头的 URL(如 /、/home、/about)都会激活该链接
<NavLink href="/" Match="NavLinkMatch.Prefix">Home</NavLink>
这里需要往深引申出~
嵌套路由
NavLinkMatch.Prefix 的行为看起来可能会导致多个链接同时被激活,但实际上它的设计是为了解决一些特定的导航场景,尤其是嵌套路由或分层路由的情况.
一个父页面可能包含多个子页面,使用 NavLinkMatch.Prefix 可以确保父页面的导航链接在子页面中仍然保持激活状态。
假设你有一个应用,结构如下:
/:主页
/dashboard:仪表盘
/dashboard/profile:用户资料
/dashboard/settings:设置
你希望当用户访问 /dashboard/profile 或 /dashboard/settings 时,/dashboard 的导航链接仍然保持激活状态,以表明用户当前位于“仪表盘”部分。
<NavLink href="/dashboard" Match="NavLinkMatch.Prefix">Dashboard</NavLink>
当用户访问 /dashboard 时,链接激活。
当用户访问 /dashboard/profile 或 /dashboard/settings 时,链接仍然激活
就是前一级是激活的,后面的就也按激活处理了
默认路由
假设你有一个默认路由 /,你希望它在任何子路由中都保持激活状态
<NavLink href="/" Match="NavLinkMatch.Prefix">Home</NavLink>
当用户访问 /、/about 或 /contact 时,Home 链接都会激活。
总的来说,路由通常都是分层涉及的
在合理的路由设计中,父路由和子路由是分层的,不会冲突。
确保路由之间没有重叠的前缀。
例如,避免设计类似 /dashboard 和 /dashboard-settings 的路由
/dashboard 和 /dashboard/profile 是父子关系,不会与其他路由(如 /about)冲突。
NavLinkMatch.Prefix 通常用于父级导航:
父级导航链接使用 NavLinkMatch.Prefix,而子级导航链接使用 NavLinkMatch.All,这样可以避免冲突。
对于子页面或独立页面,使用 NavLinkMatch.All 来确保精确匹配
CSS 样式的控制:
即使多个链接激活,也可以通过 CSS 样式来控制视觉效果,确保用户只关注当前的主要导航。
NavLinkMatch.Prefix 的主要意义在于支持嵌套路由和分层导航。它的设计并不是为了让多个链接同时激活,而是为了让父级导航在子页面中仍然保持激活状态,从而提供更好的用户体验。
如果你不希望多个链接同时激活,可以通过合理设计路由、使用 NavLinkMatch.All 或自定义逻辑来实现。
激活是指当前导航链接与浏览器 URL 匹配时,链接被标记为“激活状态”。 激活状态通过 CSS 类(如 active)体现,为用户提供视觉反馈。 在 Blazor 中,NavLink组件会自动处理激活状态,支持精确匹配(NavLinkMatch.All)和前缀匹配(NavLinkMatch.Prefix)。 你可以通过ActiveClass 属性自定义激活状态的 CSS 类名。
激活的作用 视觉反馈: 当用户点击一个导航链接或通过其他方式导航到某个页面时,激活状态会为当前链接添加一个 CSS 类(如active),从而改变链接的样式(例如高亮显示)。 这为用户提供了清晰的视觉反馈,帮助他们知道当前所在的页面。 提升用户体验:激活状态可以帮助用户快速识别当前页面的位置,尤其是在复杂的导航菜单中。