使用 Blazor 事件處理常式將 C# 程式碼附加到 DOM 事件
大部分的 HTML 元素都會公開發生重大情況 (例如當頁面已完成載入、使用者按了一下按鈕,或 HTML 元素的內容已變更時) 所觸發的事件。 應用程式可以透過下列幾種方式來處理事件:
- 應用程式可以忽略事件。
- 應用程式可執行以 JavaScript 撰寫的事件處理常式來處理事件。
- 應用程式可執行以 C# 撰寫的 Blazor 事件處理常式來處理事件。
在本單元中,我們將深入了解第三個選項:如何以 C# 建立 Blazor 事件處理常式來處理事件。
使用 Blazor 和 C# 來處理事件
Blazor 應用程式中 HTML 標記的每個元素,都可支援許多事件。 這些事件大多可對應到一般 Web 應用程式中可用的 DOM 事件,但您也可以建立由撰寫程式碼觸發的使用者定義事件。 若要使用 Blazor 來擷取事件,請編寫可處理事件的 C# 方法,然後使用 Blazor 指示詞將事件繫結到此方法。 如果是 DOM 事件,則 Blazor 指示詞會與對等的 HTML 事件 (例如 @onkeydown
或 @onfocus
) 共用相同的名稱。 例如,使用 Blazor Server 應用程式所產生的範例應用程式,包含了 Counter.razor 頁面上的下列程式碼。 此頁面會顯示一個按鈕。 當使用者選取按鈕時,@onclick
事件會觸發 IncrementCount
方法以遞增計數器,該計數器會指出按下按鈕的次數。 計數器變數的值是由頁面上的 <p> 元素顯示:
@page "/counter"
<h1>Counter</h1>
<p>Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
private int currentCount = 0;
private void IncrementCount()
{
currentCount++;
}
}
許多事件處理常式方法,都會採用可提供額外上下文資訊的參數。 這種參數就是所謂的 EventArgs
參數。 例如,@onclick
事件會在 MouseEventArgs
參數中,傳送使用者按下了哪個按鈕,或是否在按下按鈕的同時也按下了 Ctrl 或 Alt 之類按鍵的相關資訊。 呼叫方法時,您不需要提供此參數;Blazor 執行階段會自動新增此參數。 您可以在事件處理常式中查詢此參數。 如果使用者在按下按鈕的同時也按下了 Ctrl 鍵,則下列程式碼會讓上一個範例中所顯示的計數器值增加 5:
@page "/counter"
<h1>Counter</h1>
<p>Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
private int currentCount = 0;
private void IncrementCount(MouseEventArgs e)
{
if (e.CtrlKey) // Ctrl key pressed as well
{
currentCount += 5;
}
else
{
currentCount++;
}
}
}
其他事件會提供不同的 EventArgs
參數。 例如,@onkeypress
事件會傳遞 KeyboardEventArgs
參數,指出使用者按下了哪個鍵。 對於任何 DOM 事件,如果您不需要這項資訊,則可以在事件處理方法中省略 EventArgs
參數。
了解以 JavaScript 和以 Blazor 處理事件的不同之處
傳統的 Web 應用程式會使用 JavaScript 來擷取及處理事件。 您要建立做為 HTML <script> 元素一部分的函式,然後安排在事件發生時呼叫該函式。 為了與上述的 Blazor 範例進行比較,下列程式碼會顯示 HTML 頁面中的片段,其中的值會增加,而且每當使用者選取了 [按一下這裡] 按鈕時,就會顯示結果。 此程式碼會使用 jQuery 程式庫來存取 DOM。
<p id="currentCount">Current count: 0</p>
<button class="btn btn-primary" onclick="incrementCount()">Click me</button>
<!-- Omitted for brevity -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script>
var currentCount = 0;
function incrementCount() {
currentCount++;
$('#currentCount').html('Current count:' + currentCount);
}
</script>
除了這兩種事件處理常式的語法差異之外,您應該注意下列功能上的差異:
- JavaScript 不會在事件名稱前面加上
@
符號,因為這不是 Blazor 指示詞。 - 而在 Blazor 程式碼中,當您將事件處理方法附加到事件時,需要指定事件處理方法的名稱。 在 JavaScript 中,需撰寫會呼叫事件處理方法的陳述式;您可以指定圓括弧和任何必要參數。
- 最重要的是,JavaScript 事件處理常式會在用戶端的瀏覽器中執行。 如果您要組建 Blazor Server 應用程式,Blazor 事件處理常式就會在該伺服器上執行,而且只有在事件處理常式完成時,才會使用您對 UI 所做的任何變更來更新瀏覽器。 此外,Blazor 機制可讓事件處理常式存取工作階段之間共用的靜態資料,而 JavaScript 模型則不會。 不過,處理某些經常發生的事件 (例如
@onmousemove
) 時,可能會因為事件需要透過網路在本機與伺服器之間往返,而導致使用者介面變得遲緩。 您可能會偏好使用 JavaScript 在瀏覽器中處理這類事件。
重要
您可以使用事件處理常式中的 JavaScript 程式碼來操作 DOM,也可以使用 C# Blazor 程式碼。 不過,Blazor 會維護自己的 DOM 複本,在必要時用以重新整理使用者介面。 如果您同時使用 JavaScript 和 Blazor 程式碼來變更 DOM 中的相同元素,則會有 DOM 損毀的風險,並可能危害到 Web 應用程式中資料的隱私權和安全性。
以非同步方式處理事件
根據預設,Blazor 事件處理常式為同步處理。 如果事件處理常式執行了可能會長時間執行的作業 (例如呼叫 Web 服務),則該事件處理常式上執行的執行緒將會遭到封鎖,直到作業完成為止。 這可能會導致使用者介面中的回應不佳。 若要解決這個問題,您可以將事件處理常式方法指定為非同步處理。 使用 C# 的 async
關鍵字。 此方法必須傳回 Task
物件。 然後,您可以使用事件處理常式方法內的 await
運算子,在不同的執行緒上起始任何長時間執行的工作,讓目前的執行緒上可以執行其他工作。 長時間執行的工作完成時,該事件處理常式會繼續執行。 下列範例事件處理常式,會以非同步方式執行耗時的方法:
<button @onclick="DoWork">Run time-consuming operation</button>
@code {
private async Task DoWork()
{
// Call a method that takes a long time to run and free the current thread
var data = await timeConsumingOperation();
// Omitted for brevity
}
}
注意
如需在 C# 中建立非同步方法的詳細資訊,請參閱非同步程式設計案例。
使用事件將焦點設定為 DOM 元素
在 HTML 頁面上,使用者可以按 Tab 鍵在元素之間切換,而焦點會自然地依 HTML 元素出現在頁面上的順序移動。 在某些情況下,您可能需要覆寫這樣的順序,並強制使用者瀏覽特定元素。
執行這項工作最簡單的方式是使用 FocusAsync
方法。 這是 ElementReference
物件的執行個體方法。 ElementReference
應該要參考您想設為焦點的項目。 您可以使用 @ref
屬性指定元素參考,並在程式碼中建立具有相同名稱的 C# 物件。
在下列範例中,<button> 元素的 @onclick
事件處理常式,會將焦點設定為 <input> 元素。 當元素取得焦點時,<input> 元素的 @onfocus
事件處理常式會顯示「已接收焦點」的訊息。 在程式碼中,會透過 InputField
變數來參考 <input> 元素:
<button class="btn btn-primary" @onclick="ChangeFocus">Click me to change focus</button>
<input @ref=InputField @onfocus="HandleFocus" value="@data"/>
@code {
private ElementReference InputField;
private string data;
private async Task ChangeFocus()
{
await InputField.FocusAsync();
}
private async Task HandleFocus()
{
data = "Received focus";
}
下圖顯示了使用者選取按鈕時的結果:
注意
應用程式應該只為了特定原因而將焦點導向到特定控制項 (例如在錯誤之後要求使用者修改輸入)。 請勿使用焦點來強制使用者以固定順序瀏覽頁面上的各項目;對於想要重新瀏覽元素以變更其輸入的使用者而言,這可能會令人心煩。
撰寫內嵌事件處理常式
C# 可支援 Lambda 運算式。 Lambda 運算式可讓您建立匿名函式。 如果您有不需要在頁面或元件中其他位置重複使用的簡單事件處理常式,Lambda 運算式會很實用。 在本單元開始時所顯示的初始點按計數範例中,您可以移除 IncrementCount
方法並改用 Lambda 運算式,可達到相同的效果:
@page "/counter"
<h1>Counter</h1>
<p>Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="() => currentCount++">Click me</button>
@code {
private int currentCount = 0;
}
注意
如需 Lambda 運算式運作方式的詳細資料,請參閱 Lambda 運算式和匿名函式。
如果您想要提供其他引數給事件處理方法,則此方法也很有用。 在下列範例中,HandleClick
方法會採用 MouseEventArgs
參數 (與一般 Click 事件處理常式相同的方式),但也能接受字串參數。 此方法會像之前一樣的方式處理 Click 事件,但若使用者已按下 Ctrl 鍵,也會顯示訊息。 Lambda 運算式會呼叫 HandleCLick
方法,並傳入 MouseEventArgs
參數 (mouseEvent
) 和字串中。
@page "/counter"
@inject IJSRuntime JS
<h1>Counter</h1>
<p id="currentCount">Current count: @currentCount</p>
<button class="btn btn-primary" @onclick='mouseEvent => HandleClick(mouseEvent, "Hello")'>Click me</button>
@code {
private int currentCount = 0;
private async Task HandleClick(MouseEventArgs e, string msg)
{
if (e.CtrlKey) // Ctrl key pressed as well
{
await JS.InvokeVoidAsync("alert", msg);
currentCount += 5;
}
else
{
currentCount++;
}
}
}
注意
此範例會使用 JavaScript 的 alert
函式來顯示訊息,因為 Blazor 中沒有對等的函式。 您可以使用 JavaScript Interop,從 Blazor 程式碼中呼叫 JavaScript。 這項技術的詳細資料,是另一個課程模組的主題。
覆寫事件的預設 DOM 動作
不論該事件是否有事件處理常式可供使用,數個 DOM 事件都有預設動作可在事件發生時執行。 例如,<input> 元素的 @onkeypress
事件,一律會顯示使用者所按下按鍵的對應字元,並處理按鍵動作。 在下一個範例中,會使用 @onkeypress
事件來將使用者輸入的字母轉換成大寫。 此外,如果使用者輸入了 @
字元,事件處理常式會顯示警示:
<input value=@data @onkeypress="ProcessKeyPress"/>
@code {
private string data;
private async Task ProcessKeyPress(KeyboardEventArgs e)
{
if (e.Key == "@")
{
await JS.InvokeVoidAsync("alert", "You pressed @");
}
else
{
data += e.Key.ToUpper();
}
}
}
如果您執行此程式碼並按下了 @
鍵,就會顯示該警示,但 @
字元仍會新增到輸入中。 新增 @
字元是該事件的預設動作。
如果您不想要讓此字元出現在輸入方塊中,可以使用事件的 preventDefault
屬性來覆寫預設動作,如下所示:
<input value=@data @onkeypress="ProcessKeyPress" @onkeypress:preventDefault />
事件仍會引發,但系統只會執行事件處理常式所定義的動作。
DOM 子項目中的某些事件,可以觸發其父元素中的事件。 在下列範例中,<div> 元素包含了 @onclick
事件處理常式。 <div> 內的 <button> 有自己的 @onclick
事件處理常式。 此外,<div> 還包含了 <input> 元素:
<div @onclick="HandleDivClick">
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
<input value=@data @onkeypress="ProcessKeyPress" @onkeypress:preventDefault />
</div>
@code {
private async Task HandleDivClick()
{
await JS.InvokeVoidAsync("alert", "Div click");
}
private async Task ProcessKeyPress(KeyboardEventArgs e)
{
// Omitted for brevity
}
private int currentCount = 0;
private void IncrementCount(MouseEventArgs e)
{
// Omitted for brevity
}
}
當應用程式執行時,如果使用者按下了 <div> 元素所佔用區域中的任何元素 (或空白處),HandleDivClick
方法就會執行並顯示訊息。 如果使用者選取了 Click me
按鈕,則 IncrementCount
方法就會執行,接著執行 HandleDivClick
;@onclick
事件會傳播到整個 DOM 樹狀結構。 如果 <div> 屬於另一個同時在處理 @onclick
事件的元素,該事件處理常式也會執行,依此類推,直到 DOM 樹狀結構的根目錄。 您可以使用事件的 stopPropagation
屬性,讓事件數向上激增的過程縮短,如此處所示:
<div @onclick="HandleDivClick">
<button class="btn btn-primary" @onclick="IncrementCount" @onclick:stopPropagation>Click me</button>
<!-- Omitted for brevity -->
</div>
使用 EventCallback 處理跨元件的事件
Blazor 頁面可以包含一或多個 Blazor 元件,且可以用巢狀結構設置這些元件,並建立上/下層的關聯性。 子元件中的事件可以用 EventCallback
來觸發父元件中的事件處理常式方法。 回呼會參考父元件中的方法。 而子元件可以叫用回呼來執行該方法。 此機制與 C# 應用程式中使用 delegate
來參考方法的機制類似。
回呼只可以使用單一參數。 EventCallback
為泛型型別。 類型參數可指定要傳遞給回呼的引數類型。
例如,請設想下列情境。 您想要建立名為 TextDisplay
的元件,讓使用者能夠輸入輸入字串,並以某種方式轉換該字串 (例如轉換成大寫、小寫、混合大小寫、篩選字串中的字元,或執行其他類型的轉換)。 不過,當您為 TextDisplay
元件撰寫程式碼時,並無法得知轉換流程,只是想要讓此作業遵從另一個元件的指令。 下列程式碼顯示 TextDisplay
元件。 該元件會以 <input> 元素的形式提供輸入字串,讓使用者能夠輸入文字值。
@* TextDisplay component *@
@using WebApplication.Data;
<p>Enter text:</p>
<input @onkeypress="HandleKeyPress" value="@data" />
@code {
[Parameter]
public EventCallback<KeyTransformation> OnKeyPressCallback { get; set; }
private string data;
private async Task HandleKeyPress(KeyboardEventArgs e)
{
KeyTransformation t = new KeyTransformation() { Key = e.Key };
await OnKeyPressCallback.InvokeAsync(t);
data += t.TransformedKey;
}
}
TextDisplay
元件會使用名為 OnKeyPressCallback
的 EventCallback
物件。 HandleKeypress
方法中的程式碼會叫用回呼。 每次有人按下按鍵時,@onkeypress
事件處理常式就會執行,並呼叫 HandleKeypress
方法。 HandleKeypress
方法會利用使用者按下的按鍵來建立 KeyTransformation
物件,並將這個物件當作參數傳遞給回呼。 KeyTransformation
類型是具有兩個欄位的簡單類別:
namespace WebApplication.Data
{
public class KeyTransformation
{
public string Key { get; set; }
public string TransformedKey { get; set; }
}
}
key
欄位包含了使用者輸入的值,而若按鍵已經過處理,則 TransformedKey
欄位會保留該按鍵轉換後的值。
在此範例中,EventCallback
物件是元件參數,在建立元件時會提供物件的值。 此動作是由另一個名為 TextTransformer
的元件所執行:
@page "/texttransformer"
@using WebApplication.Data;
<h1>Text Transformer - Parent</h1>
<TextDisplay OnKeypressCallback="@TransformText" />
@code {
private void TransformText(KeyTransformation k)
{
k.TransformedKey = k.Key.ToUpper();
}
}
TextTransformer
元件是會建立 TextDisplay
元件執行個體的 Blazor 頁面。 該元件會參考此頁面程式碼區段中的 TransformText
方法,填入 OnKeypressCallback
參數。 TransformText
方法會使用做為其方法引數的 KeyTransformation
物件,並使用 Key
屬性中所找到的值,轉換成大寫後填入 TransformedKey
屬性。 下圖說明了當使用者在 TextTransformer
頁面所顯示的 TextDisplay
元件中,於 <input> 欄位中輸入值時的控制項流程:
這種方法的優點是,您可以搭配能提供 OnKeypressCallback
參數回呼的任何頁面來使用 TextDisplay
元件。 顯示與處理之間有完整的分隔。 您可以為符合 TextDisplay
元件中 EventCallback
參數簽章的任何其他回呼,切換 TransformText
方法。
如果回呼是以適當的 EventArgs
參數輸入,您可以直接將回呼連結到事件處理常式,而無須使用中繼方法。 例如,子元件可能會參考可處理滑鼠事件 (例如 @onclick
) 的回呼,如下所示:
<button @onclick="OnClickCallback">
Click me!
</button>
@code {
[Parameter]
public EventCallback<MouseEventArgs> OnClickCallback { get; set; }
}
在此情況下,EventCallback
會使用 MouseEventArgs
類型參數,因此可將其指定為 @onclick
事件的處理常式。