使用 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 参数中单击按钮的同时按住了 CtrlAlt 等按钮的信息。 调用方法时无需提供此参数;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 代码以及使用 C# Blazor 代码来操作 DOM。 但是,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 事件处理程序在元素获得焦点时显示消息“已接收到焦点”<>。 input 元素是通过代码中的 InputField 变量引用的<>:

<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";
    }

下图显示了用户选择按钮时的结果:

Screenshot of the web page after the user has clicked the button to set the focus to the input element.

注意

应用应仅出于特定原因(例如要求用户在出错后修改输入)将焦点指向特定控件。 请勿使用聚焦来强制用户按照固定顺序浏览页面上的元素;对于可能需要再次访问元素以更改其输入的用户来说,这可能会让他们感到非常苦恼。

编写内联事件处理程序

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 参数,但它也接受字符串参数。 该方法照常处理单击事件,但还会在用户按下 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 互操作从 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();
        }
    }
}

如果运行此代码并按了 @ 键,将显示警报,但 @ 字符也将添加到输入中。 添加 @ 字符是事件的默认操作。

Screenshot of the user input showing the @ character.

如果要禁止该字符出现在输入框中,可以使用事件的 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 触发父组件中的事件处理程序方法。 回调将引用父组件中的方法。 子组件可以通过调用回调来运行该方法。 此机制类似于使用 delegate 来引用 C# 应用程序中的方法。

回调可采用单个参数。 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 组件使用名为 OnKeyPressCallbackEventCallback 对象。 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> 字段中输入值时的控制流:

Diagram of the flow of control with an EventCallback in a child component.

此方法的优点在于,可以将 TextDisplay 组件用于为 OnKeypressCallback 参数提供回调的任何页面。 显示和处理之间完全分离。 可以为与 TextDisplay 组件中的 EventCallback 参数的签名匹配的任何其他回调切换 TransformText 方法。

如果使用适当的 EventArgs 参数键入回调,则可以将回调直接连接到事件处理程序,而无需使用中间方法。 例如,子组件可能会引用一个回调,该回调可以处理 @onclick 等鼠标事件,如下所示:

<button @onclick="OnClickCallback">
    Click me!
</button>

@code {
    [Parameter]
    public EventCallback<MouseEventArgs> OnClickCallback { get; set; }
}

在这种情况下,EventCallback 采用 MouseEventArgs 类型参数,因此你可以将其指定为 @onclick 事件的处理程序。

知识检查

1.

应该使用哪种功能在 Blazor 组件之间传递事件?

2.

可以使用哪种方法来替代 HTML DOM 元素中的默认操作?