2

I'd want to generate html of myview.cshtml while passing string test to it

pseudo code based of return View("myview", "test");

var html = View("myview", "test")

I found a few working fine solutions to it, but they're limited to being executed in context of Controller

Where are the ControllerContext and ViewEngines properties in MVC 6 Controller?

public static class ControllerExtensions
{
    public static async Task<string> RenderViewAsync<TModel>(this Controller controller, string viewName, TModel model, bool partial = false)
    {
        if (string.IsNullOrEmpty(viewName))
        {
            viewName = controller.ControllerContext.ActionDescriptor.ActionName;
        }

        controller.ViewData.Model = model;

        using (var writer = new StringWriter())
        {
            IViewEngine viewEngine = controller.HttpContext.RequestServices.GetService(typeof(ICompositeViewEngine)) as ICompositeViewEngine;
            ViewEngineResult viewResult = viewEngine.FindView(controller.ControllerContext, viewName, !partial);

            if (viewResult.Success == false)
            {
                return $"A view with the name {viewName} could not be found";
            }

            ViewContext viewContext = new ViewContext(
                controller.ControllerContext,
                viewResult.View,
                controller.ViewData,
                controller.TempData,
                writer,
                new HtmlHelperOptions()
            );

            await viewResult.View.RenderAsync(viewContext);

            return writer.GetStringBuilder().ToString();
        }
    }
}

But how can I achieve that when I don't have any controller? do I have to create fake one? I'd want to avoid it because it requires a lot of DI magic to create instance of Controller

Joelty
  • 1,751
  • 5
  • 22
  • 64
  • 2
    The normal Razor engine requires a view context and there is no way around it. If you want to render arbitrary Razor views to text, I would recommend you to use a library like [RazorLight](https://github.com/toddams/RazorLight). – poke Jan 21 '20 at 14:34

1 Answers1

1

I have struggled to find a good answer to this for a while, and this is what I've come up with. Note that this assumes that the code is being executed as part of a HTTP request, just not in the controller. In my case, I know the calls to RenderViewAsync() will only come via a few controller methods, so I created a custom attribute that will store the controller instance in the HttpContext. You could, alternatively, write a filter or custom middleware to do this globally.

public class StoreControllerContextAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext context)
    {
        var controller = (Controller)context.Controller;

        context.HttpContext.Items.Add("Controller", controller);
    }
}

Decorate the relevant controller methods with this attribute:

[StoreControllerContext]
public async Task<IActionResult> ControllerMethod()

I put my RenderViewAsync() method in a service class, which gets passed an IHttpContextAccessor via DI.

public class RenderService
{
    private readonly IHttpContextAccessor _httpContextAccessor;

    public RenderService(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }

    public async Task<string> RenderViewAsync<TModel>(string viewName, TModel model, bool partial = false)
    {
        // Get the HttpContext using the accessor
        var httpContext = _httpContextAccessor.HttpContext;
        // The controller should have been put into HttpContext.Items by the [StoreControllerContext] attribute on the controller method
        var controller = (Controller)(httpContext.Items["Controller"]);
        controller.ViewData.Model = model;

        using (var writer = new StringWriter())
        {
            IViewEngine viewEngine = httpContext.RequestServices.GetService(typeof(ICompositeViewEngine)) as ICompositeViewEngine;
            // Using an absolute path, so use GetView() not FindView()
            ViewEngineResult viewResult = viewEngine.GetView(viewName, viewName, !partial);

            var temp = viewResult.SearchedLocations;

            if (viewResult.Success == false)
            {
                return $"A view with the name {viewName} could not be found";
            }

            ViewContext viewContext = new ViewContext(
                controller.ControllerContext,
                viewResult.View,
                controller.ViewData,
                controller.TempData,
                writer,
                new HtmlHelperOptions()
            );

            await viewResult.View.RenderAsync(viewContext);

            return writer.GetStringBuilder().ToString();
        }
    }
}

In Startup.cs you will need to register the service:

services.AddScoped<RenderService>();

and also ensure you enable the HttpContextAccessor:

services.AddHttpContextAccessor();

Then I inject my RenderService where it is required, and call it like so:

string html = await _renderService.RenderViewAsync("~/Views/Path/To/MyView.cshtml", myModel, true);
John M
  • 2,510
  • 6
  • 23
  • 31