• Using JSON payloads to improve modern ASP.NET MVC applications

    Published by on March 20th, 2012 3:39 pm under Code

    No Comments

    Getting Started: An ASP.NET MVC site that lists customers

    Download all the code here.

    This section is the vanilla MVC to set the stage for talking about AJAX and JSON payloads later on.  This should all be very familiar to ASP.NET MVC developers.

    1. First, wel add a very simple customer model.

    public class Customer
    {
        public string Name { get; set; }
    }

    2. The we add a very simple home page model.

    public class HomePageModel
    {
        public IEnumerable<Customer> Customers { get; set; }
    }

    3. We write an Index action in the Home controller.

    public ActionResult Index()
    {
        var model = new HomePageModel();
        model.Customers = LoadCustomers();
        return View(model);
    }

    4. We write a LoadCustomers method to get the data. For demo purposes just returns a static list of customers sorted by name. This would likely be factored into a data access layer in a real application.

    private static Customer[] customers = new Customer[]
    {
        new Customer() { Name = "John"},
        new Customer() { Name = "Bill"},
        …
    };
    private static IEnumerable<Customer> LoadCustomers()
    {
        return customers.OrderBy(x => x.Name);
    }

    5. We write a Index view in the Home folder to list the customers.

    @model PayloadDemo.Models.HomePageModel
    @{
        ViewBag.Title = "Payload Demo";
    }
    <h2>
        @ViewBag.Title</h2>
    <table>
        <thead>
            <tr>
                <th style="text-align:left; margin:0">
                    Name
                </th>
            </tr>
        </thead>
        <tbody>
        @foreach (var item in Model.Customers)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.Name)
                </td>
            </tr>
        }
        </tbody>
    </table>

    PART 1: Traditional Pagination

    That customer list could get pretty long, so we next we will want to add pagination.  We can start by doing using traditional request/response to page through the data.

    1. Wel update Index action in the Home controller to support pagination.  Notice that it sets the previous and next page numbers in the ViewBag.  Our view will use these later on.

    private const int pageSize = 5;
    public ActionResult Index(int pageNumber)
    {
        ViewBag.PreviousPageNumber = Math.Max(1, pageNumber + -1);
        ViewBag.NextPageNumber = pageNumber + 1;
        var model = new HomePageModel();
        model.Customers = LoadCustomers((pageNumber - 1) * pageSize, pageSize);
        if (pageNumber != 1 && model.Customers.FirstOrDefault() == null)
        {
            ViewBag.NextPageNumber = -1;
        }
        return View(model);
    }

    2. We update the LoadCustomers method to supports skip and top parameters.

    private static IEnumerable<Customer> LoadCustomers(int skip, int top)
    {
        return customers.OrderBy(x => x.Name).Skip(skip).Take(top);
    }

    3. We update the route entry in Global.asax.cs to support the pageNumber parameter.

    routes.MapRoute(
    "Default", // Route name
    "{controller}/{action}/{pageNumber}", // URL with parameters
    new { controller = "Home", action = "Index", pageNumber = 1 } // Parameter defaults
    );

    4. We add a div with action links to the Home/Index view.  The action links  use the ViewBag properties to navigate to the previous or next page.

    <div style="margin:5px">
        @Html.ActionLink("Previous", "Index", new { pageNumber = ViewBag.PreviousPageNumber })
        @if (ViewBag.NextPageNumber != -1)
        {
            @Html.ActionLink("Next", "Index", new { pageNumber = ViewBag.NextPageNumber })
        }
    </div>

    Now the user can page through data.  Notice the URL reflects the current page number.

    Part1

    Part 2: Pagination using AJAX and JSON

    The traditional MVC approach works, but the user is forced to refresh the entire page each time they navigate to the previous or next set of customers.  Modern web applications make an asynchronous AJAX request to populate the customer list with the paged data.

    1. We keep the changes we made to the Index action, LoadCustomers method, and routing table to support pagination for requests to the server.

    public ActionResult Index(int pageNumber)

    2. Wel add a new method to support returning paged customer data as JSON. Here is the new GetCustomers action method in the Home controller.

    [HttpPost]
    public JsonResult GetCustomers(int pageNumber)
    {
        return Json(LoadCustomers((pageNumber - 1) * pageSize, pageSize));
    }

    2. We replace the links in the Home/Index view with buttons.

    <div style="margin:5px">
        <button id="previous-page" style="width: 80px; height: 25px; display: inline;" >Previous</button>
        <button id="next-page" style="width: 80px; height: 25px; display: inline;" >Next</button>
    </div>

    3. We also add a data attribute and and ID to the Home/Index view.  This will help us call back using AJAX.

    <table id="customers" data-url="@Url.Action("GetCustomers", "Home")">

    4. We add a new JavaScript file called payload-demo.js to the project.

    Here is the payload-demo.js code.  This code tracks the current page number.  When the user clicks one of the buttons, we make an AJAX request and then repopulate the customer list with the data.

    $(function () {
        var currentPageNumber = 1;
    
        var populateCustomers = function(data) {
            var $customerList = $('#customers tbody');
            $customerList.children().detach();
            $.each(data, function (i, customer) {
                $('<td></td>').text(customer.Name)
                .appendTo($('<tr></tr>'))
                .parent()
                .appendTo($customerList);
            });
        };
    
       $("#next-page").click(function (event) {
            currentPageNumber += 1;
            var $customers = $('#customers');
            var url = $customers.data('url');
            url = url + '/' + currentPageNumber.toString();
            $.ajax({
                url: url,
                context: document.body,
                type: 'POST',
                success: function (data) {
                    if ($(data).length == 0)
                    {
                        currentPageNumber -= 1;
                        return;
                    }
                    populateCustomers(data);
                }
            });
        });
    
        $("#previous-page").click(function (event) {
            if (currentPageNumber == 1)
            {
                return;
            }
    
            currentPageNumber -= 1;
            var $customers = $('#customers');
            var url = $customers.data('url');
            url = url + '/' + currentPageNumber.toString();
            $.ajax({
                url: url,
                context: document.body,
                type: 'POST',
                success: function (data) {
                    populateCustomers(data);
                }
            });
        });
    });

    5. We add a reference to payload-demo.js to _Layout.cshtml so that each page gets the script.

    <script src="@Url.Content("~/Scripts/payload-demo.js")" type="text/javascript"></script>

    Now the user can navigate paged customer data without leaving the page. Notice that we’re on page 2, but the URL hasn’t changed.

    Part2

    Part 3: Pagination with AJAX and a JSON payload

    At this point, we’ve done the same work twice.

    To produce the initial page, we created a model for the home page, an action that loaded the data into the model and invoked the view, and the Razor syntax in the view to produce the table with rows and columns.

    To update the page, we created a JSON model, an action to return the JSON, and jQuery code to update the table with new rows and columns.

    We could decide to do everything with AJAX and JSON and get rid of the initial page code.  We could leave the customer table empty on the initial page response, and as soon as the page loads make a request for the first page of customer data.

    This works OK, but there can be a delay as the request is made.  The customer may see an empty customer list for a moment.  You could add a wait indicator, but this further delays getting data in front of the user and makes the application feel less responsive.

    Note: I never liked those pages where it loads and then a hundred wait spinners go nuts as I wait and wait for the data to show up. I forgive the spinners on Mint.com because they are asynchronously connecting to my bank accounts and they provide the most recent data immediately.

    You don’t have to make that extra web request! In this section, I’ll show you how to send down initial JSON data with the page as a payload and let the same script that populates the table with the results of the AJAX request, immediately populate the customer list.

    1. We update our Index action in the Home controller to put the JSON data into the ViewBag.  This uses the JavaScriptSerializer to turn the JsonResult from GetCustomers into a string.  Warning: Be sure to remember the .Data or it won’t serialize the right data.

    public ActionResult Index(int pageNumber)
    {
        ViewBag.CustomersPayload = new JavaScriptSerializer().Serialize(GetCustomers(pageNumber).Data);
        return View();
    }

    2. We can delete the HomePageModel and simplify our Home/Index view.  Notice the table is empty. We add a hidden div with a data attribute to the end of the page.  We put it at the end of the page so that the browser can doesn’t have to parse it as it is working to render the visible elements and so that we can delete it after we’ve used the data.

    @{
      ViewBag.Title = "Payload Demo";
    }
    <h2>@ViewBag.Title</h2>
    <table id="customers" data-url="@Url.Action("GetCustomers", "Home")">
      <thead>
        <tr>
          <th style="text-align:left; margin:0">Name</th>
        </tr>
      </thead>
      <tbody>
      </tbody>
    </table>
    <div style="margin:5px">
      <button id="previous-page" style="width: 80px; height: 25px; display: inline;" >Prev</button>
      <button id="next-page" style="width: 80px; height: 25px; display: inline;" >Next</button>
    </div>
    <div id="payload" style="display:none" data-customers="@ViewBag.CustomersPayload">

    3. We update our payload-demo.js script to use the payload.  We delete the div once we have used it to ensure we won’t apply stale data later when the user clicks a navigation button.

    var $payload = $('#payload');
        if ($payload != null) {
            populateCustomers($payload.data('customers'));
            $payload.detach();
    }

    Now we get the initial page populated and only have a single code path to write, debug, and maintain.

    Part3

    Wrap-Up

    If you inspect the client/server traffic in Fiddler or IE F12, you will see that the DNS resolution, proxy routing and connection negotiation are more expensive than the a few extra Kb of downloaded data.  This is even more true if your users are in other countries or on lower bandwidth connections.

    By including JSON payloads in your page, you can get the best of both worlds: Dynamic applications built using jQuery and AJAX and and immediate display of data with fewer asynchronous requests.

    Tags: , , , ,