Bulletproof Ajax with ASP.NET MVC

Posted by andy gaskell on Aug 2nd, 2008

After reading the excellent Bulletproof Ajax by Jeremy Keith, I thought porting the Bulletproof Books sample application to ASP.NET MVC would be a great first attempt at building an ASP.NET MVC application. One of the main points of Bulletproof Ajax is that Ajax should be used to enhance usability and not to make Ajax a requirement to access your content. My version of the Bulletproof Books Shop also makes Javascript and XMLHttpRequest support optional.

Keeping the site accessible to most web browsers took some extra work and consideration when building the Controllers. I decided that any HomeController actions would render html for the entire page.

   13         public ActionResult Index()

   14         {

   15             ViewData.Model = Product.Products;

   16             return View(“Index”);

   17         }

   18 

   19         public ActionResult AddProductToCart(string productID, int quantity)

   20         {

   21             CartController cartController = new CartController();

   22             cartController.AddProduct(productID, quantity);

   23             return Index();

   24         }

   25 

   26         public ActionResult RateProduct(string productID, string rating)

   27         {

   28             RatingController ratingController = new RatingController();

   29             ratingController.RateProduct(productID, rating);

   30             return Index();

   31         }

Controllers that refresh partial chunks of html will know how to respond to actions (add product to cart, rate a product) and render views mapped to MVC user controls. The client will make http requests to different urls based on javascript support in the browser. For example to add a product to a cart with javascript enabled, the javascript will make a post to /Cart.mvc/AddProduct and the server will response with a chunk of html. To add a product with javascript disabled the client will post to /Home.mvc/AddProductToCart and the server will respond with an entire page. The HomeController ends up forwarding the call to the appropriate controller so we’re able to avoid duplicating logic.

   28         public ActionResult DisplayCart()

   29         {

   30             Cart cart = GetCart();

   31             return View(“Cart”, cart);

   32         }

   33 

   34         public ActionResult AddProduct(string productID, int quantity)

   35         {

   36             Cart cart = GetCart();

   37             Product product = Product.Products.Find(s => s.ID == productID);

   38             cart.AddProduct(product, quantity);

   39             SetCart(cart);

   40             return View(“Cart”, cart);

   41         }

One thing I’m not sure about is state management in ASP.NET MVC, so I’m currently storing the cart and ratings in session.

   22         private Rating GetRatings()

   23         {

   24             Rating rating = System.Web.HttpContext.Current.Session["Rating"] as Rating;

   25             if (rating == null)

   26             {

   27                 rating = new Rating();

   28             }

   29             return rating;

   30         }

   31 

   32         private void SetRating(Rating rating)

   33         {

   34             System.Web.HttpContext.Current.Session["Rating"] = rating;

   35         }

You can download the source here.

