Asociación de código de C# a eventos de DOM con controladores de eventos de Blazor

Completado

La mayoría de los elementos HTML exponen eventos que se desencadenan cuando sucede algo significativo, como cuando una página termina de cargarse, el usuario hace clic en un botón o cambia el contenido de un elemento HTML. Una aplicación puede controlar un evento de varias maneras:

  • La aplicación puede omitir el evento.
  • La aplicación puede ejecutar un controlador de eventos escrito en JavaScript para procesar el evento.
  • La aplicación puede ejecutar un controlador de eventos de Blazor escrito en C# para procesar el evento.

En esta unidad, verá con detalle la tercera opción; cómo crear un controlador de eventos de Blazor en C# para procesar un evento.

Control de un evento con Blazor y C#

Cada elemento del marcado HTML de una aplicación de Blazor admite muchos eventos. La mayoría de estos eventos corresponden a los eventos de DOM disponibles en aplicaciones web normales, pero también puede crear eventos definidos por el usuario que se desencadenen mediante la escritura de código. Para capturar un evento con Blazor, escriba un método de C# que controle el evento y, a continuación, enlace el evento al método con una directiva de Blazor. Para un evento de DOM, la directiva de Blazor comparte el mismo nombre que el evento HTML equivalente, como @onkeydown o @onfocus. Por ejemplo, la aplicación de muestra generada mediante la aplicación Blazor Server contiene el código que se muestra a continuación en la página Counter.razor. Esta página muestra un botón. Cuando el usuario selecciona el botón, el evento @onclick desencadena el método IncrementCount que incrementa un contador que indica cuántas veces se ha hecho clic en el botón. El elemento <p> de la página muestra el valor de la variable de contador:

@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++;
    }
}

Muchos métodos de un controlador de eventos toman un parámetro que proporciona información contextual adicional. Este parámetro se denomina parámetro EventArgs. Por ejemplo, el evento @onclick pasa información sobre en qué botón hizo clic el usuario, o si presionó un botón, como CTRL o Alt al mismo tiempo que hizo clic en el botón, en un parámetro MouseEventArgs. No es necesario proporcionar este parámetro al llamar al método, el runtime de Blazor lo agrega automáticamente. Puede consultar este parámetro en el controlador de eventos. El código siguiente incrementa el contador que se muestra en el ejemplo anterior en cinco si el usuario presiona la tecla CTRL al mismo tiempo que hace clic en el botón:

@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++;
        }
    }
}

Otros eventos proporcionan parámetros EventArgs diferentes. Por ejemplo, el evento @onkeypress pasa un parámetro KeyboardEventArgs que indica qué tecla ha presionado el usuario. En el caso de los eventos de DOM, si no necesita esta información, simplemente puede omitir el parámetro EventArgs del método de control de eventos.

Información sobre el control de eventos en JavaScript frente al control de eventos con Blazor

Una aplicación web tradicional usa JavaScript para capturar y procesar eventos. Cree una función como parte de un elemento <script> de HTML y, después, establezca que se llame a esa función cuando se produzca el evento. En comparación con el ejemplo de Blazor anterior, el código siguiente muestra un fragmento de una página HTML que incrementa un valor y muestra el resultado cada vez que el usuario selecciona el botón Hacer clic aquí. El código aprovecha la biblioteca jQuery para acceder a 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>

Además de las diferencias sintácticas de las dos versiones del controlador de eventos, debería tener en cuenta las siguientes diferencias funcionales:

  • JavaScript no agrega un signo @ como prefijo del nombre del evento; no es una directiva de Blazor.
  • En el código de Blazor, el nombre del método de control de eventos se especifica cuando se adjunta a un evento. En JavaScript, se escribe una instrucción que llama al método de control de eventos; se especifican los corchetes y los parámetros obligatorios.
  • Lo más importante es que el controlador de eventos de JavaScript se ejecuta en el explorador, en el cliente. Si va a compilar una aplicación Blazor Server, el controlador de eventos de Blazor se ejecuta en el servidor, y solo actualiza el explorador con los cambios realizados en la interfaz de usuario cuando se completa el controlador de eventos. Además, el mecanismo de Blazor permite que un controlador de eventos acceda a datos estáticos que se comparten entre sesiones, pero el modelo de JavaScript no. Sin embargo, controlar algunos eventos frecuentes, como @onmousemove, puede hacer que la interfaz de usuario se vuelva lenta, ya que requieren un recorrido de ida y vuelta de red al servidor. Es posible que prefiera controlar eventos como estos en el explorador, mediante JavaScript.

Importante

Puede manipular DOM mediante código de JavaScript desde un controlador de eventos, así como con código de Blazor en C#. Sin embargo, Blazor mantiene su propia copia de DOM, que se usa para actualizar la interfaz de usuario cuando sea necesario. Si usa código de JavaScript y Blazor para cambiar los mismos elementos de DOM, corre el riesgo de dañar DOM y, posiblemente, poner en peligro la privacidad y la seguridad de los datos en la aplicación web.

Control asincrónico de eventos

