ASP.NET MVC: Enhancing The WebGrid - Lazy Loading with WebAPI
Part four of our WebGrid series answers a reader's question of how difficult it is to make a WebGrid lazy load records.
In the last post, I showed you how to export WebGrid results to CSV. This week, we reply to a reader's question as how to perform a lazy load on a WebGrid.
For those who aren't familiar with a lazy-load, a lazy-load is a process that is more ad-hoc (records pulled upon request) that all at once and displayed without paging.
Google+ and Pinterest users should be familiar with this type of retrieving records. As you scroll further down the list, a new set of records appear. It's almost considered a never ending list of content (Has anyone EVER seen the footer of Facebook?)
Today, we get into a WebGrid that can have a large supply of users, but we are only displaying 5 users at a time.
Benefits: Why do this?
Why would we implement such a feature into a Web Grid?
- Minimal amount of bandwidth
- Quicker access pulling smaller records (easier to pull 5 than 500)
- Depending on how you display your records and if it's a sort by relevance, there's no need to go to page 2 or 3, meaning you are saving CPU cycles by not requesting another page of data. Ask Google!
Of course, we could also include paging as a lazy-loading technique which would accomplish the same thing, but as I said in the first WebGrid Series post, I want to push the limits and show all of the crazy things we can do with a WebGrid to impress our users.
Overview
For those wondering how to do this, the only way to load the records without doing a postback is to implement AJAX with jQuery. We definitely don't want to interrupt the user's experience with a hard postback.
First, we need to focus our efforts on the initial display of records in the WebGrid. The two immediate pieces of code that stand out are the UserRepository and the UserController.
In the UserController, we need to make sure we can display the first set of records with paging. So we replace this:
// GET: User public ActionResult Index() { var model = new UserViewModel { Users = _repository.GetAll() }; return View(model); }
with this:
// GET: User public ActionResult Index(int? page) { var defaultPageSize = 5; var model = new UserViewModel { Users = _repository.GetPagedUsers(page, defaultPageSize) }; return View(model); }
If you wanted to, you can even remove the page parameter and hard-code a '1' into the GetPagedUsers method so the user doesn't page ahead.
Next (and it should be obvious) is the UserRepository. We need to create a paging method and adjust our interface.
Our interface now looks like this:
using System.Collections.Generic; using WebGridExample.Models; namespace WebGridExample.Interface { public interface IUserRepository: IRepository<User> { User GetById(int id); IEnumerable<User> GetPagedUsers(int? pageIndex, int pageSize); } }
and we add our implementation of the method in UserRepository.
using System.Collections.Generic; using System.Data.Entity; using System.Linq; using WebGridExample.Interface; using WebGridExample.Models; namespace WebGridExample.Repository { public class UserRepository: Repository<User>, IUserRepository { public UserRepository() : this(new UserContext()) { } public UserRepository(DbContext context) : base(context) { } public User GetById(int id) { return First(e => e.Id == id); } public IEnumerable<User> GetPagedUsers(int? page, int pageSize) { var pageIndex = page-1 ?? 0; return GetAll() .OrderBy(e=> e.UserName) .Skip(pageIndex*pageSize) .Take(pageSize) .ToList(); } } }
For demo purposes, I'm using a quick paging technique for Entity Framework (Notice the OrderBy? It has to be there for the Skip to work).
Web API or SignalR?
You may be wondering if we aren't doing a postback with ASP.NET MVC, then how do we get the data?
Ahhh, the joys of being a developer...so many choices.
There are two choices: Web API or SignalR.
While Web API is not new, it is in the latest version of ASP.NET MVC. In vNext, Microsoft combined MVC, Web API, Bootstrap, and ASP.NET Identity and are calling it One ASP.NET.
SignalR is another library that I've grown quite fond of since Codemash 2012. SignalR is a two-way communication library that uses Web Sockets. When one client sends a request back to the server, the server acts on it and then optionally broadcasts actions to the every client that is connected. If you want to see an example of SignalR, check out this post about making your own real-time like button.
Both are viable solutions and it depends on your use.
Use WebAPI if:
- You have other pieces of code that will call this API (code reuse)
- The ultimate decoupling of components (make a web call and you get data)
- You want to create Single Page Applications (no postback required)
Use SignalR if:
- You are using a more coupled architecture (it can't be a separate library)
- You require a real-time application where everyone connected has to be notified of events (100,000 simultaneous connections can be achieved)
- You want to create Single Page Applications (yes, I said it again)
Today, I will show you how to use the Web API in your WebGrid and then focus on SignalR later in the week.
Web API Implementation
After creating a new item (under Add Item -> Web API Controller Class (v2.1)) called UserController.cs, we get our template.
api\UserController.cs
using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; using System.Web.Http; namespace WebGridExample { public class UserController : ApiController { // GET api/<controller> public IEnumerable<string> Get() { return new string[] { "value1", "value2" }; } // GET api/<controller>/5 public string GetPaging(int id) { return "value"; } // POST api/<controller> public void Post([FromBody]string value) { } // PUT api/<controller>/5 public void Put(int id, [FromBody]string value) { } // DELETE api/<controller>/5 public void Delete(int id) { } } }
Ok, notice the GetPaging? We'll be renaming that method signature and adding our own return values. But before we do that, we need to make a couple of modifications to this project.
- Add a reference to System.Web.Http.WebHost
- Add App_Start\WebApiConfig.cs
using System.Linq; using System.Web.Http; using Newtonsoft.Json; namespace WebGridExample { public class WebApiConfig { public static void Register(HttpConfiguration configuration) { configuration.Routes.MapHttpRoute("API Default", "api/{controller}/{id}", new { id = RouteParameter.Optional }); } } }
- Call WebApiConfig.Register(GlobalConfiguraton.Configuration) in your Application_Start() in file Global.asax.cs
- Because we are adding this Web API to an existing project, we need to include some modifications to our output. Let's remove the XML returned from the WebAPI.
using System.Linq; using System.Web.Http; using Newtonsoft.Json; namespace WebGridExample { public class WebApiConfig { public static void Register(HttpConfiguration configuration) { configuration.Routes.MapHttpRoute("API Default", "api/{controller}/{id}", new { id = RouteParameter.Optional }); // Remove the xml so we get back JSON var appXmlType = configuration.Formatters.XmlFormatter.SupportedMediaTypes.FirstOrDefault(t => t.MediaType == "application/xml"); GlobalConfiguration.Configuration.Formatters.XmlFormatter.SupportedMediaTypes.Remove(appXmlType); } } }
- And finally, we need to set the JSON serialization settings for our User object or else we get some wierd "K__BackingField" issues returned to our browser.
using System.Linq; using System.Web.Http; using Newtonsoft.Json; namespace WebGridExample { public class WebApiConfig { public static void Register(HttpConfiguration configuration) { configuration.Routes.MapHttpRoute("API Default", "api/{controller}/{id}", new { id = RouteParameter.Optional }); // Remove the xml so we get back JSON var appXmlType = configuration.Formatters.XmlFormatter.SupportedMediaTypes.FirstOrDefault(t => t.MediaType == "application/xml"); GlobalConfiguration.Configuration.Formatters.XmlFormatter.SupportedMediaTypes.Remove(appXmlType); // Remove the k__backingfield issue in the names. JsonSerializerSettings jSettings = new JsonSerializerSettings(); GlobalConfiguration.Configuration.Formatters.JsonFormatter.SerializerSettings = jSettings; } } }
Let's finish up our UserApi Controller.
The finished template looks like this:
using System.Collections.Generic; using System.Web.Http; using WebGridExample.Interface; using WebGridExample.Models; using WebGridExample.Repository; namespace WebGridExample.Api { public class UserController : ApiController { private readonly IUserRepository _repository; public UserController() : this(new UserRepository()) { } public UserController(IUserRepository repository) { _repository = repository; } // GET api/<controller> (Used for a test to make sure it works) public IEnumerable<string> Get() { return new string[] { "value1", "value2" }; } // GET api/<controller>/5 public IEnumerable<User> GetPaging(int page, int pageSize) { return _repository.GetPagedUsers(page, pageSize); } } }
A lot of work for something so small, huh?
Now, when you run the app, you should be able to type:
http://localhost:4537/api/user/GetPaging?page=1&pageSize=5
and receive records in JSON.
Let's head to the client and finish the job.
Working the Client with jQuery
The client side should be relatively easy since we just need to plug in an AJAX call to our new Web API service and we already have a JavaScript file already started in our User\Index.cshtml View.
But we seem to have a problem with our thinking. How do we keep track of what page we're on when we're ready to make a call to the server? We can use the number of rows displayed to calculate the page number.
Here is our updated JavaScript:
<script type="text/javascript"> $(function () { function formatDate(date) { var hours = date.getHours(); var minutes = date.getMinutes(); var seconds = date.getSeconds(); var ampm = hours >= 12 ? 'pm' : 'am'; hours = hours % 12; hours = hours ? hours : 12; // the hour '0' should be '12' minutes = minutes < 10 ? '0' + minutes : minutes; seconds = seconds < 10 ? '0' + seconds : seconds; var strTime = hours + ':' + minutes + ':'+seconds+' ' + ampm.toUpperCase(); return date.getMonth() + 1 + "/" + date.getDate() + "/" + date.getFullYear() + " " + strTime; } $("#allBox").on("click", function () { $("[name=select]").prop("checked", $("#allBox").is(":checked")); }); $("#moreButton").click(function() { var pageSize = 5; var getNextPageNo = ($("#grid tbody tr").size() / pageSize) + 1; var row = "<tr><td class=\"text-center checkbox-width\"></td><td></td><td></td><td></td><td></td></tr>"; $.getJSON('/api/User/GetPaging?page=' + getNextPageNo + "&pageSize=" + pageSize, function (users) { if (users.length > 0) { $.each(users, function(index, user) { $('#grid tbody').append($(row)); var bottomRow = $("#grid tbody tr:last"); $("td:nth-child(1)", bottomRow).html("<input id=\"select\" class=\"box\" name=\"select\" " + "type=\"checkbox\" value=\"" + user.Id + "\">"); $("td:nth-child(2)", bottomRow).html(user.UserName); $("td:nth-child(3)", bottomRow).html(user.FirstName); $("td:nth-child(4)", bottomRow).html(user.LastName); var formattedDate = formatDate(new Date(user.LastLogin)); $("td:nth-child(5)", bottomRow).html(formattedDate); }); } else { $(".more-button").html("<div class=\"alert alert-info\">No more records.</div>"); } }); }); }); </script>
Let's go through the moreButton click.
To calculate the next page, we take the list of TR elements in the body and divide it by the page size which we set to 5 in the previous line and then add 1 to request the next page.
After the calculation, we create a row template. This template has the very basics of our row without data included but with CSS formatting.
Next, we execute the Web API GetPaging method and get our JSON User records back. If we receive ANY user records, we populate the WebGrid. If we don't receive any records, we replace the Load More button with an alert that says "No More Records."
Conclusion
In today's post, we went over how to perform lazy loading in a WebGrid using the Web API and jQuery to achieve quick loading results to your WebGrid.
This makes the user's experience even better because they don't have to wait for a huge HTML View to return. They just wait for data to return and the browser takes care of the REST (Get it?) ;-)
If you have any question regarding how to implement something in a WebGrid, don't hesitate to comment.