Saltar al contenido

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript... y lo que venga ;)

17 años online

el blog de José M. Aguilar

Inicio El autor Contactar

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web
ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript...

¡Microsoft MVP!
martes, 18 de mayo de 2021
Blazor

Durante la implementación de páginas o componentes Blazor en archivos .razor, es relativamente frecuente encontrarse con casos en los que nos interesa reutilizar un bloque de código Razor en más de un punto. 

Por ejemplo, observad el siguiente ejemplo:

<h1>Todo list</h1>
<h2>Pending tasks</h2>
<ul>
    @foreach (TodoItem item in TodoItems.Where(i=>!i.IsDone).OrderBy(i=>i.Priority))
    {
        <li>@item.Task, owned by @item.Owner and created at @item.CreatedAt</li>
    }
</ul>
<h2>Finished tasks</h2>
<ul>
    @foreach (TodoItem item in TodoItems.Where(i=>i.IsDone).OrderBy(i=>i.DateFinished))
    {
        <li>@item.Task, owned by @item.Owner and created at @item.CreatedAt</li>
    }
</ul>

En este componente, podemos ver claramente que estamos repitiendo los dos bloques de código encargados de mostrar los elementos de cada una de las listas, por lo que, si en el futuro quisiéramos cambiar la forma de mostrar un TodoItem, tendríamos que modificar el interior de los dos bloques. Es frecuente en estos casos optar por crear un nuevo componente que se encargue de ello, por ejemplo, llamado TodoListItem:

<li>@Item.Task, owned by @Item.Owner and created at @Item.CreatedAt</li>
@code {
    [Parameter]
    public TodoItem Item { get; set;}
}

De esta forma ya tendremos el código de renderización del TodoItem centralizado y podremos simplificar el bloque anterior eliminando la duplicidad:

<h1>Todo list</h1>
<h2>Pending tasks</h2>
<ul>
    @foreach (TodoItem item in TodoItems.Where(i=>!i.IsDone).OrderBy(i=>i.Priority))
    {
        <TodoListItem Item="item" />
    }
</ul>
<h2>Finished tasks</h2>
<ul>
    @foreach (TodoItem item in TodoItems.Where(i=>i.IsDone).OrderBy(i=>i.DateFinished))
    {
        <TodoListItem Item="item" />
    }
</ul>

Aunque conceptualmente la solución que hemos implementado es correcta, introduce un problema en nuestra aplicación: por el mero hecho de querer evitar la duplicación de código, estamos introduciendo en la página un número indeterminado de componentes, lo cual podría afectar drásticamente a su rendimiento.

Por llevarlo al extremo, imaginad que esas listas tienen miles de elementos. En este caso, en nuestra página estaríamos introduciendo miles de componentes, con lo que esto implica:

  • Deberían instanciarse miles de componentes (objetos).
  • Deberían ejecutarse los eventos del ciclo de vida de cada componente al crearlos, inicializarlos, renderizarlos, etc.
  • Mientras se encuentren en la página cada componente ocuparía memoria, ya sea en cliente (Blazor WebAssembly) o en servidor (Blazor Server).

Esto podría llegar incluso a hacer una página inutilizable, por lo que es importante disponer de otros métodos para crear y reutilizar bloques de código HTML sin necesidad de crear componentes. Esta es una de las utilidades de los render fragments.

Introducing Render Fragments

Los fragmentos de renderización, o render fragments, son delegados reutilizables que permiten introducir contenido en páginas u otros componentes Blazor. Los hemos visto ya algunas veces por aquí, como cuando echamos un vistazo a los componentes con cuerpo y componentes genéricos.

Estos delegados reciben como parámetro un objeto de tipo RenderTreeBuilder, usado para configurar el contenido que deseamos insertar en la página cuando son invocados. Por ejemplo, el siguiente código muestra cómo crear un render fragment que muestra la hora actual, y cómo puede ser invocado desde el cuerpo de una página Blazor:

@page "/time"
<p>La hora actual es: @CurrentTime</p>
<p>Y vuelvo a repetirla: @CurrentTime</p>
...
@code {
    private RenderFragment CurrentTime = builder =>
    {
        builder.AddMarkupContent(1, "<time>" + DateTime.Now + "</time>");
    };
}

Podéis leer más sobre la generación de componentes usando RenderTreeBuilder en este post de Chris Sainty.

El código anterior es lo suficientemente claro y fácil de escribir, pero aún podemos mejorarlo. Gracias a la magia del tooling de Blazor, es posible introducir código Razor directamente en el cuerpo del delegado, por lo que podríamos simplificarlo de esta forma:

@code {
    private RenderFragment CurrentTime = __builder =>
    {
        <time>@DateTime.Now</time>
    };
}

Por convención, el parámetro RenderTreeBuilder del delegado debe llamarse __builder. En caso contrario, fallará en compilación.

Estos miembros pueden ser también estáticos, por lo que podrían ser compartidos entre distintos componentes. Por ejemplo, si creásemos un archivo llamado Utils.razor con el siguiente código, todos los componentes podrían usar nuestro render fragment simplemente haciendo referencia a él a través su clase (@Utils.CurrentTime):

@* Archivo Utils.razor *@
@code {
    public static RenderFragment CurrentTime = __builder =>
    {
        <time>@DateTime.Now</time>
    };
}

Los ejemplos anteriores eran bastante sencillos, porque el contenido no dependía de ningún valor externo, pero también es posible enviar al delegado datos para que los utilice a la hora de componer la salida.

Por ejemplo, volviendo al ejemplo con el que comenzamos este post, tiene bastante sentido enviar al delegado un objeto de tipo TodoItem para generar la descripción de cada tarea, para lo que utilizaremos en esta ocasión el tipo RenderFragment<TodoItem>:

@code {
    private RenderFragment<TodoItem> ItemView = item => __builder =>
    {
        <li>
            @item.Task, owned by @item.Owner and created at @item.CreatedAt
        </li>
    };
}

La sintaxis es algo más compleja, pero básicamente se trata de un delegado que acepta un parámetro de tipo TodoItem, y retorna otro delegado, que ya es el que recibe el RenderTreeBuilder y define el contenido a retornar.

Para utilizar este fragmento parametrizado bastaría con suministrarle el valor en el momento de realizar la llamada:

<h1>Todo list</h1>
<h2>Pending tasks</h2>
<ul>
    @foreach (TodoItem item in TodoItems.Where(i=>!i.IsDone).OrderBy(i=>i.Priority))
    {
        @ItemView(item)
    }
</ul>
<h2>Finished tasks</h2>
<ul>
    @foreach (TodoItem item in TodoItems.Where(i=>i.IsDone).OrderBy(i=>i.DateFinished))
    {
        @ItemView(item)
    }
</ul>

¡Y esto es todo! Espero que os haya resultado interesante y os sea de utilidad para mejorar vuestras aplicaciones Blazor :)

Publicado en: www.variablenotfound.com.

Aún no hay comentarios, ¡sé el primero!