ASP.NET Core - Render Partial View To String Outside Controller Context
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();
}
}