Introduction
Recently, I had to develop a single page application in which menu bar should have been on left side of the page and all entity lists should have been loaded as PartialView inside a HTML DIV element on the right side. Also all edit, create and detail views should have been loaded in modal dialog. I decided to combine all these functionalities in a complete MVC example and add other features like loadmask to it.
Using the code
Home page
Home Index view is made of three DIVs: logo, navigation and contentFrame like this:
<table>
<tr>
<td colspan="2">
<div id="logo" </div>
</td>
</tr>
<tr style="height:600px">
<td id="navigation" style="width:200px; ">
<div>
</div>
</td>
<td>
<div id="contentFrame" />
</td>
</tr>
</table>
navigation DIV contains menu bar and all PartialViews should be loaded inside contentFrame DIV. So We put an ActionLinke inside navigation DIV:
@Html.ActionLink("Customers", "Index", "Customers", new { },
new { id = "btnCustomers", @class = "btn btn-default btn-xs" })
Because we need to find this element via JavaScript, we can set its id attribute to btnCustomers. We need to wire up click event of ActionLink to call Index action of CustomersController and load data inside contentFrame:
$(function () {
$('#btnCustomers').click(function () {
$('#contentFrame').mask("waiting ...");
$('#contentFrame').load(this.href, function (response, status, xhr) {
$('#contentFrame').unmask("waiting ...");
});
return false;
});
});
Because it might take time to load PartialView, its beter to show a loadmask inside contentFrame when it is loading data:
$('#contentFrame').mask("waiting ...");
And after loading PartailView was finished, loadmask should be disappeared with this function call:
$('#contentFrame').unmask("waiting ...");
We need to include These JS and CSS files in web application:
~/Scripts/jquery.mask.js
~/Content/jquery.loadmask.css
You may put this two files inside BundleConfig.cs as I did. Also we should put this image file in application:
~/images/loading.gif
For more information about JQuery loadmask, please refer to this web address:
https://code.google.com/p/jquery-loadmask/
Pagination
For pagination you can use pagedlist.mvc package by installing it through NuGet package manager in your application. We need to put these lines of code on top of the customer Index View:
@using PagedList.Mvc
@model PagedList.IPagedList<CompleteMVCExample.Models.Customer>
After end of the view, we need to put page control:
Page @(Model.PageCount < Model.PageNumber ? 0 : Model.PageNumber) of @Model.PageCount
<div id="myPager">
@Html.PagedListPager(Model, page => Url.Action("Index",
new { page, sort = ViewBag.CurrentSort, search = ViewBag.CurrentSearch }))
</div>
And because pager is inside PartialView, We need to wire up pager click event to load data inside contentFrame:
$('#myPager').on('click', 'a', function (e) {
e.preventDefault();
$('#contentFrame').mask("waiting ...");
$.ajax({
url: this.href,
type: 'GET',
cache: false,
success: function (result) {
$('#contentFrame').unmask("waiting ...");
$('#contentFrame').html(result);
}
});
});
Again you can use mask and unmask functions to make your application more beautiful. On controller side, we need to add a nullable integer argument named page, to index controller action:
public ActionResult Index(int? page)
At the end of the index function we should set pageSize and pageNumber:
int pageSize = 3;
int pageNumber = page ?? 1;
And finally, ToList is changed to ToPagedList and two arguaments pageNumber and pageSize is passed to it and PartialView is returned instead of View:
return PartialView(customers.ToPagedList(pageNumber, pageSize));
Sorting
When header of each column is clicked for the first time, list is sorted based on that column on ascending order and if clicked again, list is sorted on descending order. So first we need to change column headers to ActionLink:
@Html.ActionLink("Customer Name", "Index", new { search = ViewBag.CurrentSearch,
sort = ViewBag.CurrentSort == "CustomerName_ASC" ? "CustomerName_DESC" : "CustomerName_ASC" },
new { @class = "SortButton" })
And then handle click event to call index action through ajax and load data in to contentFrame:
$(".SortButton").click(function (e) {
e.preventDefault();
$('#contentFrame').mask("waiting ...");
$.ajax({
url: this.href,
type: 'POST',
cache: false,
success: function (result) {
$('#contentFrame').unmask("waiting ...");
$('#contentFrame').html(result);
}
})
});
On controller side you can use any method to sort your list. Here I used this simple method:
if(!String.IsNullOrWhiteSpace(sort))
{
if(sort == "CustomerName_ASC")
customers = customers.OrderBy(c => c.CustomerName);
else if (sort == "CustomerName_DESC")
customers = customers.OrderByDescending(c => c.CustomerName);
else if (sort == "Address_ASC")
customers = customers.OrderBy(c => c.Address);
else if (sort == "Address_DESC")
customers = customers.OrderByDescending(c => c.Address);
else if (sort == "Phone_ASC")
customers = customers.OrderBy(c => c.Phone);
else if (sort == "Phone_DESC")
customers = customers.OrderByDescending(c => c.Phone);
}
else
{
customers = customers.OrderBy(c => c.CustomerName);
}
Filtering
I have selected only Customer Name to filter my list, so we need a textbox and a button in view:
<td>@Html.TextBox("search", ViewBag.CurrentSearch as string, new { @class = "form-control" }) </td>
<td>@Html.ActionLink("Filter", "Index", new { sort = ViewBag.CurrentSort, search = "xyz" },
new { @class = "btn btn-default", btnName = "FilterCustomer" })</td>
we need to wire up click event to call index action and if succeeded, load returned data inside contentFrame:
$("a[btnName=FilterCustomer]").click(function (e) {
e.preventDefault();
var search = $('input[name=search]').val();
this.href = this.href.replace('xyz', search);
$('#contentFrame').mask("waiting ...");
$.ajax({
url: this.href,
type: 'POST',
cache: false,
success: function (result) {
$('#contentFrame').unmask("waiting ...");
$('#contentFrame').html(result);
}
});
});
Create action
Create, Edit and Details controller actions are similar to each other, so I’m going to describe only create action. In the beginning of the view we define controller name, action and method for the form:
@using (Html.BeginForm("Create", "Customers", FormMethod.Post, new { id = "myForm" }))
Inside Customer Index view add btnName = "btnCreate" to Create ActionLink to be able to find this element through JQuery:
@Html.ActionLink("Create New", "Create", new { id = -1 },
new { btnName = "btnCreate", @class = "btn btn-default" })
We need to wire up its click event to call setDialogLink JavaScript function that is described later:
$(function () {
$.ajaxSetup({ cache: false });
setDialogLink($('a[btnName=btnCreate]'),'Create Customer', 500, 600, "contentFrame",
Function setDialogLink gets six arguments as follows:
- element: action link javascript object
- dialogTitle: modal dialog title
- dialogHeight: modal dialog height
- dialogWidth: modal dialog width
- updateTargetId: content DIV id
- updateUrl: index controller action URL that should be reloaded after saving data
and wires up click event to create modal dialog:
function setDialogLink(element, dialogTitle, dialogHeight, dialogWidth, updateTargetId, updateUrl) {
element.on('click', function () {
var dialogId = 'uniqueName-' + Math.floor(Math.random() * 1000)
var dialogDiv = "<div id='" + dialogId + "'></div>";
$(dialogDiv).load(this.href, function () {
$(this).dialog({
modal: true,
resizable: false,
title: dialogTitle,
height: dialogHeight,
width: dialogWidth,
buttons: {
"Save": function () {
var form = $('form', this);
$(form).submit();
},
"Cancel": function () {
$(this).dialog('close');
}
},
close: function () {
$('#' + dialogId).remove();
}
});
$.validator.unobtrusive.parse(this);
wireUpForm(this, updateTargetId, updateUrl);
});
return false;
});
}
wireUpForm function is called inside setDialogLink to wire up submit funtion of the form inside modal dialog:
function wireUpForm(dialog, updateTargetId, updateUrl) {
$('form', dialog).submit(function () {
if (!$(this).valid())
return false;
$.ajax({
url: this.action,
type: this.method,
data: $(this).serialize(),
success: function (result) {
if (result.success) {
$(dialog).dialog('close');
$("#" + updateTargetId).load(updateUrl);
} else {
$(dialog).html(result);
$.validator.unobtrusive.parse(dialog);
wireUpForm(dialog, updateTargetId, updateUrl);
}
}
});
return false;
});
}
We don't need to change HttpPost Create action more. Here we need to return json object when it saves data successfully:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind(Include = "CustomerID,CustomerName,Phone,Address")] Customer customer)
{
if (ModelState.IsValid)
{
db.Customers.Add(customer);
db.SaveChanges();
return Json(new {success = true});
}
return PartialView(customer);
}
Delete
We set btnName attribute of Delete ActionLink to btnDelete to find it through JavaScript:
@Html.ActionLink("Delete", "Delete", new { id = item.CustomerID },
new { @class = "btn btn-default btn-xs" , btnName="btnDelete"})
Then wire up its click event to call Delete controller action. Here we don't need to open a modal dialog but we need to get confirm of user about deleting data. for this reason we use a standard javascript confirm function:
$('a[btnName=btnDelete]').click(function (e) {
e.preventDefault();
var confirmResult = confirm("Are you sure?");
if (confirmResult) {
$('#contentFrame').mask("waiting ...");
$.ajax(
{
url: this.href,
type: 'POST',
data: JSON.stringify({}),
dataType: 'json',
traditional: true,
contentType: "application/json; charset=utf-8",
success: function (data) {
if (data.success) {
$('#contentFrame').load("/Customers/Index");
}
else {
alert(data.errormessage);
}
$('#contentFrame').unmask("waiting ...");
},
error: function (data) {
alert("An error has occured!!!");
$('#contentFrame').unmask("waiting ...");
}
});
}
})
Inside HttpPost Delete action we need to return json object when even if it fails to delete record.
[HttpPost]
public ActionResult Delete(int id)
{
try
{
var customer = db.Customers.Find(id);
db.Customers.Remove(customer);
db.SaveChanges();
return Json(new {success = true, Message =""});
}
catch (Exception exp)
{
return Json(new { success=false, ErrorMessage=exp.Message});
}
}