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

18 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, 25 de junio de 2024
Blazor

Blazor ha venido en ASP.NET Core 8 cargadito de novedades, aunque probablemente la más destacable sea la introducción de las Blazor Web Apps como modelo de proyecto que unifica los distintos modos de renderizado de componentes. Aunque se trata de un cambio positivo, la realidad es que ha complicado algunas cosas que antes, con unos modelos de proyecto más sencillos, eran más fáciles de implementar.

Un ejemplo claro lo tenemos en las páginas de error 404 (not found): con este nuevo modelo unificado no hay una fórmula trivial o integrada de serie en el framework para implementar esta funcionalidad, tan habitual en nuestras aplicaciones.

En este post vamos a ver un posible enfoque para conseguir que si un usuario introduce una ruta inexistente en el navegador o bien pulsa un enlace interno que no exista, podamos mostrarle una página de error 404 totalmente personalizada, implementada como componente Blazor, e integrada en nuestra Blazor Web App.

Solucionando el problema en el lado servidor

Para dar con una solución apropiada, lo primero que tenemos que hacer es entender el problema, y vamos a comenzar por el lado servidor, donde se procesan los componentes Blazor cuando renderizamos con SSR o en la prerenderización inicial de componentes interactivos.

Comencemos desde el principio. Durante el arranque de la aplicación, cuando hacemos la llamada a app.MapRazorComponents<App>() en Program.cs las páginas Blazor, es decir, los componentes decorados con directivas @page) son mapeados como endpoints en el sistema de routing de ASP.NET Core.

Es decir, el sistema de routing de ASP.NET Core conocerá desde primer momento las páginas definidas en nuestra aplicación, y las rutas de acceso a cada una de ellas. Aparte, tendrá también información de otros endpoints definidos, como controladores MVC o minimal APIs.

Cuando se recibe una petición directa (por ejemplo, si introducimos la URL en el navegador), la petición entrará por el pipeline y llegará al sistema de routing, quien determina si existe un endpoint que pueda procesarla:

  • En caso afirmativo, delegará la ejecución al manejador del endpoint correspondiente, que podría ser un componente Blazor, un controlador MVC, una página Razor, una minimal API o cualquier otro tipo de handler.
  • En caso negativo, si no se ha encontrado ningún handler asociado a la ruta, se retornará un error 404 al cliente.

Por defecto, el framework retorna una página 404 vacía, aunque tenemos varias fórmulas para controlar este escenario.

Una muy sencilla de aplicar sería usar alguno de los extensores app.MapFallback(), que permite especificar el manejador que procesará la petición en caso de no existir un endpoint específico para la misma. Por ejemplo, el siguiente código hace que cualquier petición cuya ruta no coincida con un endpoint conocido retorne el error 404 con el contenido del archivo notfound.html:

...
app.MapFallbackToFile("/notfound.html");
app.Run(); // Arranca la aplicación

Ojo, recordad que esto requiere que en pipeline hayamos registrado también el middleware StaticFilesMiddleware para que el archivo estático pueda ser servido.

Existen extensores similares, que permiten derivar el control a controladores (MapFallbackToController()) o páginas Razor (MapFallbackToPage()), entre otros.

Otra posibilidad es utilizar el middleware StatusCodePagesMiddleware, que permite tomar el control antes de retornar el resultado de la petición y procesarla de forma alternativa. Por ejemplo, el siguiente código consigue lo mismo que el anterior, es decir, hace que el resultado de cualquier petición que retorne un código de estado 404 sea el contenido del archivo notfound.html:

...
var app = builder.Build();
app.UseStatusCodePages(async ctx =>
{
    if (ctx.HttpContext.Response.StatusCode == 404)
    {
        ctx.HttpContext.SetEndpoint(null);
        ctx.HttpContext.Request.Path = "/notfound.html";
        await ctx.Next(ctx.HttpContext);
    }
});
app.UseRouting(); // Aseguramos que el sistema de routing se introduce 
                  // después de StatusCodePagesMiddleware
...

El uso de StatusCodePagesMiddleware es más flexible que los extensores MapFallback(), porque permite decidir el comportamiento en función del código de estado retornado, y también porque permite volver a introducir la petición en el pipeline, previamente retocada, para que sea procesada por otros middlewares.

En el caso anterior, cuando detectamos que el resultado de la petición ha sido un error 400, reescribimos la ruta de la misma a "/notfound.html" y volvemos a introducirla en el pipeline. El resultado de su proceso es el que será retornado definitivamente al cliente.

