In this recipe, we will take a look at how we can use the RenderAction
method to get a partial view. We will do this by creating a shared partial view that is capable of rendering any action in which we might be interested. We will then create an extension
method, which is capable of calling our helper view and converting that into a string. This string can then be cached. Best of all, this particular approach allows us to use the appropriate controller to take care of the requirements for each of the views being rendered.
Product
class in the Models
folder, which we will use to hold the data in our list. We will also have some properties, which we will use to do some calculations for each displayed product.Models/Product.cs:
public class Product { public Guid ProductId { get; set; } public string Name { get; set; } public decimal Discount { get; set; } public decimal Retail { get; set; } public decimal Tax { get; set; } public decimal Price { get { decimal finalCost = Retail; if(Discount > 0) finalCost = finalCost - (finalCost * Discount); finalCost = finalCost + (finalCost * Tax); return finalCost; } private set { } } }
ProductService
class in the models
folder to generate our list of data. We will use NBuilder to generate a list of 1000 products. Each product will have a value of 10 cents for the tax property and we will randomly generate a discount for each product.Models/ProductService.cs:
public class ProductService { public List<Product> GetProducts() { Thread.Sleep(1000); //connect to the database Thread.Sleep(1000); //run a query and return data Random rnd = new Random(99); return Builder<Product>.CreateListOfSize(1000) .WhereAll() .Have(p => p.Discount = Convert.ToDecimal("." + rnd.Next(1,99))) ..WhereAll() ..Have(p => p.Tax = new decimal(.10)) ..Build() ..ToList(); } }
Products
. In this action, we will get a list of products and return that list to the view as its model. Knowing that this list of data is going to take a while to generate, we need to add an output cache attribute to cache the data for an hour.Controllers/HomeController.cs:
[OutputCache(Duration = (60*60), VaryByParam = "none")] public ActionResult Products() { List<Product> products = new ProductService().GetProducts(); return PartialView(products); }
Product
, which will show a list of the products. CachedByAttribute
. This action will simply return the view.Controllers/HomeController.cs:
public ActionResult CachedByAttribute() { return View(); }
Html.RenderAction
. I also wrapped it with a stopwatch to see how long the page takes to render with the products list inside of it.Views/Home/CachedByAttribute.aspx:
<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server"> <h2>Cached By Attribute</h2> <p>A non cached bit of data on this page: <%= DateTime.Now %> </p> <%System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch(); sw.Start(); %> <% Html.RenderAction("Products"); %> <% sw.Stop(); %> <%= "Getting 1000 products took " + sw.ElapsedMilliseconds + " milliseconds."%> </asp:Content>
/Home/CachedByAttribute
; you should see the list of products and that it took roughly two seconds and change (on my computer). Now refresh the page to see that the products are cached. RenderAction
) is ignored.Now, let's look at another approach to caching the products listing.
RenderAction
for us, to which we will pass the name of an action and its controller, so that we can reuse this approach anywhere in our site. CachedView.ascx
and will be placed in the Views/Shared
directory. All this view does is call RenderAction
on the passed-in action and controller names.Views/Shared/ChacedViewChacedView.ascx:
<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<dynamic>" %> <% Html.RenderAction(ViewData["ViewName"].ToString(), ViewData["ControllerName"].ToString()); %>
ControllerExtensions
and place it in the Controllers
directory. This class will be responsible for rendering our views to a string. Let's create a method called RenderViewToString
. It will use the CachedViews
partial view that we created as the handler of the partial view to render.Models/ControllerExtensions.cs:
private static string RenderViewToString(HttpContext httpContext, ControllerContext context, ViewDataDictionary viewData) { string pathToView = "~/Views/Shared/CachedView.ascx"; }
StringBuilder
and a StringWriter
to capture the rendered view.Models/ControllerExtensions.cs:
//Create memory writer var sb = new StringBuilder(); var memWriter = new StringWriter(sb);
HttpResponse, HttpContext
, and ControllerContext
.Models/ControllerExtensions.cs:
//Create fake http context to render the view var fakeResponse = new HttpResponse(memWriter); var fakeContext = new HttpContext(httpContext.Request, fakeResponse); var fakeControllerContext = new ControllerContext( new HttpContextWrapper(fakeContext), context.RouteData, context.Controller);
HtmlHelper
to which we will pass all of our faked instances, as well as create some new inline instances of other required classes. Then we can call the RenderPartial
method (you will need to add a reference to the System.Web.Mvc.Html
namespace) on the HtmlHelper
and pass in the path to our CachedView
partial view, as well as any passed-in ViewData
(such as the action and controller name...more on this in a sec).Models/ControllerExtensions.cs:
//Use HtmlHelper to render partial view to fake context var html = new HtmlHelper(new ViewContext(fakeControllerContext, new FakeView(), new ViewDataDictionary(), new TempDataDictionary(), memWriter), new ViewPage()); html.RenderPartial(pathToView, viewData);
FakeView()
in the previous code. It didn't make much sense to mention this until now. But we need to create the FakeView
class, which will implement IView
. This is a requirement for creating an instance of HtmlHelper
. Here is that class—notice that there actually isn't any implementation in this class.Models/ControllerExtensions.cs:
public class FakeView : IView { public void Render(ViewContext viewContext, System.IO.TextWriter writer) { throw new NotImplementedException(); } }
RenderPartial
on the HtmlHelper
, we rendered our view to the StringBuilder
we created at the top of this method. We can now do some clean up and return our view as a string.Models/ControllerExtensions.cs:
//Flush memory and return output memWriter.Flush(); string result = sb.ToString(); return result;
RenderViewToString
and it will extend the Controller
class (so that we can use it from within any class that derives from Controller)
. This method will take in the name of the view and controller that the user wants to render. It will then put those strings into view data to be passed to the CachedView
. And of course, it will take care of all the dirty work that our RenderViewToString
method requires.Models/ControllerExtensions.cs:
public static string RenderViewToString(this Controller controller, string viewName, string controllerName) { controller.ViewData.Add("ViewName", viewName); controller.ViewData.Add("ControllerName", controllerName); string result = RenderViewToString( System.Web.HttpContext.Current, controller.ControllerContext, controller.ViewData); return result; }
Controller
. This method is very similar to the last extension method we created, but it will take in a couple of other parameters. This method will hide our caching concepts. It will take in a DateTime
to set an absolute expiration value and it will take in a TimeSpan
to allow us a way to set a sliding scale. It will also set the controller and view names into ViewData
for use by the CachedView
. Then we will check the cache to see if this view has been cached before (by its view name). If it has, then we will return the cached view. If the item hasn't been cached before, we will get the view as a string, stuff it in the cache, and return it.Models/ControllerExtensions.cs:
public static string RenderViewToString(this Controller controller, string viewName, string controllerName, DateTime absoluteExpiration, TimeSpan slidingExpiration) { string result = ""; controller.ViewData.Add("ViewName", viewName); controller.ViewData.Add("ControllerName", controllerName); if (System.Web.HttpContext.Current.Cache[viewName] != null) result = HttpContext.Current.Cache[viewName].ToString(); else { result = RenderViewToString(System.Web.HttpContext.Current, controller.ControllerContext, controller.ViewData); HttpContext.Current.Cache.Add(viewName, result, null, absoluteExpiration, slidingExpiration, CacheItemPriority.Default, null); } return result; }
CachedAsString
. This action will call our RenderViewToString
method and pass in caching parameters (a sliding scale of one hour) to use the caching implementation of our string render. Then we will pass the rendered view down to our CachedAsString
view via ViewData
as CachedProducts
. (I have also added some stopwatch data for us to track how long the cached version takes.)Controllers/HomeController.cs:
public ActionResult CachedAsString() { Stopwatch sw = new Stopwatch(); sw.Start(); string products = this.RenderViewToString("Products", "Home", DateTime.MaxValue, new TimeSpan(0, 1, 0, 0)); sw.Stop(); ViewData["CachedProducts"] = products; ViewData["CachedStopwatch"] = sw.ElapsedMilliseconds; return View(); }
CachedProducts
string and the stopwatch data.Views/Home/CachedAsString.aspx:
<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server"> <h2>Cached As String</h2> <p>A non cached bit of data on this page: <%= DateTime.Now %> </p> <%= ViewData["CachedProducts"] %> <%= "Getting 1000 products took " + ViewData["CachedStopwatch"] + " milliseconds."%> </asp:Content>
/Home/CachedAsString
. You should see that the product's partial view took a little over two seconds to render. Now refresh the page. 0 seconds to render! Not too bad.The power for this recipe comes in two fashions. It was very important that we get the Html.RenderAction
method to work as this method calls an action on a controller, which means that the data for the view being rendered is handled outside of the view that is doing the rendering. This means that we can easily reuse this logic without having to duplicate it. This is an important distinction. All other forms of rendering a view inline require that you also create the data that the view needs in the controller for the parent view. This can quickly become very complex and ugly.
In order to use the RenderAction
method, we had to find a way to get it to execute in a manner that is copasetic for the MVC framework, but that would return something that we could cache in a more manual fashion. Looking at the Web, you will find all sorts of methods to render a view as a string. Not all of them are the same. Most of them didn't work in the manner in which they are advertised. And even fewer of those would actually work with the RenderAction
method. I did find one post on Lorenz Cuno Klopfenstein's blog, which gave me my basic implementation for rendering a view to a string. Here is that post: http://www.klopfenstein.net/lorenz.aspx/render-partial-view-to-string-in-asp-net-mvc.
Once you get the RenderAction
to be exposed as a string, it is really just a matter of creating some basic MVC components such as the Products view and the pass through view, which we used for the RenderAction
. We also used the standard cache that is built into the framework to cache the view string. This is the one place where I would ask you to think a bit further. Adding dependencies in your code to a framework that you don't own is usually not a good idea. If nothing else, I would suggest that you put a wrapper around the cache usage.