Recently I had the challenge on a project to create a grid for my MVC ASP.NET web application that could sort and paginate on the page, but all population of the grid happened from the controller (server). Also, the grid had to be updated asynchronously without rendering the rest of the page.
In the old ASP.NET world you could just use a GridView stuck inside an AJAX UpdatePanel control and it would work. What exactly the UpdatePanel was doing (or the GridView for that matter) is pretty much a mystery to the developer, but for the most part it works.
In the MVC world you need to have a little bit more understanding of the web request/response and Ajax world in order to get this same thing to work. Especially since there are not that many controls out for MVC yet.
An opensource codeplex group callled MVCContrib has created a set of useable controls (extensions) that can generate HTML from code. One of these controls is the Grid control. This grid control is so flexible (not to mention you have the source code), that if used creatively, you can get it to solve the problem I mentioned above. That is, you can get the MVCContrib Grid control to populate in an "ajax manner" and do sorting and paging in-place. Meanwhile, getting all of it's data by calling the controller.
The way to do this is to construct your MVC content page as you normally would and leave a blank div for the grid, let's id it as "order_grid"
<h1>Simple Grid</h1>
<div id="order_grid" >
</div>
Next we will create a separate 'component' page that holds the mvccontrib grid itself (called OrderGrid.aspx). I also added a pager from Suteki Extensions (the built in MVCContrib pager won't work here).
<div id="grid1">
<%
Html.Grid<
Orders>("orders",
column => {
column.For(c => c.Text,
String.Format("<a id=\"col{0}\" href=\"javascript:Sort('{0}')\">Order</a>", "Text"));
column.For(c => c.CreateDate,
"receive date");
column.For(c => c.CategoryID,
"type");
column.For(
"Edit").Do(c => { %>
<td class="unProcessed" id=Process<%= c.ID %>><a href="javascript:Process(<%= c.ID %>)">Claim</a>
</td>
<%}); }
);
%>
<%
= Html.Pager((IPagedList)ViewData["orders"])%>
</
div>
Populating the Grid
First we will create a method in the controller to initially populate the order grid with orders on the OrderPage.
public ActionResult GetOrderGrid(int? page)
{
if (page == null)
page = 0;
_currentPage = page;
DateTime date = new DateTime(2008, 08, 19);
var orderDB = new ordersDataContext();
var orders = orderDB .GetOrders("test");
int numOrders = (orders as IEnumerable<Order>).ToList().Count();
List<Order> orderList = (orders as IEnumerable<Order>).Skip((int)(30 * _currentPage)).Take(30).ToList();
IPagedList ordersOnPage = orderList.ToPagedList(_currentPage.GetValueOrDefault(), 30, numOrders);
ViewData[
"orders"] = ordersOnPage;
return View("OrderGrid");
}
This populates the ViewData needed to fill the grid with 30 orders, depending on what page we are in. Notice that all the pagination logic is happening in the server and not the client.
Now we need a way to tell the OrderPage to populate the empty div tag by calling GetOrderGrid. We also need to make sure that it only populate the div tag and doesn't rerender the screen. This is accomplished through the magic of jquery which nicely raps ajax calls. because this is the initial population of the grid, we want to make the call on the jquery ready method that automatically gets called when the page is finished rendering its response.
<script type="text/javascript" language="javascript">
$(document).ready(
function() {
// Your code here
$.ajax({
type:
"POST",
url:
"/GridTest/GetOrderGrid",
success:
function(msg) {
$(
"#order_grid").append(msg);
},
error:
function(msg) {
debugger;
}
});
});
The ajax call will call GetOrderGrid in the controller, and upon success it will call the success callback. The callback just appends the html generated by the GetOrderGrid call into the order_grid div directly in the DOM.
Sorting
In order to follow an ajax strategy, when a header column is pressed in the grid, we only want the grid to sort in-place, and we want the grid to do its sorting in the controller. In order to do any ajax, we need to attach a javascript call to the link of our Order header instead of, for example, a url directly to the controller. A url to the controller would render the whole page, and we don't want this behavior.
Looking back at the mvccontrib grid code we notice that the link is shown as follows
column.For(c => c.Text,
String.Format("<a id=\"col{0}\" href=\"javascript:Sort('{0}')\">Order</a>", "Text"));
The Text column has a link to call a javascript function called sort.
Here is what that function looks like in our OrderGrid:
function Sort(col) {
$.ajax({
type:
"POST",
url:
"/GridTest/Sort",
data:
"columnName=" + col,
success:
function(msg) {
$(
"#order_grid").html(msg);
},
error:
function(msg) {
alert(
"Data Failed to Sort: " + msg);
}
});
}
Note the Sort method doesn't do any sorting, it simply calls the controller through an ajax call. In the success callback it appends html returned by the controller into the div of the order grid.
Here is the Sort method of the controller:
public ActionResult Sort(string columnName)
{
if (columnName == null)
return View();
_currentColumn = columnName;
var orderDB = new ordersDataContext();
var orders= orderDB .GetOrdersSorted(_currentColumn);
int numOrders = (orders as IEnumerable<Order>).ToList().Count();
List<Order> orderList = (orders as IEnumerable<Order>).Skip((int)(30*_currentPage)).Take(30).ToList();
IPagedList ordersOnPage = orderList.ToPagedList(_currentPage.GetValueOrDefault(), 30, numOrders );
ViewData[
"orders"] = ordersOnPage; //.AsPagination(_currentPage ?? 1, 10);
return View("OrderGrid");
}
After sorting the orders in the database, it gets only the orders to be presented on the current page and returns it in the View call. Using the MVCContrib grid, the View then renders the html with the sorted orders.