La ventaja de este enfoque es que, igual que estamos indicando la ruta de un archivo .html estático, podríamos especificar la de una acción MVC, una página Razor o incluso una página Blazor.

Por ejemplo, imaginad que tenemos una página Blazor llamada NotFoundPage.razor asociada a la ruta /notfound mediante la directiva @page, como la siguiente:

@page "/notfound"
<h3>Page not found 🤔</h3>
<p>The specified page could not be found.</p>

Si queremos que ésta sea mostrada en caso de error 404, podríamos hacer lo siguiente:

...
var app = builder.Build();
app.UseStatusCodePages(async ctx =>
{
    if (ctx.HttpContext.Response.StatusCode == 404)
    {
        ctx.HttpContext.SetEndpoint(null);
        ctx.HttpContext.Request.Path = "/notfound";
        await ctx.Next(ctx.HttpContext);
    }
});
app.UseRouting(); // Aseguramos que el sistema de routing se introduce 
                  // después de StatusCodePagesMiddleware
...

Al ejecutar la aplicación, podremos ver que funciona correctamente, siempre que los componentes no sean interactivos, es decir, usen Server-Side Rendering para generar su contenido:

Blazor Web App sin componentes interactivos, con página 'Not found' personalizada

Pero si estamos renderizando componentes interactivos, la cosa se complica un poco...

Solucionando el problema en el lado cliente

Cuando estamos hablando de componentes interactivos (ya sea en cliente o en servidor), el sistema de routing de ASP.NET Core ya no entra en juego, porque no estaremos procesando peticiones directas, sino cambios de página gestionados por el sistema de routing de Blazor.

Fijaos en el vídeo siguiente, de una aplicación con componentes interactivos, donde podréis observar un curioso efecto: cuando navegamos hacia una ruta que no existe, en primer lugar visualizaremos durante unos segundos nuestra página de error personalizada, pero desaparecerá algunos segundos después, siendo sustituida por una página en blanco con el texto "Not found":

Blazor Web App interactiva, cuando accedemos a una ruta inexistente, la página de error es sustituida por un contenido en blanco, con el texto "Not found"

Esto ocurre porque, cuando el sistema de routing de Blazor (gestionado por el componente <Router>) no encuentra la página al que queremos navegar, realiza una petición para intentar obtener el contenido de forma externa. Esta petición es procesada por el middleware StatusCodePagesMiddleware que hemos establecido anteriormente en el pipeline, retornando el HTML de la página de error 404 personalizada. Por este motivo, podemos verla en el navegador, al menos durante unos segundos.

Sin embargo, al cargar la página, dado que estamos usando componentes interactivos, se ponen en marcha los mecanismos de interactividad, provocando que el sistema de routing de Blazor vuelva a comprobar si existe una página para la ruta actual. Y dado que la respuesta es negativa, se muestra el contenido por defecto, la página en blanco con el texto "Not found".

Por tanto, para solucionarlo, lo único que tendríamos que hacer es evitar que el sistema de routing de Blazor muestre el contenido por defecto, sustituyéndolo por el que nos interesa, en nuestro caso, la página de error que hemos creado previamente.

Esto es sencillo: basta con añadir la sección <NotFound> a la instancia del componente <Router> en el archivo Routes.razor. Esta sección especifica el contenido a mostrar cuando el sistema de routing de Blazor no encuentra una página para la ruta actual; en este caso, usamos el componente <LayoutView> para mostrar el contenido con el layout del sitio, y en su interior instanciamos el componente NotFoundPage que hemos creado previamente:

@using BlazorNotFound.Client.Layout @*Include Layout namespace*@
@using BlazorNotFound.Client.Pages  @*Include NotFoundPage namespace*@
<Router AppAssembly="typeof(Program).Assembly">
    <Found Context="routeData">
        <RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
        <FocusOnNavigate RouteData="routeData" Selector="h1" />
    </Found>
    <NotFound>
        <LayoutView Layout="@typeof(MainLayout)">
            <NotFoundPage />
        </LayoutView>
    </NotFound>
</Router>

Hecho esto, al ejecutar la aplicación, podremos ver que ahora la página de error 404 personalizada se muestra correctamente, tanto si estamos usando componentes interactivos como si no.

¡Espero que os resulte útil!


Publicado en: www.variablenotfound.com.

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