De manera predeterminada, los controladores de eventos de Blazor son sincrónicos. Si un controlador de eventos realiza una operación de ejecución potencialmente larga, como llamar a un servicio web, el subproceso en el que se ejecuta el controlador de eventos se bloqueará hasta que se complete la operación. Esto puede provocar una respuesta deficiente en la interfaz de usuario. Para evitar esto, puede designar un método del controlador de eventos como asincrónico. Utilice la palabra clave async de C#. El método debe devolver un objeto Task. A continuación, puede usar el operador await dentro del método del controlador de eventos para iniciar cualquier tarea de larga duración en un subproceso independiente y liberar el subproceso actual para otro trabajo. Cuando se completa una tarea de larga duración, se reanuda el controlador de eventos. El controlador de eventos de ejemplo a continuación ejecuta un método que consume mucho tiempo de forma asincrónica:

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

Nota:

Para obtener información detallada sobre cómo crear métodos asincrónicos en C#, lea Escenarios de programación asincrónica.

Uso de un evento para establecer el foco en un elemento de DOM

En una página HTML, el usuario puede tabular entre los elementos, y el foco viaja de forma natural en el orden en que los elementos HTML aparecen en la página. En ocasiones, es posible que tenga que invalidar esta secuencia y obligar al usuario a visitar un elemento específico.

La manera más sencilla de hacer esto es usar el método FocusAsync. Este es un método de instancia de un objeto ElementReference. ElementReference debe hacer referencia al elemento en el que desea establecer el foco. Designe una referencia de elemento con el atributo @ref y cree un objeto de C# con el mismo nombre en el código.

En el ejemplo siguiente, el controlador de eventos @onclick para el elemento <button> establece el foco en el elemento <input>. El controlador de eventos @onfocus del elemento <input> muestra el mensaje "Received focus" (Foco recibido) cuando el elemento recibe el foco. Se hace referencia al elemento <input> mediante la variable InputField en el código:

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

En la imagen siguiente se muestra el resultado cuando el usuario selecciona el botón:

Captura de pantalla de la página web después de que el usuario ha hecho clic en el botón para establecer el foco en el elemento de entrada.

Nota:

Una aplicación solo debe dirigir el foco a un control específico por un motivo específico, como pedir al usuario que modifique la entrada después de un error. No use el foco para obligar al usuario a navegar por los elementos de una página en un orden fijo; esto puede resultar muy frustrante para el usuario, quien puede querer volver a visitar los elementos para cambiar su entrada.

Escritura de controladores de eventos en línea

C# admite expresiones lambda. Una expresión lambda le permite crear una función anónima. Una expresión lambda es útil si tiene un controlador de eventos simple que no es necesario reutilizar en otra parte de una página o componente. En el ejemplo de recuento de clics inicial que se muestra al principio de esta unidad, puede quitar el método IncrementCount y, en su lugar, reemplazar la llamada de método por una expresión lambda que realice la misma tarea:

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

Nota:

Para obtener más información sobre cómo funcionan las expresiones lambda, lea Expresiones lambda y funciones anónimas.

Este enfoque también es útil si desea proporcionar otros argumentos para un método de control de eventos. En el ejemplo siguiente, el método HandleClick toma un parámetro MouseEventArgs de la misma manera que un controlador de eventos de clic normal, pero también acepta un parámetro de cadena. El método procesa el evento de clic como antes, pero también muestra el mensaje si el usuario ha presionado la tecla CTRL. La expresión lambda llama al método HandleCLick, pasando el parámetro MouseEventArgs (mouseEvent) y una cadena.

@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++;
        }
    }
}

Nota:

En este ejemplo se usa la función alert de JavaScript para mostrar el mensaje, ya que no hay ninguna función equivalente en Blazor. La interoperabilidad de JavaScript se usa para llamar a JavaScript desde código de Blazor. Los detalles de esta técnica son tema de otro módulo.

Invalidación de acciones de DOM predeterminadas para eventos

Varios eventos de DOM tienen acciones predeterminadas que se ejecutan cuando se produce el evento, independientemente de si hay un controlador de eventos disponible para ese evento. Por ejemplo, el evento @onkeypress de un elemento <input> siempre muestra el carácter que corresponde a la tecla que el usuario ha presionado y controla la pulsación de la tecla. En el ejemplo siguiente, se usa el evento @onkeypress para convertir la entrada del usuario a mayúsculas. Además, si el usuario escribe un carácter @, el controlador de eventos muestra una alerta:

<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();
        }
    }
}

Si ejecuta este código y presiona la tecla @, se mostrará la alerta, pero el carácter @ también se agregará a la entrada. La adición del carácter @ es la acción predeterminada del evento.

Captura de pantalla de la entrada del usuario que muestra el carácter @.

Si desea evitar que este carácter aparezca en el cuadro de entrada, puede invalidar la acción predeterminada con el atributo preventDefault del evento, de la siguiente manera:

<input value=@data @onkeypress="ProcessKeyPress" @onkeypress:preventDefault />

El evento aún se activará, pero solo se realizarán las acciones definidas por el controlador de eventos.

