我是大标题
我学习Blazor的顺序是基于Blazor University,然后实际内容不完全基于它,因为它的例子还是基于.NET Core 3.1做的,距离现在很遥远了。
截至本文撰写的时间,2025年,最新的.NET是.NET9了都,可能10也快了。我发现有些它上面说的例子其实现在都不一定能运行了,我结合Deep Seek和其他几家人工智能辅助的情况下,进行Blazor的学习,基于原来的教程,并补充了一些我好奇的部分,结合人工智能给我的教学,掌握blazor的核心知识。
我是冗长你不爱看的目录
- 我是大标题
- 组件与布局
- 单向绑定和双向绑定
- 单向绑定
- 访问HTML元素的DOM事件
- 双向绑定
- 组件之间的参数传递
- Blazor所支持的指令
- Blazor的属性与属性展开Attribute Splatting
- 如何捕获了未在组件中显式定义的属性CaptureUnmatchedValues
- Blazor的变量生命期
- Blazor的多线程与InvokeAsync
- 同步初始化
- 异步初始化
- ConfigureAwait(continueOnCapturedContext: bool)方法
- InvokeAsync
- Blazor的虚拟DOM功能
- Blazor的@key关键字
- Blazor的RenderFragments模板化
- Blazor的路由功能
- Blazor的表单功能
- Blazor的组件库
- Blazor下的.NET和JS互操作
- Blazor下的DI依赖注入
组件与布局
首先什么是布局,就是页面的模板
创建自己的模板需要
- 使用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