使用 Blazor 事件处理程序将 C# 代码附加到 DOM 事件
- 5 分钟
大多数 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
事件在参数中传递有关用户单击哪个按钮的信息,或者他们是否在单击按钮的同时按下 Ctrl 或 MouseEventArgs
等按钮。 调用方法时无需提供此参数;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 处理此类事件。
Important
可以使用事件处理程序中的 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
}
}
Note
有关在 C# 中创建异步方法的详细信息,请阅读 异步编程方案。
使用事件将焦点设置为 DOM 元素
在 HTML 页面上,用户可以在元素之间按 Tab 键,焦点自然会按照 HTML 元素出现在页面上的顺序移动。 在某些情况下,可能需要替代此序列并强制用户访问特定元素。
执行此任务的最简单方法是使用 FocusAsync
方法。 此方法是对象的 ElementReference
实例方法。
ElementReference
应会引用要设置焦点的项。 使用 @ref
属性指定元素引用,并在代码中创建一个同名的 C# 对象。
在下面的示例中,@onclick
button< 元素的 > 事件处理程序将焦点设置到 <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";
}
下图显示了用户选择按钮时的结果:
Note
应用应仅出于特定原因(例如要求用户在出错后修改输入)将焦点指向特定控件。 不要使用焦点来强制用户按固定顺序浏览页面上的元素。 此设计可能令人沮丧,用户可能想要重新访问元素以更改其输入。
编写内联事件处理程序
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;
}
Note
有关 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++;
}
}
}
Note
此示例使用 JavaScript alert
函数来显示消息,因为 Blazor 中没有等效函数。 使用 JavaScript 互操作从 Blazor 代码调用 JavaScript。 将在单独的模块主题中详细介绍此方法。
替代事件的默认 DOM 操作
多个 DOM 事件具有在事件发生时运行的默认操作,而无论是否有可用于该事件的事件处理程序。 例如,@onkeypress
input< 元素的 > 事件始终显示与用户按下的键对应的字符,然后会处理该按键操作。 在下一个示例中,@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 是另一个也处理 < 事件的元素的一部分,那么该事件处理程序也将运行到 DOM 树的根部等>@onclick
。 可以使用事件的 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
组件使用名为 EventCallback
的 OnKeyPressCallback
对象。
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 页面。 它使用对页面代码部分中的 OnKeypressCallback
方法的引用填充 TransformText
参数。
TransformText
方法将提供的 KeyTransformation
对象作为其参数,并用转换为大写的 TransformedKey
属性中的值来填充 Key
属性。 下图说明了当用户在 < 页面显示的 > 组件的 TextDisplay
inputTextTransformer
字段中输入值时的控制流:
此方法的优点在于,可以将 TextDisplay
组件用于为 OnKeypressCallback
参数提供回调的任何页面。 显示和处理之间完全分离。 可以为与 TransformText
组件中的 EventCallback
参数的签名匹配的任何其他回调切换 TextDisplay
方法。
如果使用适当的 EventArgs
参数键入回调,则可以将回调直接连接到事件处理程序,而无需使用中间方法。 例如,子组件可能会引用一个回调,该回调可以处理 @onclick
等鼠标事件,如下所示:
<button @onclick="OnClickCallback">
Click me!
</button>
@code {
[Parameter]
public EventCallback<MouseEventArgs> OnClickCallback { get; set; }
}
在这种情况下,EventCallback
采用 MouseEventArgs
类型参数,因此你可以将其指定为 @onclick
事件的处理程序。