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}";
            }
        }
    }
}

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

 

Extension Method To Render Action As String

A while ago, I wrote a post that showed how you would Render a Partial View As A String. But what if you had a Partial View and wanted to output its action to a string?

Just like the majority of all other coding problems I encounter, StackOverflow always has the answer. In this case, a clever guy posted this piece of code:

var sw = new StringWriter();
PartialViewResult result = Email("Subject", "Body");

result.View = ViewEngines.Engines.FindPartialView(ControllerContext, "Email").View;

ViewContext vc = new ViewContext(ControllerContext, result.View, result.ViewData, result.TempData, sw);

result.View.Render(vc, sw);

var html = sw.GetStringBuilder().ToString();

Works well enough. However, I didn't like the thought of having to add all this code inside my controller, especially when I have to output many Partial View actions to a string. So I created an extension method:

/// <summary>
/// Renders a Partial View Action to string.
/// </summary>
/// <param name="controller">Controller to extend</param>
/// <param name="partialView">PartialView to render</param>
/// <param name="partialViewName">Name of Partial View</param>
/// <returns>Renders Partial View as a string</returns>
public static string RenderActionToString(this Controller controller, PartialViewResult partialView, string partialViewName)
{
    using (var sw = new StringWriter())
    {
        partialView.View = ViewEngines.Engines.FindPartialView(controller.ControllerContext, partialViewName).View;

        ViewContext vc = new ViewContext(controller.ControllerContext, partialView.View, partialView.ViewData,
            partialView.TempData, sw);

        partialView.View.Render(vc, sw);

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

This extension method can be used in the following way:

//Access PollController class.
PollController pc = new PollController();

//Get the LatestPoll PartialView action and output to string.
string myPartialView = this.RenderActionToString(pc.LatestPoll(), "../Poll/_LatestPoll");

Much cleaner!

 

Render Partial View As A String

One of the many nice things of using ASP.NET MVC Razor is that you have full control over how you segregate your HTML markup when building a page through rendering PartialViews. Since becoming an avid MVC developer, I am increasingly noticing how easy it is to make nice neat reusable code, whether it is used server or client-side.

Just today, I found something really useful that is a truly defines this, where markup within PartialViews can be output to a page as string:

/// <summary>
/// Controller extension class that adds controller methods
/// to render a partial view and return the result as string.
///
/// Based on http://craftycodeblog.com/2010/05/15/asp-net-mvc-render-partial-view-to-string/
/// </summary>
public static class ControllerExtension
{
 
  /// <summary>
  /// Renders a (partial) view to string.
  /// </summary>
  /// <param name="controller">Controller to extend</param>
  /// <param name="viewName">(Partial) view to render</param>
  /// <returns>Rendered (partial) view as string</returns>
  public static string RenderPartialViewToString(this Controller controller, string viewName)
  {
    return controller.RenderPartialViewToString(viewName, null);
  }
 
  /// <summary>
  /// Renders a (partial) view to string.
  /// </summary>
  /// <param name="controller">Controller to extend</param>
  /// <param name="viewName">(Partial) view to render</param>
  /// <param name="model">Model</param>
  /// <returns>Rendered (partial) view as string</returns>
  public static string RenderPartialViewToString(this Controller controller, string viewName, object model)
  {
    if (string.IsNullOrEmpty(viewName))
      viewName = controller.ControllerContext.RouteData.GetRequiredString("action");
 
      controller.ViewData.Model = model;
 
      using (var sw = new StringWriter())
      {
        var viewResult = ViewEngines.Engines.FindPartialView(controller.ControllerContext, viewName);
        var viewContext = new ViewContext(controller.ControllerContext, viewResult.View, controller.ViewData, controller.TempData, sw);
        viewResult.View.Render(viewContext, sw);
 
        return sw.GetStringBuilder().ToString();
      }
    } 
}

I can't take credit for this code. But here is the guy who can: Jan Jonas.

Being able to output PartialViews as a string is actually quite handy, since you could have a paginated news listings page that displays the first page of articles server-side and any additional pages could be loaded in via jQuery Ajax. Each article item would be a PartialView so you could serve the same markup client-side. My code below probably explains things a little better:

Article Listing View

This page will list all my News Articles. As you can see, I am using an "ArticleListItem" as my PartialView.

@model List<Article>

@if (Model.Any())
{
    <div class="article-list">
    @foreach (var a in Model.Select((value, index) => new { value, index }))
    {
        Html.RenderPartial("/Views/Article/_ArticleListItem.cshtml", new ArticleListItemView { Article = a.value, CssClass = ArticleHtmlHelper.GetItemCssClass((a.index + 1)), IsFullWidth = false});
    }
    </div>
}
else
{
    <div>
        No articles could be returned.
    </div>
}

Article List Item PartialView

My PartialView has quite a bit going on to determine how the markup should be rendered and it's definitely something I wouldn't want to have to duplicate elsewhere just to load in client-side. Nice!

@model Site.Web.Models.Views.ArticleListItemView
@{
    string fullWidthClass = String.Empty;

    if (Model.IsFullWidth)
    {
        fullWidthClass = "full-width";
    }
}
<div class="article-summary @Model.CssClass @fullWidthClass">
    <a href="@Model.Article.PageUrl" class="img">
        @if (Model.CssClass == "large")
        {
        <img src="@Model.Article.Images.ImageCollection[1].Url" />
        }
        else
        {
        <img src="@Model.Article.Images.ImageCollection[0].Url" />
        }
    </a>
    @if (Model.Article.Category != null)
    {
    <span class="cat">@Model.Article.Category.Name</span>
    }
    @if (Model.Article.ReadTime != null)
    {
    <span class="time">@String.Format("{0} read", Model.Article.ReadTime)</span>
    }
    <h2 class="@Model.CssClass"><a href="@Model.Article.PageUrl">@Model.Article.Title</a></h2>
    @if (Model.Article.Author != null)
    {
    <a href="@Model.Article.Author.PageUrl.Url" class="author">
        <img src="@Model.Article.Author.Images.ImageCollection[0].Url" />
        <span>@String.Concat(Model.Article.Author.FirstName, " ", Model.Article.Author.LastName)</span>
    </a>
    }
</div>

GetArticleItems() Controller

This is where the RenderPartialViewToString() method shines! This controller is called within my jQuery Ajax function to get the next page of news articles. I am then calling my "ArticleListItem" PartialView to return the HTML markup as a string through my client-side call.

[HttpPost]
public JsonResult GetArticleItems(DBContext ctx, int pageNo, int pageSize, string categoryId)
{
    ApiDocumentInfo docInfo = DocumentHelper.SearchDocuments(ctx, true, "article", "category", categoryId, pageSize, pageNo, "articles", "date desc");

    List<Article> articles = docInfo.Documents.Select(doc => doc.ToArticle(ctx)).ToList();

    StringBuilder articleHtml = new StringBuilder();

    if (articles.Any())
    {
        for (int a = 0; a < articles.Count; a++)
            articleHtml.Append(this.RenderPartialViewToString("_ArticleListItem", new ArticleListItemView { Article = articles[a], CssClass = ArticleHtmlHelper.GetItemCssClass((a + 1)), IsFullWidth = false } ));
    }

    return Json(articleHtml.ToString());
}