Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

MVC pagination, filtering and sorting inside partial view with edit in modal dialog

0.00/5 (No votes)
3 Nov 2015 1  
Complete MVC example of pagination, filtering and sorting inside partial view with edit in modal dialog

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 () {
   //find ActionLink throuth its id 
   $('#btnCustomers').click(function () {
        //show loadmask when it is loading data
        $('#contentFrame').mask("waiting ...");
        //load data in to contentFrame DIV
        $('#contentFrame').load(this.href, function (response, status, xhr) {
            //to disappear loadmask after loading finished
            $('#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) {
    //prevent action link normal functionality
    e.preventDefault();
    $('#contentFrame').mask("waiting ...");
    //ajax call index controller action
    $.ajax({
        url: this.href,
        type: 'GET',
        cache: false,
        success: function (result) {
            $('#contentFrame').unmask("waiting ...");
            //load returned data inside contentFrame DIV
            $('#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:

//wire up all sort ActionLinks throuth their class name 
$(".SortButton").click(function (e) {
    e.preventDefault();
    $('#contentFrame').mask("waiting ...");
    $.ajax({
        url: this.href,
        type: 'POST',
        cache: false,
        success: function (result) {
            $('#contentFrame').unmask("waiting ...");
            //load returned data inside contentFrame DIV
            $('#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:

//find filter ActionLink throuth HTML5 attribute named btnName
$("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 () {
     //&hellip;&hellip;
     $.ajaxSetup({ cache: false });
     setDialogLink($('a[btnName=btnCreate]'),'Create Customer', 500, 600, "contentFrame", 
     //&hellip;..

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) {

    // Wire up the click event of any dialog links
    element.on('click', function () {

        // Generate a unique id for the dialog div
        var dialogId = 'uniqueName-' + Math.floor(Math.random() * 1000)
        var dialogDiv = "<div id='" + dialogId + "'></div>";

        // Load the form into the dialog div
        $(dialogDiv).load(this.href, function () {
            $(this).dialog({
                modal: true,
                resizable: false,
                title: dialogTitle,
                height: dialogHeight,
                width: dialogWidth,
                buttons: {
                    "Save": function () {
                        // Manually submit the form                        
                        var form = $('form', this);
                        $(form).submit();
                    },
                    "Cancel": function () {
                        $(this).dialog('close');
                    }
                },
                close: function () {

                    // It turns out that closing a jQuery UI dialog
                    // does not actually remove the element from the
                    // page but just hides it. For the server side
                    // validation tooltips to show up you need to
                    // remove the original form the page
                    $('#' + dialogId).remove();
                }
            });

            // Enable client side validation
            $.validator.unobtrusive.parse(this);

            // Setup the ajax submit logic
            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 () {

        // Do not submit if the form
        // does not pass client side validation
        if (!$(this).valid())
            return false;

        // Client side validation passed, submit the form
        // using the jQuery.ajax form
        $.ajax({
            url: this.action,
            type: this.method,
            data: $(this).serialize(),
            success: function (result) {
                // Check whether the post was successful
                if (result.success) {
                    // Close the dialog
                    $(dialog).dialog('close');

                    // Reload the updated data in the target div
                    $("#" + updateTargetId).load(updateUrl);
                    //$(this).dialog('destroy').remove()
                } else {
                    // Reload the dialog to show model errors                    
                    $(dialog).html(result);

                    // Enable client side validation
                    $.validator.unobtrusive.parse(dialog);

                    // Setup the ajax submit logic
                    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) {
                        //reload data inside contentFrame
                        $('#contentFrame').load("/Customers/Index");
                    }
                    else {
                        //show error message
                        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});
    }
}

 

 

 

 

 

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here