Using partial views to cache pieces of a page individually

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.

How to do it...

  1. Start by creating a new default ASP.NET MVC 2 application.
  2. Then add a reference to NBuilder, so that we can generate a huge list of data. This huge list of data should give us a slow page to be cached later.
  3. Next, we will create a 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 { }
    }
    }
    
  4. Now we need to create a 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();
    }
    }
    
  5. With the ability to generate a list of products, we need to be able to show them in a view. To do this, we will create a new action in our home controller called 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);
    }
    
  6. Then we need to generate a partial view to display our list of products. Do this by right-clicking on the action and adding a new view. This view will be a strongly typed partial view of type Product, which will show a list of the products.
    How to do it...
  7. Now we need to create a view to render our cached products to. We will create a new action in our home controller called CachedByAttribute. This action will simply return the view.

    Controllers/HomeController.cs:

    public ActionResult CachedByAttribute() {
    return View();
    }
    
  8. Then right-click on the action and add a new view. This view will be a simple no-frills view with nothing in it. Inside of it, we will make a call to 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>
    
  9. Now you are ready to see the cached listing of products. Browse to /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.
    How to do it...
  10. Uh oh...they didn't cache! That is because the output cache attribute on a view that is rendered as a child of another view (such as when using RenderAction) is ignored.

    Now, let's look at another approach to caching the products listing.

    Note

    This is a total hack and should be addressed in the MVC futures project, as well as the next release of the MVC framework.

  11. In this approach, we need to render a partial view as a string. We will then cache that string for future use and return it to our view to be rendered. We will use a common shared partial view to do the 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.
  12. First, we need to create the shared partial view that will handle our product's view rendering. This view will be called 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()); %>
    
  13. Now we can create a static class called 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";
    }
    
  14. Next, we need to create a StringBuilder and a StringWriter to capture the rendered view.

    Models/ControllerExtensions.cs:

    //Create memory writer
    var sb = new StringBuilder();
    var memWriter = new StringWriter(sb);
    
  15. We then need to create a faked 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);
    
  16. With all of these variables in place, we are ready to get to work. All we need to do now is to create an instance of 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);
    
  17. Notice that we created an instance of a 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();
    }
    }
    
  18. When we called 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;
    
  19. Technically, this is all that is needed to render our view to a string, which could then be cached pretty easily. But we want to make this easier to work with. To do this, we will add a couple of extension methods. The first one is called 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;
    }
    
  20. The next extension method will also extend 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;
    }
    
  21. Now we need to put our view renderer into action. To do this, we need to create a new action in our home controller called 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();
    }
    
  22. Now right-click on the new action and add a new view. This will be an empty view. It will simply render our 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>
    
  23. Now run your application and browse to /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.
    How to do it...

How it works...

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset