ASP.NET Core - Render Partial View To String Outside Controller Context

Published on
-
3 min read

When building MVC websites, I cannot get through a build without using a method to convert a partial view to a string. I have blogged about this in the past and find this approach so useful especially when carrying out heavy AJAX processes. Makes the whole process of maintaining and outputting markup dynamically a walk in the park.

I've been dealing with many more ASP.NET Core builds and migrating over the RenderPartialViewToString() extension I developed previously was not possible. Instead, I started using the approach detailed in the following StackOverflow post: Return View as String in .NET Core. Even though the approach was perfectly acceptable and did the job nicely, I noticed I had to make one key adjustment - allow for views outside controller context.

The method proposed in the StackOverflow post uses ViewEngine.FindView(), from what I gather only returns a view within the current controller context. I added a check that will use ViewEngine.GetView() if a path of the view ends with a ".cshtml" which is normally the approach used when you refer to a view from a different controller by using a relative path.

public static class ControllerExtensions
{
    /// <summary>
    /// Render a partial view to string.
    /// </summary>
    /// <typeparam name="TModel"></typeparam>
    /// <param name="controller"></param>
    /// <param name="viewNamePath"></param>
    /// <param name="model"></param>
    /// <returns></returns>
    public static async Task<string> RenderViewToStringAsync<TModel>(this Controller controller, string viewNamePath, TModel model)
    {
        if (string.IsNullOrEmpty(viewNamePath))
            viewNamePath = controller.ControllerContext.ActionDescriptor.ActionName;

        controller.ViewData.Model = model;

        using (StringWriter writer = new StringWriter())
        {
            try
            {
                IViewEngine viewEngine = controller.HttpContext.RequestServices.GetService(typeof(ICompositeViewEngine)) as ICompositeViewEngine;

                ViewEngineResult viewResult = null;

                if (viewNamePath.EndsWith(".cshtml"))
                    viewResult = viewEngine.GetView(viewNamePath, viewNamePath, false);
                else
                    viewResult = viewEngine.FindView(controller.ControllerContext, viewNamePath, false);

                if (!viewResult.Success)
                    return $"A view with the name '{viewNamePath}' 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();
            }
            catch (Exception exc)
            {
                return $"Failed - {exc.Message}";
            }
        }
    }

    /// <summary>
    /// Render a partial view to string, without a model present.
    /// </summary>
    /// <typeparam name="TModel"></typeparam>
    /// <param name="controller"></param>
    /// <param name="viewNamePath"></param>
    /// <returns></returns>
    public static async Task<string> RenderViewToStringAsync(this Controller controller, string viewNamePath)
    {
        if (string.IsNullOrEmpty(viewNamePath))
            viewNamePath = controller.ControllerContext.ActionDescriptor.ActionName;
            
        using (StringWriter writer = new StringWriter())
        {
            try
            {
                IViewEngine viewEngine = controller.HttpContext.RequestServices.GetService(typeof(ICompositeViewEngine)) as ICompositeViewEngine;

                ViewEngineResult viewResult = null;

                if (viewNamePath.EndsWith(".cshtml"))
                    viewResult = viewEngine.GetView(viewNamePath, viewNamePath, false);
                else
                    viewResult = viewEngine.FindView(controller.ControllerContext, viewNamePath, false);

                if (!viewResult.Success)
                    return $"A view with the name '{viewNamePath}' 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();
            }
            catch (Exception exc)
            {
                return $"Failed - {exc.Message}";
            }
        }
    }

}

Quick Example

As you can see from my quick example below, the Home controller is using the RenderViewToStringAsync() when calling:

  • A view from another controller, where a relative path to the view is used.
  • A view from within the realms of the current controller and the name of the view alone can be used.
public class HomeController : Controller
{
    public async Task<IActionResult> Index()
    {
        NewsListItem newsItem = GetSingleNewsItem(); // Get a single news item.

        string viewFromAnotherController = await this.RenderViewToStringAsync("/Views/News/_NewsList.cshtml", newsItem);
        string viewFromCurrentController = await this.RenderViewToStringAsync("_NewsListHome", newsItem);

        return View();
    }
}

Before you go...

If you've found this post helpful, you can buy me a coffee. It's certainly not necessary but much appreciated!

Buy Me A Coffee

Leave A Comment

If you have any questions or suggestions, feel free to leave a comment. Your comment will not only help others, but also myself.