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, 20 de febrero de 2018
ASP.NET Core MVC En ocasiones puede resultar interesante renderizar una vista de ASP.NET Core MVC a una cadena de caracteres. Probablemente el escenario clásico de uso sea utilizar este tipo de vistas como motores de plantillas, por ejemplo, a la hora de generar el cuerpo de un email, pero puede haber muchos otros casos en los que nos vendría bien saber hacerlo.

En las versiones "clásicas" de ASP.NET MVC era algo que se podía resolver con relativa facilidad gracias a proyectos como RazorGenerator o RazorEngine, pero, como en ASP.NET Core las cosas han cambiado bastante, he pensado que quizás sería interesante comentar por aquí cómo podríamos conseguirlo en este nuevo framework.

Cómo renderizar una vista a un string

Aunque se pueden diseñar soluciones más potentes y elegantes, vamos a ver cómo podríamos hacer sin complicarnos demasiado y aprovechando al máximo la infraestructura que nos ofrece ASP.NET Core MVC.

Para renderizar una vista (Razor o de cualquier otro tipo) a una cadena de texto lo único que necesitamos es localizarla utilizando el view engine apropiado e invocar a su método RenderAsync() suministrándole un contexto de ejecución con la información que necesita para renderizarse. Toda esta información contextual la tenemos disponible en el interior de las acciones MVC, por lo que en principio la tarea no es muy compleja.

Así, bajando a nivel de código, la renderización de plantilla vista desde acciones MVC podría llevarse a cabo mediante un método como el siguiente, que podría implementarse en el propio controlador, en una clase base para que esté disponible en todos los controladores que hereden de ella, o incluso podríamos implementarlo fácilmente como un extensor de Controller:
protected async Task<string> RenderViewAsync(string viewName = null, object model = null)
{
    // Primero, intentamos localizar la vista...
    var actionContext = new ActionContext(
            HttpContext, RouteData, ControllerContext.ActionDescriptor, ModelState);
    var viewEngine = HttpContext.RequestServices.GetService<ICompositeViewEngine>();

    var viewEngineResult = viewEngine.FindView(actionContext, viewName, isMainPage: false);

    if (!viewEngineResult.Success)
    {
        var searchedLocations = string.Join(", ", viewEngineResult.SearchedLocations);
        throw new InvalidOperationException(
            $"Couldn't find view '{viewName}', " +
            $"searched locations: [{searchedLocations}]");
    }

    // Hemos encontrado la vista, vamos a renderizarla...
    using (var sw = new StringWriter())
    {
        // Preparamos el contexto de la vista
        var viewData = new ViewDataDictionary(ViewData) { Model = model };
        var helperOptions = HttpContext.RequestServices
                .GetService<IOptions<HtmlHelperOptions>>()
                .Value;
        var viewContext = new ViewContext(
                actionContext, viewEngineResult.View, viewData, TempData, sw, helperOptions);

        // Y voila!
        await viewEngineResult.View.RenderAsync(viewContext);
        return sw.ToString();
    }
}
Como se puede observar, el código se estructura en dos partes. La primera de ellas la dedicamos a buscar la vista utilizando los view engines. La búsqueda, realizada mediante una llamada a FindView() retornará un objeto de tipo ViewEngineResult a través del cual podremos saber si fue exitosa o no. En el primer caso, en viewEngineResult.View tendremos el objeto IView que representa a la vista, y sobre el que podemos ejecutar el método RenderAsync() para obtener la cadena de caracteres resultado de su proceso.

Su uso desde un controlador sería trivial:
public class HomeController : Controller
{
    public async Task<IActionResult> Index()
    {
        string result = await RenderViewAsync(viewName: "HelloTemplate", model: "John");
        // Hacer aquí algo interesante con "result"

        // De momento, sólo lo retornamos como "text/plain"        
        return Content(result); 
    }

    protected async Task<string> RenderViewAsync(string viewName = null, object model = null) 
    {
        ... // El código que vimos antes
    }
}
Y la plantilla “Views/Home/HelloTemplate.cshtml” podría ser algo como lo siguiente:
@model string
<!DOCTYPE html>
<html>
<head>
    <title>Hello @Model</title>
</head>
<body>
    <h1>Hello @Model!</h1>
</body>
</html>
Por supuesto, la plantilla es una vista Razor normal y corriente, por lo que podremos utilizar en su interior cualquier tipo de construcción válida en este tipo de componentes (bloques de código, helpers, tag helpers, bloques @function, layouts…) En cuanto a su ubicación, tened en cuenta que la vista se renderiza en el contexto de una acción, por lo que el archivo .cshtml deberá encontrarse según las convenciones habituales (por defecto en /Views/{Controller}/{NombreDePlantilla} o /Views/Shared/{NombreDePlantilla}).

Espero que os sea de utilidad :)

Publicado en Variable not found.

3 Comentarios:

Anónimo dijo...

Este código también tendría que ser válido para MVC común cierto? (para no utilizar ninguna librería externa)

José María Aguilar dijo...

Hola!

Si te refieres a MVC 5 o anteriores, el código tal cual no es compatible porque utiliza conceptos exclusivos de ASP.NET Core, pero no tendrás problema en encontrar ejemplos para esos frameworks, tanto usando bibliotecas externas como sin usarlas.

Personalmente casi siempre he usado estas bibliotecas externas porque en muchas ocasiones el renderizado me ha interesado hacerlo desde fuera de una aplicación MVC, y, en ese escenario, algo como RazorGenerator me ha venido bastante bien.

Saludos!

Crahun dijo...

Hola, para que este código funcione debeis añadir:
using Microsoft.Extensions.DependencyInjection;