6 Responses

  1. ASP.NET MVC Archived Blog Posts, Page 1 Says:

    [...] to VoteBulletproof Ajax with ASP.NET MVC (8/2/2008)Saturday, August 02, 2008 from andy gaskellNET MVC would be a great first attempt at building an [...]

  2. Matthew Says:

    A couple of issues with what you have so far:
    1. you have the controllers implementing model functionality.

    2. your Hijax.js code is modifying the URL which messes thinks up if you change the routes, particularly to pass parameter like
    routes.MapRoute(
    “RateProduct”, // Route name
    “Rating/{action}/{productid}/{ratingVal}”, // URL with parameters
    new { controller = “Rating”, action = “RateProduct”, productid = “”, ratingVal = “” } // Parameter defaults
    );
    I’ve modified the code for the controllers to
    using System.Web;
    using System.Web.Mvc;
    using BulletproofShop.Models;

    namespace BulletproofShop.Controllers
    {
    public class RatingController : Controller
    {
    private const string YOUR_RATING = “Your rating:”;
    private const string RATE_THIS_BOOK = “Rate this book:”;

    public class ProductRatingViewData
    {
    public string Rating { get; set; }
    public string RatingMessage { get; set; }
    public Product Product { get; set; }
    }

    public ActionResult DisplayRating(Product product)
    {
    Rating ratings = new Rating();
    string rating = null;
    string ratingMessage = RATE_THIS_BOOK;
    if (ratings.Ratings.ContainsKey(product))
    {
    rating = ratings.Ratings[product];
    ratingMessage = YOUR_RATING;
    }
    return View(”Rating”, new ProductRatingViewData { Product = product, Rating = rating, RatingMessage = ratingMessage });
    }

    public ActionResult RateProduct(string productid, string ratingVal)
    {
    //RatingController ratingController = new RatingController();
    //ratingController.RateProduct(productID, rating);
    Rating ratings = new Rating();
    ratings.SetRating(productid, ratingVal);
    return RedirectToAction(”Index”, “Home”);
    }

    }
    }

    ——————————–

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Web;
    using System.Web.Mvc;
    using BulletproofShop.Models;

    namespace BulletproofShop.Controllers
    {
    [HandleError]
    public class HomeController : Controller
    {
    public ActionResult Index()
    {
    ViewData.Model = Product.Products;
    return View(”Index”);
    }

    public ActionResult AddProductToCart(string productid, int quantity)
    {
    Cart cart = new Cart();
    cart.AddProduct(productid, quantity);
    return Index();
    }
    }
    }
    —————————–
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Web;
    using System.Web.Mvc;
    using BulletproofShop.Models;

    namespace BulletproofShop.Controllers
    {
    public class CartController : Controller
    {
    // I’m sure there’s a better way to handle state management. I just don’t know what it is.

    public ActionResult DisplayCart()
    {
    Cart cart = new Cart();
    return View(”Cart”, cart);
    }
    }
    }
    —————————-
    and the models to
    namespace BulletproofShop.Models
    {
    public class Cart
    {
    Dictionary contents ;

    public Cart()
    {
    contents = Contents;
    }

    public Dictionary Contents
    {
    get
    {
    contents = System.Web.HttpContext.Current.Session["Cart"] as Dictionary;
    if (contents == null)
    {
    contents = new Dictionary();
    }
    return contents;
    }
    }

    public void AddProduct(Product product, int quantity)
    {
    if(Contents.ContainsKey(product))
    {
    contents[product] += quantity;
    }
    else
    {
    contents.Add(product, quantity);
    }
    System.Web.HttpContext.Current.Session["Cart"] = contents;
    }

    public void AddProduct(string productID, int quantity)
    {
    Product product = Product.Products.Find(s => s.ID == productID);
    AddProduct(product, quantity);
    }

    }
    }
    ————————–
    using System;
    using System.Data;
    using System.Collections.Generic;
    using System.Configuration;
    using System.Linq;
    using System.Web;
    using System.Web.Security;
    using System.Web.UI;
    using System.Web.UI.HtmlControls;
    using System.Web.UI.WebControls;
    using System.Web.UI.WebControls.WebParts;
    using System.Xml.Linq;

    namespace BulletproofShop.Models
    {
    public class Rating
    {
    Dictionary ratings ;

    public Rating()
    {
    ratings = Ratings;
    }

    public Dictionary Ratings
    {
    get
    {
    if (ratings == null)
    {
    ratings = System.Web.HttpContext.Current.Session["Rating"] as Dictionary;
    {
    if (ratings == null)
    ratings = new Dictionary();
    }
    }
    return ratings;
    }
    }

    public void SetRating(Product product, string rating)
    {
    if (Ratings.ContainsKey(product))
    {
    ratings[product] = rating;
    }
    else
    {
    ratings.Add(product, rating);
    }
    System.Web.HttpContext.Current.Session["Rating"] = ratings;
    }

    public void SetRating(string productID, string rating)
    {
    Product product = Product.Products.Find(s => s.ID == productID);
    SetRating(product, rating);
    }
    }
    }
    —————————
    with Product unchanged.

    I made some hacks to Hijax.js to stop it changing the URL but need to look at it more to understand if this file is needed at all.

  3. Matthew Says:

    The post stripped the <Product, string> from the Dictionary in Cart.cs and Rating.cs

  4. andy gaskell Says:

    Thanks for the feedback Matthew. Most of the articles and examples I’ve seen so far use TempData (sort of a toned down Session object) in the Controllers and sometimes views. I’m not really sure if the Model should be responsible for state management - It would be nice to reuse the Model in non-web applications if need be.

    Regarding routes - I don’t know a whole lot about routes. I’ll do more reading up on them. However Hijax is supposed to be making the request to a different url. This allows us to return html fragments if the client is ajax enabled. Try turning off javascript (or removing Hijax) - the request will be handled by the Home controller. What made sense to me was that the Home controller would be responsible for full page requests and the Ratings/Cart controllers would be responsible for their fragments of html.

  5. Matthew Says:

    The Model is resposible for the management of data or state. The means of storage, session, xml, sql, etc., should be transparent to the user. If you program to an Interface, then you can substitute alternate implementations as required.

    There is good support in APS.NET MVC, and several blog articles, on how to implement AJAX patterns in your MVC code.
    try http://www.hanselman.com/blog/ASPNETMVCPreview4UsingAjaxAndAjaxForm.aspx

  6. Dan Says:

    @Andy,

    Thanks for the post. I just bought this book but haven’t read it yet. I’ve been playing around with ASP.NET MVC too so this exact topic has been on my mind.

    @Matthew, it seems by your comments that you are not aware of the “Bulletproof Ajax” book. I don’t think Andy is trying to propose the right way to do Ajax in ASP.NET MVC. Instead I think he is trying to show how the lessons learned in Bulletproof Ajax might fit into ASP.NET MVC.

    With respect to the Model I disagree with Matthew… the model should not have any knowledge of HttpSession (or anything else in System.Web*). Decoupling domain objects from the persistence store is a good thing ™ - I’m not arguing that. But there is a difference between Session State Patterns and Data Source Patterns. HttpSession falls into the former category and is a concern of the Controller.

    To prove the point… ShoppingCart is an entity in your example. Whether that entity is transient and dies when the session dies or persistent is a feature of the application. In the first case the controller would need a Session State pattern (Server Session State) but in the second case would need a Data Access pattern (Table Data Gateway, Row Data Gateway, Active Record, Data Mapper).

    Cheers,
    Dan

    ref: http://martinfowler.com/eaaCatalog/

Leave a Comment

Please note: Comment moderation is enabled and may delay your comment. There is no need to resubmit your comment.