大家好,我是码农刚子。上一章介绍了Blazor的简介,开发工具及环境,基本语法和一些示例。接下来我们继续了解Blazor 组件相关的基础知识,希望对你有所帮助。
1、组件生命周期
1.简介
Blazor的生命周期与React组件的生命周期类似,也分为三个阶段:初始化阶段、运行中阶段和销毁阶段,其相关方法有10个,包括设置参数前、初始化、设置参数之后、组件渲染后以及组件的销毁,但是这些方法有些是重复的,只不过是同步与异步的区别。
2.图解
首先将结果图呈现,代码位于第3部分:

Blazor生命周期方法主要包括:
|---|----------|--------------------------------------|
| 1 | 设置参数前 | SetParametersAsync |
| 2 | 初始化 | OnInitialized/OnInitializedAsync |
| 3 | 设置参数后 | OnParametersSet/OnParametersSetAsync |
| 4 | 组件渲染呈现后 | OnAfterRender/OnAfterRenderAsync |
| 5 | 判断是否渲染组件 | ShouldRender |
| 6 | 组件删除前 | Dispose |
| 7 | 通知组件渲染 | StateHasChanged |
在所有生命周期函数中,有以下需要注意的点:
(1)前5种方法的声明都是virtual,除SetParametersAsync为public外,其他的都是protected。
(2)OnAfterRender/OnAfterRenderAsync方法有一个bool类型的形参firstRender,用于指示是否是第一次渲染(即组件初始化时的渲染)。
(3)同步方法总是先于异步方法执行。
(4)Dispose函数需要通过使用@implements指令实现IDisposable接口来实现。
(5)StateHasChanged无法被重写,可以被显示调用,以便强制实现组件刷新(如果ShouldRender返回true,并且Blazor认为需要刷新);当组件状态更改时不必显示调用此函数,也可导致组件的重新渲染(如果ShouldRender返回true),因为其已经在ComponentBase内部的处理过程(第一次初始化设置参数时、设置参数后和DOM事件处理等)中被调用。
3.代码示例
设置参数时 (SetParametersAsync 设置由组件的父组件在呈现树或路由参数中提供的参数。
每次调用 ParameterView 时,方法的 参数都包含该组件的SetParametersAsync值集。 通过重写 SetParametersAsync 方法,C#代码可以直接与 ParameterView 参数交互。
@page "/set-params-async/{Param?}"
<PageTitle>Set Parameters Async</PageTitle>
<h1>Set Parameters Async Example</h1>
<p>@message</p>
@code {
private string message = "Not set";
[Parameter]
public string? Param { get; set; }
public override async Task SetParametersAsync(ParameterView parameters)
{
if (parameters.TryGetValue<string>(nameof(Param), out var value))
{
if (value is null)
{
message = "The value of 'Param' is null.";
}
else
{
message = $"The value of 'Param' is {value}.";
}
}
await base.SetParametersAsync(parameters);
}
}
组件初始化 (OnInitialized 和 OnInitializedAsync 专门用于在组件实例的整个生命周期内初始化组件。 参数值和参数值更改不应影响在这些方法中执行的初始化。 例如,将静态选项加载到下拉列表中,该下拉列表在组件的生命周期内不会更改,也不依赖于参数值,这是在这些生命周期方法之一中执行的操作。 如果参数值或参数值更改会影响组件状态,请改为使用 OnParametersSet{Async}。
组件在接收 SetParametersAsync 中的初始参数后初始化,此时,将调用这些方法。
如果使用同步父组件初始化,则保证父组件初始化在子组件初始化之前完成。 如果使用异步父组件初始化,则无法确定父组件和子组件初始化的完成顺序,因为它取决于正在运行的初始化代码。
对于同步操作,重写 OnInitialized:
@page "/on-init"
<PageTitle>On Initialized</PageTitle>
<h1>On Initialized Example</h1>
<p>@message</p>
@code {
private string? message;
protected override void OnInitialized() =>
message = $"Initialized at {DateTime.Now}";
}
若要执行异步操作,请替代 OnInitializedAsync 并使用 await 运算符:
protected override async Task OnInitializedAsync()
{
//await ...
await Task.Delay(2000); //2秒之后
message = $"Initialized at {DateTime.Now} after 2 second delay";
}
如果自定义基类与自定义初始化逻辑一起使用,需在基类上调用 OnInitializedAsync:
protected override async Task OnInitializedAsync()
{
await ...
await base.OnInitializedAsync();
}
设置参数之后 (OnParametersSet 或 OnParametersSetAsync 在以下情况下调用:
- 在 OnInitialized 或 OnInitializedAsync 中初始化组件后。
- 当父组件重新呈现并提供以下内容时:
- 至少一个参数已更改时的已知或基元不可变类型。
- 复杂类型的参数。 框架无法知道复杂类型参数的值是否在内部发生了改变,因此,如果存在一个或多个复杂类型的参数,框架始终将参数集视为已更改。
在组件路由中,不能同时对DateTime参数使用datetime路由约束,并将该参数设为可选。 因此,以下 OnParamsSet 组件使用两个 @page 指令来处理具有和没有 URL 中提供的日期段的路由。
@page "/on-params-set"
@page "/on-params-set/{StartDate:datetime}"
<PageTitle>On Parameters Set</PageTitle>
<h1>On Parameters Set Example</h1>
<p>
Pass a datetime in the URI of the browser's address bar.
For example, add <code>/1-1-2024</code> to the address.
</p>
<p>@message</p>
@code {
private string? message;
[Parameter]
public DateTime StartDate { get; set; }
protected override void OnParametersSet()
{
if (StartDate == default)
{
StartDate = DateTime.Now;
message = $"No start date in URL. Default value applied " +
$"(StartDate: {StartDate}).";
}
else
{
message = $"The start date in the URL was used " +
$"(StartDate: {StartDate}).";
}
}
}
应用参数和属性值时,异步操作必须在 OnParametersSetAsync 生命周期事件期间发生:
protected override async Task OnParametersSetAsync()
{
await ...
}
如果自定义基类与自定义初始化逻辑一起使用,需在基类上调用 OnParametersSetAsync:
protected override async Task OnParametersSetAsync()
{
await ...
await base.OnParametersSetAsync();
}
组件呈现之后 (OnAfterRender 和 OnAfterRenderAsync 在组件以交互方式呈现并且 UI 完成更新之后被调用(例如,元素添加到浏览器 DOM 之后)。 此时会填充元素和组件引用。 在此阶段中,可使用呈现的内容执行其他初始化步骤,例如与呈现的 DOM 元素交互的 JS 互操作调用。
这些方法不会在预呈现或静态服务器端渲染(静态 SSR)期间在服务器上调用,因为这些进程未附加到实时浏览器 DOM,并且已在 DOM 更新之前完成。
对于 OnAfterRenderAsync,组件在任何返回 Task 的操作完成后不会自动重渲染,以避免无限渲染循环。
firstRender 和 OnAfterRender 的 OnAfterRenderAsync 参数:
-
在第一次呈现组件实例时设置为 true。
-
可用于确保初始化操作仅执行一次。
@page "/after-render"
@inject ILogger<AfterRender> Logger<PageTitle>After Render</PageTitle>
After Render Example
<button @onclick="HandleClick">Log information (and trigger a render)</button>
Study logged messages in the console.
@code {
protected override void OnAfterRender(bool firstRender) =>
Logger.LogInformation("firstRender = {FirstRender}", firstRender);private void HandleClick() => Logger.LogInformation("HandleClick called");
}
加载页面并选择按钮时,AfterRender.razor 示例向控制台输出以下内容:

在渲染后立即进行的异步工作必须在 OnAfterRenderAsync 生命周期事件期间发生:
protected override async Task OnAfterRenderAsync(bool firstRender)
{
...
}
如果自定义基类与自定义初始化逻辑一起使用,需在基类上调用 OnAfterRenderAsync:
protected override async Task OnAfterRenderAsync(bool firstRender)
{
...
await base.OnAfterRenderAsync(firstRender);
}
基类生命周期方法
重写 Blazor 的生命周期方法时,无需为 ComponentBase 调用基类生命周期方法。 但在以下情况下,组件应调用重写的基类生命周期方法:
- 重写 ComponentBase.SetParametersAsync 时,通常会调用 await base.SetParametersAsync(parameters);, 因为基类方法会调用其他生命周期方法并以复杂的方式触发渲染。 有关详细信息,请参阅设置参数时 (SetParametersAsync) 部分。
- 如果基类方法包含必须执行的逻辑。 库使用者通常在继承基类时调用基类生命周期方法,因为库基类通常具有要执行的自定义生命周期逻辑。 如果应用使用某个库中的基类,请参阅该库的文档以获取指导。
以下示例中调用了 base.OnInitialized(); 以确保会执行基类的 OnInitialized 方法。 如果没有调用,BlazorRocksBase2.OnInitialized 不会执行。
@page "/blazor-rocks-2"
@inherits BlazorRocksBase2
@inject ILogger<BlazorRocks2> Logger
<PageTitle>Blazor Rocks!</PageTitle>
<h1>Blazor Rocks! Example 2</h1>
<p>
@BlazorRocksText
</p>
@code {
protected override void OnInitialized()
{
Logger.LogInformation("Initialization code of BlazorRocks2 executed!");
base.OnInitialized();
}
}
using Microsoft.AspNetCore.Components;
namespace BlazorAppWasm
{
public class BlazorRocksBase2: ComponentBase
{
[Inject]
private ILogger<BlazorRocksBase2> Logger { get; set; } = default!;
public string BlazorRocksText { get; set; } = "Blazor rocks the browser!";
protected override void OnInitialized() =>
Logger.LogInformation("Initialization code of BlazorRocksBase2 executed!");
}
}
2、数据绑定
Blazor提供了强大的数据绑定机制,主要包括单向绑定 和双向绑定 两种模式。
1. 单向数据绑定
单向绑定是指数据从组件流向UI,但UI的变化不会自动更新数据源。
基本语法
<!-- 使用 @ 符号进行单向绑定 -->
<p>当前值: @currentValue</p>
<span>用户名: @UserName</span>
<div>创建时间: @CreateTime.ToString("yyyy-MM-dd")</div>
完整示例
<!-- OneWayBinding.razor -->
<div class="one-way-demo">
<h3>单向绑定示例</h3>
<!-- 显示数据,但不允许编辑 -->
<div class="display-area">
<p>计数器: <strong>@count</strong></p>
<p>消息: <strong>@message</strong></p>
<p>用户信息: <strong>@user.Name</strong> - <strong>@user.Age</strong>岁</p>
</div>
<!-- 控制按钮 -->
<div class="control-area">
<button @onclick="Increment" class="btn btn-primary">增加计数</button>
<button @onclick="ChangeMessage" class="btn btn-secondary">更改消息</button>
<button @onclick="UpdateUser" class="btn btn-info">更新用户</button>
</div>
</div>
@code {
private int count = 0;
private string message = "初始消息";
private User user = new User { Name = "张三", Age = 25 };
private void Increment()
{
count++;
// StateHasChanged(); // 通常不需要手动调用,事件处理会自动触发重新渲染
}
private void ChangeMessage()
{
message = $"消息已更新: {DateTime.Now:HH:mm:ss}";
}
private void UpdateUser()
{
user = new User { Name = "李四", Age = 30 };
}
class User
{
public string Name { get; set; } = string.Empty;
public int Age { get; set; }
}
}
2. 双向数据绑定
双向绑定允许数据在组件和UI之间双向流动:UI变化自动更新数据源,数据源变化自动更新UI。
基本语法
<!-- 使用 @bind 指令进行双向绑定 -->
<input @bind="propertyName" />
<input @bind="fieldName" />
<select @bind="selectedValue">...</select>
完整示例
<!-- TwoWayBinding.razor -->
<div class="two-way-demo">
<h3>双向绑定示例</h3>
<div class="form-group">
<label>用户名:</label>
<input @bind="userName" class="form-control" />
<small>显示: @userName</small>
</div>
<div class="form-group">
<label>邮箱:</label>
<input @bind="email" class="form-control" />
<small>显示: @email</small>
</div>
<div class="form-group">
<label>年龄:</label>
<input @bind="age" type="number" class="form-control" />
<small>显示: @age</small>
</div>
<div class="form-group">
<label>城市:</label>
<select @bind="selectedCity" class="form-control">
<option value="">请选择</option>
<option value="Beijing">北京</option>
<option value="Shanghai">上海</option>
<option value="Guangzhou">广州</option>
<option value="Shenzhen">深圳</option>
</select>
<small>选择: @selectedCity</small>
</div>
<div class="form-group">
<label>是否同意协议:</label>
<input type="checkbox" @bind="isAgreed" />
<span>@(isAgreed ? "已同意" : "未同意")</span>
</div>
<!-- 显示汇总信息 -->
<div class="summary">
<h4>汇总信息:</h4>
<p>用户名: @userName</p>
<p>邮箱: @email</p>
<p>年龄: @age</p>
<p>城市: @selectedCity</p>
<p>同意协议: @isAgreed</p>
</div>
</div>
@code {
private string userName = string.Empty;
private string email = string.Empty;
private int age = 0;
private string selectedCity = string.Empty;
private bool isAgreed = false;
}
3. 绑定事件控制
3.1 绑定特定事件
默认情况下,@bind 在失去焦点时更新。可以使用 @bind:event 指定触发事件:
<!-- 实时绑定(输入时立即更新) -->
<div class="real-time-demo">
<h4>实时绑定示例</h4>
<input @bind="searchText" @bind:event="oninput"
placeholder="输入搜索内容..." />
<p>实时搜索: @searchText</p>
<!-- 对比默认行为 -->
<input @bind="normalText" placeholder="默认绑定(失去焦点更新)" />
<p>默认绑定: @normalText</p>
</div>
@code {
private string searchText = string.Empty;
private string normalText = string.Empty;
}
3.2 绑定格式化
<div class="format-demo">
<h4>格式化绑定示例</h4>
<!-- 日期格式化 -->
<input @bind="startDate" @bind:format="yyyy-MM-dd" type="date" />
<p>选择的日期: @startDate.ToString("yyyy年MM月dd日")</p>
<!-- 数字格式化 -->
<input @bind="price" @bind:format="F2" type="number" step="0.01" />
<p>价格: @price.ToString("C")</p>
</div>
@code {
private DateTime startDate = DateTime.Today;
private decimal price = 0.00m;
}
4. 自定义组件双向绑定
在自定义组件中实现双向绑定:
子组件
<!-- CustomInput.razor -->
<div class="custom-input">
<label>@Label</label>
<input
value="@Value"
@oninput="HandleInput"
class="form-control @AdditionalClass"
placeholder="@Placeholder" />
@if (!string.IsNullOrEmpty(ValidationMessage))
{
<div class="text-danger">@ValidationMessage</div>
}
</div>
@code {
[Parameter]
public string Value { get; set; } = string.Empty;
[Parameter]
public EventCallback<string> ValueChanged { get; set; }
[Parameter]
public string Label { get; set; } = string.Empty;
[Parameter]
public string Placeholder { get; set; } = string.Empty;
[Parameter]
public string AdditionalClass { get; set; } = string.Empty;
[Parameter]
public string ValidationMessage { get; set; } = string.Empty;
private async Task HandleInput(ChangeEventArgs e)
{
Value = e.Value?.ToString() ?? string.Empty;
await ValueChanged.InvokeAsync(Value);
}
}
父组件使用
<!-- ParentComponent.razor -->
<div class="parent-demo">
<h3>自定义组件双向绑定</h3>
<CustomInput
@bind-Value="userName"
Label="用户名"
Placeholder="请输入用户名" />
<CustomInput
@bind-Value="email"
Label="邮箱"
Placeholder="请输入邮箱地址"
ValidationMessage="@(IsValidEmail ? "" : "邮箱格式不正确")" />
<div class="result">
<p>用户名: @userName</p>
<p>邮箱: @email</p>
</div>
</div>
@code {
private string userName = string.Empty;
private string email = string.Empty;
private bool IsValidEmail => email.Contains("@") && email.Contains(".");
}
5.复杂对象绑定
<!-- ComplexObjectBinding.razor -->
<div class="complex-binding">
<h3>复杂对象绑定</h3>
<div class="form-section">
<h4>用户信息</h4>
<div class="form-group">
<label>姓名:</label>
<input @bind="currentUser.Name" class="form-control" />
</div>
<div class="form-group">
<label>年龄:</label>
<input @bind="currentUser.Age" type="number" class="form-control" />
</div>
<div class="form-group">
<label>地址:</label>
<input @bind="currentUser.Address.Street" class="form-control" placeholder="街道" />
<input @bind="currentUser.Address.City" class="form-control" placeholder="城市" />
</div>
</div>
<div class="display-section">
<h4>当前用户信息:</h4>
<pre>@userInfoJson</pre>
</div>
<button @onclick="ResetUser" class="btn btn-warning">重置用户</button>
<button @onclick="CreateNewUser" class="btn btn-success">创建新用户</button>
</div>
@code {
private User currentUser = new User();
private string userInfoJson =>
System.Text.Json.JsonSerializer.Serialize(currentUser, new System.Text.Json.JsonSerializerOptions
{
WriteIndented = true
});
private void ResetUser()
{
currentUser = new User();
}
private void CreateNewUser()
{
currentUser = new User
{
Name = "新用户",
Age = 18,
Address = new Address { Street = "新建街道", City = "新建城市" }
};
}
class User
{
public string Name { get; set; } = string.Empty;
public int Age { get; set; }
public Address Address { get; set; } = new Address();
}
class Address
{
public string Street { get; set; } = string.Empty;
public string City { get; set; } = string.Empty;
}
}
6.绑定模式对比
|-------|----------------------------------------|---------|-----------|
| 绑定类型 | 语法 | 更新时机 | 适用场景 |
| 单向绑定 | @property | 数据源变化时 | 显示数据、计算属性 |
| 双向绑定 | @bind="property" | 失去焦点时 | 表单输入、用户交互 |
| 实时双向 | @bind="property" @bind:event="oninput" | 输入时实时更新 | 搜索框、实时验证 |
| 自定义绑定 | @bind-Value="property" | 自定义事件触发 | 自定义表单组件 |
3、事件处理
1. 基本事件处理
1.1 单击事件
<!-- ClickEvents.razor -->
<div class="click-demo">
<h3>单击事件示例</h3>
<!-- 基本点击事件 -->
<button @onclick="HandleClick" class="btn btn-primary">
点击我
</button>
<!-- 带参数的事件处理 -->
<div class="button-group">
<button @onclick="() => HandleButtonClick(1)" class="btn btn-secondary">按钮 1</button>
<button @onclick="() => HandleButtonClick(2)" class="btn btn-secondary">按钮 2</button>
<button @onclick="() => HandleButtonClick(3)" class="btn btn-secondary">按钮 3</button>
</div>
<!-- 显示点击结果 -->
<div class="result">
<p>最后点击的按钮: @lastClickedButton</p>
<p>点击次数: @clickCount</p>
</div>
</div>
@code {
private int lastClickedButton = 0;
private int clickCount = 0;
private void HandleClick()
{
clickCount++;
Console.WriteLine("按钮被点击了!");
}
private void HandleButtonClick(int buttonNumber)
{
lastClickedButton = buttonNumber;
clickCount++;
StateHasChanged();
}
}
1.2 异步事件处理
<!-- AsyncEvents.razor -->
<div class="async-demo">
<h3>异步事件处理</h3>
<button @onclick="HandleAsyncClick" class="btn btn-primary" disabled="@isLoading">
@if (isLoading)
{
<span>加载中...</span>
}
else
{
<span>模拟异步操作</span>
}
</button>
<div class="result">
<p>操作结果: @operationResult</p>
<p>耗时: @elapsedTime 毫秒</p>
</div>
</div>
@code {
private bool isLoading = false;
private string operationResult = string.Empty;
private long elapsedTime = 0;
private async Task HandleAsyncClick()
{
isLoading = true;
operationResult = "操作开始...";
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
// 模拟异步操作
await Task.Delay(2000);
stopwatch.Stop();
elapsedTime = stopwatch.ElapsedMilliseconds;
operationResult = $"操作完成!数据已保存。";
isLoading = false;
StateHasChanged();
}
}
2. 表单事件处理
2.1 输入事件
<!-- FormEvents.razor -->
<div class="form-events">
<h3>表单事件处理</h3>
<div class="form-group">
<label>输入文本:</label>
<input @oninput="HandleInput"
@onchange="HandleChange"
class="form-control"
placeholder="输入内容..." />
<small>实时输入: @inputValue | 变化事件: @changeValue</small>
</div>
<div class="form-group">
<label>选择选项:</label>
<select @onchange="HandleSelectChange" class="form-control">
<option value="">请选择</option>
<option value="option1">选项一</option>
<option value="option2">选项二</option>
<option value="option3">选项三</option>
</select>
<small>选择的值: @selectedValue</small>
</div>
<div class="form-group">
<label>
<input type="checkbox" @onchange="HandleCheckboxChange" />
同意条款
</label>
<small>状态: @(isChecked ? "已选中" : "未选中")</small>
</div>
<!-- 表单提交 -->
<form @onsubmit="HandleSubmit" @onvalidSubmit="HandleValidSubmit">
<div class="form-group">
<label>用户名:</label>
<input @bind="user.Username" class="form-control" required />
</div>
<div class="form-group">
<label>邮箱:</label>
<input @bind="user.Email" type="email" class="form-control" required />
</div>
<button type="submit" class="btn btn-success">提交表单</button>
</form>
<div class="form-result">
<h4>表单数据:</h4>
<pre>@System.Text.Json.JsonSerializer.Serialize(user, new System.Text.Json.JsonSerializerOptions { WriteIndented = true })</pre>
<p>提交状态: @submitStatus</p>
</div>
</div>
@code {
private string inputValue = string.Empty;
private string changeValue = string.Empty;
private string selectedValue = string.Empty;
private bool isChecked = false;
private string submitStatus = "未提交";
private User user = new User();
private void HandleInput(ChangeEventArgs e)
{
inputValue = e.Value?.ToString() ?? string.Empty;
}
private void HandleChange(ChangeEventArgs e)
{
changeValue = e.Value?.ToString() ?? string.Empty;
}
private void HandleSelectChange(ChangeEventArgs e)
{
selectedValue = e.Value?.ToString() ?? string.Empty;
}
private void HandleCheckboxChange(ChangeEventArgs e)
{
isChecked = (bool)(e.Value ?? false);
}
private void HandleSubmit()
{
submitStatus = "表单提交(可能有验证错误)";
}
private void HandleValidSubmit()
{
submitStatus = $"表单验证通过!数据已保存 - {DateTime.Now:HH:mm:ss}";
// 这里可以调用API保存数据
}
class User
{
public string Username { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
}
}
3. 鼠标和键盘事件
3.1 鼠标事件
<!-- MouseEvents.razor -->
<div class="mouse-events">
<h3>鼠标事件</h3>
<div class="interactive-area"
@onmousedown="HandleMouseDown"
@onmouseup="HandleMouseUp"
@onmousemove="HandleMouseMove"
@onmouseover="HandleMouseOver"
@onmouseout="HandleMouseOut"
@onclick="HandleAreaClick"
@ondblclick="HandleDoubleClick"
style="width: 300px; height: 200px; border: 2px solid #007bff; padding: 20px; margin: 10px 0;">
鼠标交互区域
</div>
<div class="event-log">
<h4>事件日志:</h4>
<ul>
@foreach (var log in eventLogs.TakeLast(10).Reverse())
{
<li>@log</li>
}
</ul>
</div>
<div class="mouse-info">
<p>鼠标位置: (@mouseX, @mouseY)</p>
<p>按钮状态: @(isMouseDown ? "按下" : "释放")</p>
<p>悬停状态: @(isMouseOver ? "在区域内" : "在区域外")</p>
</div>
</div>
@code {
private double mouseX = 0;
private double mouseY = 0;
private bool isMouseDown = false;
private bool isMouseOver = false;
private List<string> eventLogs = new List<string>();
private void LogEvent(string eventName)
{
eventLogs.Add($"{DateTime.Now:HH:mm:ss.fff} - {eventName}");
StateHasChanged();
}
private void HandleMouseDown(MouseEventArgs e)
{
isMouseDown = true;
LogEvent($"MouseDown - 按钮: {e.Button}, 位置: ({e.ClientX}, {e.ClientY})");
}
private void HandleMouseUp(MouseEventArgs e)
{
isMouseDown = false;
LogEvent($"MouseUp - 按钮: {e.Button}, 位置: ({e.ClientX}, {e.ClientY})");
}
private void HandleMouseMove(MouseEventArgs e)
{
mouseX = e.ClientX;
mouseY = e.ClientY;
// 注意:频繁触发,生产环境需要节流
// LogEvent($"MouseMove - 位置: ({e.ClientX}, {e.ClientY})");
}
private void HandleMouseOver(MouseEventArgs e)
{
isMouseOver = true;
LogEvent("MouseOver");
}
private void HandleMouseOut(MouseEventArgs e)
{
isMouseOver = false;
LogEvent("MouseOut");
}
private void HandleAreaClick(MouseEventArgs e)
{
LogEvent($"Click - 按钮: {e.Button}");
}
private void HandleDoubleClick(MouseEventArgs e)
{
LogEvent($"DoubleClick - 按钮: {e.Button}");
}
}
3.2 键盘事件
<!-- KeyboardEvents.razor -->
<div class="keyboard-events">
<h3>键盘事件</h3>
<div class="input-area">
<input @onkeydown="HandleKeyDown"
@onkeyup="HandleKeyUp"
@onkeypress="HandleKeyPress"
class="form-control"
placeholder="在这里输入并观察键盘事件..." />
</div>
<div class="event-log">
<h4>键盘事件日志:</h4>
<ul>
@foreach (var log in keyEventLogs.TakeLast(10).Reverse())
{
<li>@log</li>
}
</ul>
</div>
<div class="key-info">
<p>最后按下的键: @lastKey</p>
<p>Ctrl 按下: @(isCtrlPressed ? "是" : "否")</p>
<p>Shift 按下: @(isShiftPressed ? "是" : "否")</p>
<p>Alt 按下: @(isAltPressed ? "是" : "否")</p>
</div>
</div>
@code {
private string lastKey = "无";
private bool isCtrlPressed = false;
private bool isShiftPressed = false;
private bool isAltPressed = false;
private List<string> keyEventLogs = new List<string>();
private void LogKeyEvent(string eventName, KeyboardEventArgs e)
{
var log = $"{DateTime.Now:HH:mm:ss.fff} - {eventName}: Key='{e.Key}', Code='{e.Code}'";
if (e.CtrlKey) log += " [Ctrl]";
if (e.ShiftKey) log += " [Shift]";
if (e.AltKey) log += " [Alt]";
keyEventLogs.Add(log);
StateHasChanged();
}
private void HandleKeyDown(KeyboardEventArgs e)
{
lastKey = e.Key;
isCtrlPressed = e.CtrlKey;
isShiftPressed = e.ShiftKey;
isAltPressed = e.AltKey;
LogKeyEvent("KeyDown", e);
// 快捷键处理示例
if (e.CtrlKey && e.Key == "s")
{
e.PreventDefault(); // 阻止浏览器默认保存行为
LogKeyEvent("快捷键: Ctrl+S", e);
}
}
private void HandleKeyUp(KeyboardEventArgs e)
{
isCtrlPressed = e.CtrlKey;
isShiftPressed = e.ShiftKey;
isAltPressed = e.AltKey;
LogKeyEvent("KeyUp", e);
}
private void HandleKeyPress(KeyboardEventArgs e)
{
LogKeyEvent("KeyPress", e);
}
}
4. 焦点和剪贴板事件
<!-- FocusClipboardEvents.razor -->
<div class="focus-clipboard">
<h3>焦点和剪贴板事件</h3>
<div class="form-group">
<label>焦点测试输入框:</label>
<input @onfocus="HandleFocus"
@onblur="HandleBlur"
class="form-control"
placeholder="点击获取焦点,点击别处失去焦点" />
</div>
<div class="form-group">
<label>复制粘贴测试:</label>
<textarea @oncopy="HandleCopy"
@oncut="HandleCut"
@onpaste="HandlePaste"
class="form-control"
rows="3"
placeholder="在这里测试复制、剪切、粘贴操作">这是一些测试文本</textarea>
</div>
<div class="event-log">
<h4>事件状态:</h4>
<p>焦点状态: <span class="@(hasFocus ? "text-success" : "text-danger")">@(hasFocus ? "有焦点" : "无焦点")</span></p>
<p>最后操作: @lastOperation</p>
<p>剪贴板内容: @clipboardContent</p>
</div>
</div>
@code {
private bool hasFocus = false;
private string lastOperation = "无";
private string clipboardContent = "无";
private void HandleFocus(FocusEventArgs e)
{
hasFocus = true;
lastOperation = "获得焦点";
StateHasChanged();
}
private void HandleBlur(FocusEventArgs e)
{
hasFocus = false;
lastOperation = "失去焦点";
StateHasChanged();
}
private void HandleCopy(ClipboardEventArgs e)
{
lastOperation = "复制操作";
clipboardContent = "复制的内容无法直接获取(安全限制)";
StateHasChanged();
}
private void HandleCut(ClipboardEventArgs e)
{
lastOperation = "剪切操作";
clipboardContent = "剪切的内容无法直接获取(安全限制)";
StateHasChanged();
}
private void HandlePaste(ClipboardEventArgs e)
{
lastOperation = "粘贴操作";
clipboardContent = "粘贴的内容无法直接获取(安全限制)";
StateHasChanged();
}
}
5. 自定义事件处理
5.1 事件参数封装
<!-- CustomEventHandling.razor -->
<div class="custom-events">
<h3>自定义事件处理</h3>
<!-- 事件冒泡和阻止默认行为 -->
<div @onclick="HandleParentClick" style="padding: 20px; border: 2px solid red;">
<p>父级区域(点击会触发)</p>
<button @onclick="HandleChildClick"
@onclick:stopPropagation
class="btn btn-primary">
子按钮(点击不会冒泡)
</button>
<button @onclick="HandleChildClickWithPrevent"
@onclick:preventDefault
class="btn btn-secondary">
阻止默认行为的按钮
</button>
</div>
<!-- 自定义事件处理逻辑 -->
<div class="custom-actions">
<h4>自定义操作:</h4>
<button @onclick="HandleCustomAction1" class="btn btn-info">操作1</button>
<button @onclick="HandleCustomAction2" class="btn btn-info">操作2</button>
<button @onclick="async () => await HandleCustomAsyncAction()" class="btn btn-info">异步操作</button>
</div>
<div class="action-log">
<h4>操作日志:</h4>
<ul>
@foreach (var log in actionLogs.TakeLast(5).Reverse())
{
<li>@log</li>
}
</ul>
</div>
</div>
@code {
private List<string> actionLogs = new List<string>();
private void LogAction(string action)
{
actionLogs.Add($"{DateTime.Now:HH:mm:ss} - {action}");
StateHasChanged();
}
private void HandleParentClick()
{
LogAction("父级区域被点击");
}
private void HandleChildClick()
{
LogAction("子按钮被点击(事件不会冒泡)");
}
private void HandleChildClickWithPrevent()
{
LogAction("阻止默认行为的按钮被点击");
}
private void HandleCustomAction1()
{
LogAction("执行自定义操作1");
// 自定义业务逻辑
}
private void HandleCustomAction2(MouseEventArgs e)
{
LogAction($"执行自定义操作2 - 点击位置: ({e.ClientX}, {e.ClientY})");
// 自定义业务逻辑
}
private async Task HandleCustomAsyncAction()
{
LogAction("开始异步操作");
await Task.Delay(1000);
LogAction("异步操作完成");
}
}
6. 事件处理最佳实践
6.1 性能优化
<!-- OptimizedEvents.razor -->
<div class="optimized-events">
<h3>事件处理性能优化</h3>
<!-- 避免内联Lambda表达式(可能引起不必要的重渲染) -->
@foreach (var item in items)
{
<div class="item" @key="item.Id">
<span>@item.Name</span>
<!-- 好的做法:使用方法引用 -->
<button @onclick="() => DeleteItem(item.Id)" class="btn btn-sm btn-danger">删除</button>
</div>
}
<!-- 大量事件考虑使用事件委托 -->
<div class="large-list">
@foreach (var item in largeList)
{
<div class="list-item" data-id="@item.Id" data-name="@item.Name" @onclick="(e) => HandleListItemClick(e, item.Id)">
@item.Name
</div>
}
</div>
<div class="action-log">
<h4>操作日志:</h4>
<ul>
@foreach (var log in actionLogs.TakeLast(5).Reverse())
{
<li>@log</li>
}
</ul>
</div>
</div>
@code {
private List<Item> items = new List<Item>
{
new Item { Id = 1, Name = "项目1" },
new Item { Id = 2, Name = "项目2" },
new Item { Id = 3, Name = "项目3" }
};
private List<Item> largeList = Enumerable.Range(1, 100)
.Select(i => new Item { Id = i, Name = $"项目{i}" })
.ToList();
private List<string> actionLogs = new List<string>();
private void DeleteItem(int id)
{
items.RemoveAll(i => i.Id == id);
LogAction($"删除了项目 {id}");
}
private void HandleListItemClick(MouseEventArgs e, int itemId)
{
// 通过参数 itemId 就知道是哪个按钮被点击了
Console.WriteLine($"Clicked item ID: {itemId}");
}
// 添加 LogAction 方法
private void LogAction(string action)
{
actionLogs.Add($"{DateTime.Now:HH:mm:ss} - {action}");
StateHasChanged();
}
class Item
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
}
}
7. 常用事件总结
|------|--------------|-------------------|--------|
| 事件类型 | 指令 | 事件参数 | 说明 |
| 点击事件 | @onclick | MouseEventArgs | 鼠标点击 |
| 双击事件 | @ondblclick | MouseEventArgs | 鼠标双击 |
| 鼠标移动 | @onmousemove | MouseEventArgs | 鼠标移动 |
| 鼠标按下 | @onmousedown | MouseEventArgs | 鼠标按下 |
| 鼠标释放 | @onmouseup | MouseEventArgs | 鼠标释放 |
| 键盘按下 | @onkeydown | KeyboardEventArgs | 键盘按下 |
| 键盘释放 | @onkeyup | KeyboardEventArgs | 键盘释放 |
| 输入事件 | @oninput | ChangeEventArgs | 输入时触发 |
| 变化事件 | @onchange | ChangeEventArgs | 值变化时触发 |
| 获得焦点 | @onfocus | FocusEventArgs | 元素获得焦点 |
| 失去焦点 | @onblur | FocusEventArgs | 元素失去焦点 |
| 表单提交 | @onsubmit | EventArgs | 表单提交 |
4、组件参数和级联参数
1. 组件参数(Parameter)
参数主要用来在各组件之间传递值,在初始项目的SurveyPrompt组件中就包含了一个参数:
[Parameter]
public string Title { get; set; }
通过用Parameter修饰符来修饰,就可以将指定的属性(注意要是public的)声明为参数,使用也很简单:
<SurveyPrompt Title="这里是参数的值" />
2. CaptureUnmatchedValues
是 Blazor 中一个非常有用的特性,它允许组件捕获所有未匹配到组件参数的额外属性。
基本概念
当你在组件上设置了属性,但这些属性没有对应的 时, 可以捕获这些"未匹配"的属性,而且修饰的属性必须要是字典类型:IDictionary<string,object>。
基本用法
`<!-- MyComponent.razor -->
<div @attributes="AdditionalAttributes">
组件内容
</div>
@code {
[Parameter(CaptureUnmatchedValues = true)]
public Dictionary<string, object> AdditionalAttributes { get; set; } =
new Dictionary<string, object>();
}`
使用场景示例
1. 创建可复用的按钮组件
`<!-- MyButton.razor -->
<button @attributes="AdditionalAttributes" class="btn @Class">
@ChildContent
</button>
@code {
[Parameter]
public string Class { get; set; } = string.Empty;
[Parameter]
public RenderFragment? ChildContent { get; set; }
[Parameter(CaptureUnmatchedValues = true)]
public Dictionary<string, object> AdditionalAttributes { get; set; } =
new Dictionary<string, object>();
}`
使用方式
`<MyButton class="btn-primary"
id="submit-btn"
onclick="console.log('clicked me')"
data-custom="value">
点击我
</MyButton>`
2.包装第三方组件
`<!-- WrapperComponent.razor -->
<ThirdPartyComponent @attributes="AdditionalAttributes"
SpecificParameter="@SpecificValue" />
@code {
[Parameter]
public string SpecificValue { get; set; } = string.Empty;
[Parameter(CaptureUnmatchedValues = true)]
public Dictionary<string, object> AdditionalAttributes { get; set; } =
new Dictionary<string, object>();
}`
实际应用案例
创建灵活的容器组件
`<!-- FlexContainer.razor -->
<div @attributes="AdditionalAttributes" class="flex-container @Class">
@ChildContent
</div>
@code {
[Parameter]
public string Class { get; set; } = string.Empty;
[Parameter]
public RenderFragment? ChildContent { get; set; }
[Parameter(CaptureUnmatchedValues = true)]
public Dictionary<string, object> AdditionalAttributes { get; set; } =
new Dictionary<string, object>();
}`
使用示例:
`<FlexContainer class="my-styles"
id="main-container"
style=""
data-tracking="user-section"
aria-label="主要区域">
<p>这里是可以自定义样式的容器内容</p>
</FlexContainer>`
3. 级联参数(CascadingParameter)
级联参数看起来就比Parameter更高级,主要用来在多级组件之间传递参数,听起来有点抽象,咱们举个栗子:
考虑以下三个组件的嵌套关系,如果想把一个参数同时传给CascComp1和CascComp2应该如何做呢?

如果采用普通的Parameter,代码应该是这样的:
`<!--this is CascCompSample.razor-->
<h3>This is the sample page</h3>
<CascComp1 NickName="沈先生"></CascComp1>`
`<!--this is CascComp1.razor-->
<h3>Comp1: @NickName</h3>
<CascComp2 NickName="@NickName"></CascComp2>
@code {
[Parameter]
public string NickName { get; set; }
}`
`<!--this is CascComp2.razor-->
<h3>Comp2: @NickName</h3>
@code {
[Parameter]
public string NickName { get; set; }
}`
采用CascadingParameter会有什么不一样呢?请看:
`<!--this is CascCompSample.razor-->
@page "/cascparamsample"
<h3>This is the sample page</h3>
<CascadingValue Value="NickName">
<CascComp1></CascComp1>
</CascadingValue>
@code
{
private string NickName = "沈先生";
}`
`<!--this is CascComp1.razor-->
<h3>Comp1: @NickName</h3>
<CascComp2></CascComp2>
@code {
[CascadingParameter]
public string NickName { get; set; }
}`
`<!--this is CascComp2.razor-->
<h3>Comp2: @NickName</h3>
@code {
[CascadingParameter]
public string NickName { get; set; }
}`
看到区别了吗?
- 首先在CascCompSample.razor页面,我们通过把CascComp1嵌套到CascadingValue里面来传递参数。
- 其次在CascComp1和CascComp2,不再需要显式传递参数,只需要声明CascadingParameter即可拿到值。
CascadingValue组件的Value参数不能直接传递字符串,必须要声明一个变量
那么什么场景下需要用到这种方式呢?我想比较多的还是用来在多个组件之间共享上下文吧。
4. CascadingParameter如何传递多个参数
前面的例子我们通过CascadingParameter传递了一个参数,那么有没有办法传递多个参数呢?
当然可以,CascadingValue是支持嵌套的,你可以这样:
`<!--this is CascCompSample.razor-->
@page "/cascparamsample"
<h3>This is the sample page</h3>
<CascadingValue Value="NickName">
<CascadingValue Value="36">
<CascComp1></CascComp1>
</CascadingValue>
</CascadingValue>
@code
{
private string NickName = "沈先生";
}`
`<!--this is CascComp1.razor-->
<h3>Comp1: @NickName - @Age</h3>
<CascComp2></CascComp2>
@code {
[CascadingParameter]
public string NickName { get; set; }
[CascadingParameter]
public int Age { get; set; }
}`
Blazor是通过参数的类型来关联的,在外层通过CascadingValue传递了一个字符串和一个整数,在里层则通过类型匹配将字符串赋值给NickName,将整数赋值给Age。所以里层的参数名是可以随便取的,你可以把NickName改为FullName,并不会影响参数值的获取。
这个方式虽然可以少写一些代码,但是容易出错,而且如果碰到多个同类型的参数就无法处理了,笔者并不建议用这种方式。
除此之外,CascadingValue还有一个Name参数,可以给每个参数指定参数名,这样就可以显式的把各个组件的参数关联起来,笔者建议不管是一个参数还是多个参数都指定一个名字,这样可以尽量避免混淆,代码如下:
`<!--this is CascCompSample.razor-->
@page "/cascparamsample"
<h3>This is the sample page</h3>
<CascadingValue Value="NickName" Name="NickName">
<CascadingValue Value="36" Name="Age">
<CascadingValue Value="Sex" Name="Sex">
<CascComp1></CascComp1>
</CascadingValue>
</CascadingValue>
</CascadingValue>
@code
{
private string NickName = "沈先生";
}`
`<!--this is CascComp1.razor-->
<h3>Comp1: @NickName - @Sex - @Age</h3>
<CascComp2></CascComp2>
@code {
[CascadingParameter(Name="NickName")]
public string NickName { get; set; }
[CascadingParameter(Name = "Sex")]
public string? Sex { get; set; }
[CascadingParameter(Name="Age")]
public int Age { get; set; }
}`
需要注意的是如果在CascadingValue组件里面指定了Name参数,那么在所有CascadingParameter的地方也需要指定Name,否则就会找不到参数值。