Blog

Tagged by 'Partial View'

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

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