Algunos eventos de un elemento secundario de DOM pueden desencadenar eventos en sus elementos primarios. En el ejemplo siguiente, el elemento <div> contiene un controlador de eventos @onclick. El elemento <button> dentro de <div> tiene su propio controlador de eventos @onclick. Además, el elemento <div> contiene un elemento <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
    }
}

Cuando se ejecuta la aplicación, si el usuario hace clic en cualquier elemento (o espacio vacío) en el área ocupada por el elemento <div>, se ejecutará el método HandleDivClick y mostrará un mensaje. Si el usuario selecciona el botón Click me, se ejecutará el método IncrementCount, seguido de HandleDivClick; el evento @onclick propaga el árbol de DOM. Si <div> formaba parte de otro elemento que también ha controlado el evento @onclick, ese controlador de eventos también se ejecutaría, y así sucesivamente, en la raíz del árbol de DOM. Puede reducir esta proliferación ascendente de eventos con el atributo stopPropagation de un evento, como se muestra aquí:

<div @onclick="HandleDivClick">
    <button class="btn btn-primary" @onclick="IncrementCount" @onclick:stopPropagation>Click me</button>
    <!-- Omitted for brevity -->
</div>

Uso de EventCallback para controlar eventos entre componentes

Una página de Blazor puede contener uno o varios componentes de Blazor, y los componentes se pueden anidar en una relación de elementos primarios y secundarios. Un evento de un componente secundario puede desencadenar un método del controlador de eventos en un componente primario mediante EventCallback. Una devolución de llamada hace referencia a un método en el componente primario. Para ejecutar el método, el componente secundario puede invocar la devolución de llamada. Este mecanismo es similar a usar delegate para hacer referencia a un método en una aplicación de C#.

Una devolución de llamada puede tomar un único parámetro. EventCallback es un tipo genérico. El parámetro de tipo especifica el tipo del argumento que se pasa a la devolución de llamada.

Como ejemplo, observe el siguiente caso. Quiere crear un componente denominado TextDisplay que permita al usuario escribir una cadena de entrada y transformar esa cadena de alguna manera; es posible que desee convertirla a mayúsculas, minúsculas, mayúsculas y minúsculas, filtrar caracteres a partir de ella o realizar algún otro tipo de transformación. Sin embargo, cuando escribe el código para el componente TextDisplay no sabe cuál será el proceso de transformación y, en su lugar, quiere aplazar esta operación hasta otro componente. El código siguiente muestra el componente TextDisplay. Proporciona la cadena de entrada en forma de un elemento <input> que permite al usuario escribir un valor de texto.

@* 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;
    }
}

El componente TextDisplay usa un objeto EventCallback denominado OnKeyPressCallback. El código del método HandleKeypress invoca la devolución de llamada. El controlador de eventos @onkeypress se ejecuta cada vez que se presiona una tecla y llama al método HandleKeypress. El método HandleKeypress crea un objeto KeyTransformation con la tecla que el usuario presionó y pasa este objeto como parámetro a la devolución de llamada. El tipo KeyTransformation es una clase simple con dos campos:

namespace WebApplication.Data
{
    public class KeyTransformation
    {
        public string Key { get; set; }
        public string TransformedKey { get; set; }
    }
}

El campo key contiene el valor especificado por el usuario, y el campo TransformedKey contendrá el valor transformado de la clave cuando se haya procesado.

En este ejemplo, el objeto EventCallback es un parámetro de componente, y su valor se proporciona cuando se crea el componente. Otro componente, denominado TextTransformer, realiza esta acción:

@page "/texttransformer"
@using WebApplication.Data;

<h1>Text Transformer - Parent</h1>

<TextDisplay OnKeypressCallback="@TransformText" />

@code {
    private void TransformText(KeyTransformation k)
    {
        k.TransformedKey = k.Key.ToUpper();
    }
}

El componente TextTransformer es una página de Blazor que crea una instancia del componente TextDisplay. Rellena el parámetro OnKeypressCallback con una referencia al método TransformText en la sección de código de la página. El método TransformText toma el objeto KeyTransformation proporcionado como argumento y rellena la propiedad TransformedKey con el valor encontrado en la propiedad Key convertida a mayúsculas. En el diagrama siguiente se muestra el flujo de control cuando un usuario escribe un valor en el campo <input> del componente TextDisplay que muestra la página TextTransformer:

Diagrama del flujo de control con EventCallback en un componente secundario.

La ventaja de este enfoque es que se puede usar el componente TextDisplay con cualquier página que proporcione una devolución de llamada para el parámetro OnKeypressCallback. Hay una separación total entre la visualización y el procesamiento. Puede cambiar el método TransformText para cualquier otra devolución de llamada que coincida con la firma del parámetro EventCallback en el componente TextDisplay.

Puede conectar una devolución de llamada a un controlador de eventos directamente sin usar un método intermedio si la devolución de llamada se escribe con el parámetro EventArgs adecuado. Por ejemplo, un componente secundario puede hacer referencia a una devolución de llamada que puede controlar eventos del mouse, como @onclick, de este modo:

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

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

En este caso, EventCallback toma un parámetro de tipo MouseEventArgs, por lo que se puede especificar como controlador para el evento @onclick.