ASP.NET Core Blazor簡介和快速入門二(組件基礎)
大家好,我是碼農剛子。上一章介紹了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>
<h1>After Render Example</h1>
<p>
<button @onclick="HandleClick">Log information (and trigger a render)</button>
</p>
<p>Study logged messages in the console.</p>
@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.綁定模式對比
|
綁定類型 |
語法 |
更新時機 |
適用場景 |
|
單向綁定 |
|
數據源變化時 |
顯示數據、計算屬性 |
|
雙向綁定 |
|
失去焦點時 |
表單輸入、用戶交互 |
|
實時雙向 |
|
輸入時實時更新 |
搜索框、實時驗證 |
|
自定義綁定 |
|
自定義事件觸發 |
自定義表單組件 |
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. 常用事件總結
|
事件類型 |
指令 |
事件參數 |
說明 |
|
點擊事件 |
|
|
鼠標點擊 |
|
雙擊事件 |
|
|
鼠標雙擊 |
|
鼠標移動 |
|
|
鼠標移動 |
|
鼠標按下 |
|
|
鼠標按下 |
|
鼠標釋放 |
|
|
鼠標釋放 |
|
鍵盤按下 |
|
|
鍵盤按下 |
|
鍵盤釋放 |
|
|
鍵盤釋放 |
|
輸入事件 |
|
|
輸入時觸發 |
|
變化事件 |
|
|
值變化時觸發 |
|
獲得焦點 |
|
|
元素獲得焦點 |
|
失去焦點 |
|
|
元素失去焦點 |
|
表單提交 |
|
|
表單提交 |
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. 創建可復用的按鈕組件
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.包裝第三方組件
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)
3. 級聯參數(CascadingParameter)級聯參數看起來就比Parameter更高級,主要用來在多級組件之間傳遞參數,聽起來有點抽象,咱們舉個栗子:
考慮以下三個組件的嵌套關系,如果想把一個參數同時傳給CascComp1和CascComp2應該如何做呢?

如果采用普通的Parameter,代碼應該是這樣的:
如果采用普通的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會有什么不一樣呢?請看:
采用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如何傳遞多個參數
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,否則就會找不到參數值。

浙公網安備 33010602011771號