How to I apply filter while paginating in Asp.net MVC and entity Framework?
I have a web app that is written using ASP.NET MVC framework. In my Homecontroller
I have an action called Index
which responds to a Get
request. In this action, I create pages using IPagedList
library to break the records into multiple pages. My Index@HttpGet
looks something like this
public ActionResult Index(int? id)
{
using(var connection = new Context())
{
int pageNumber = (id ?? 1);
var presenter = new Presenter
{
Presenter = pageNumber,
Tasks = connection.Tasks.ToPagedList(pageNumber, 30),
Form = new TasksFiltersViewModel()
}
return View(presenter);
}
}
I also have an action called Index
that respond to the Post
request which apply some filters. So in the Post
request I do something like this
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Index(Presenter model)
{
int pageNumber = (id ?? 1);
if (ModelState.IsValid)
{
using(var connection = new Context())
{
model.Tasks = connection.Tasks
.Where(task => task.Status == 5)
.ToPagedList(pageNumber, 30);
}
}
return View(model);
}
This also work fine unless the user changed the page then the filters are rest.
Here is how my presentation class looks like
public class Presenter
{
public IPagedList<Task> Tasks { get; set; }
public TasksFiltersViewModel Form { get; set; }
public int PageNumber { get; set; }
public IEnumerable<SelectListItem> Statuses { get; set; }
}
How can I allow the users to use the pages while keeping the filters?
Here is my filters VM
public class TasksFiltersViewModel
{
public int Status { get; set; }
}
The view looks like this
@using (Html.BeginForm("Index", "Tasks", FormMethod.Post, new { @class = "form-horizontal" }))
{
@Html.AntiForgeryToken()
<div class="form-group">
@Html.LabelFor(m => m.Form.Status, new { @class = "control-label col-sm-3" })
<div class="col-sm-9">
@Html.DropDownListFor(m => m.Form.Status, Model.Statuses, new { @class = "form-control" })
@Html.ValidationMessageFor(m => m.Form.Status, "", new { @class = "text-danger" })
</div>
</div>
<div class="row">
<div class="col-sm-9 col-md-push-3">
<div>
<button type="submit" class="btn btn-default">Filter</button>
</div>
</div>
</div>
}
foreach (var task in Model.Tasks)
{
<tr>
<td>@task.Name</td>
<td>@task.Type</td>
<td>@Html.ActionLink("Edit", "Details", "Task", new { @id = task.Id }, new { @class = "btn btn-primary btn-sm" })</td>
</tr>
}
@Html.PagedListPager(Model.Tasks, id => Url.Action("Index", new { id }))
Your form needs to post back to the GET method, and that method needs to include parameters for your filter properties. Your PagedListPager
code in the view also need to include those filter properties so they are retained when you navigate to the next/previous page. Note that the Index()
POST method is not used and can be deleted.
Having your model contain a complex object to the filter properties and extra complexity when binding, so start by changing your model to
public class Presenter
{
public IPagedList<Task> Tasks { get; set; }
public int? Status { get; set; } // note nullable
... // add any other properties of TasksFiltersViewModel
public int PageNumber { get; set; }
public IEnumerable<SelectListItem> Statuses { get; set; }
}
Then change the Index()
method to
public ActionResult Index(int? id, int? status) // add any other parameters your filtering on
{
int pageNumber = (id ?? 1);
var tasks = db.Tasks; // IQueryable<Task>
if (status.HasValue)
{
tasks = tasks.Where(x => x.Status == status.Value)
}
if (otherParametersHaveValue)
{
tasks = tasks.Where(....);
}
Presenter model = new Presenter()
{
PageNumber = id ?? 1,
Status = status,
.... // set any other filter properties from the parameters
Statuses = new SelectList(...),
Tasks = tasks.ToPagedList(pageNumber, 30)
};
return View(model );
}
and change the view to
// Make the form a GET
@using (Html.BeginForm("Index", "Tasks", FormMethod.Get, new { @class = "form-horizontal" }))
{
....
// Modify expression based on revised model properties
@Html.DropDownListFor(m => m.Status, Model.Statuses, ...)
}
....
// Add filter parameters to url so they are retained
@Html.PagedListPager(Model.Tasks, id => Url.Action("Index", new { id, status = Model.Status })) // add other filter properties as required
I think a better way should be passing back to the view the filters in ViewBag
.
You can make something like below:
@Html.PagedListPager(
Model.Tasks, id =>
Url.Action("Index", new { id,
Status = ViewBag.Status , AnotherFilterValue = ViewBag.AnotherFilterValue, ... }))
But keep in mind to test ViewBag.Status
for null value. If it does have a value put it in the route parameter list else set a default ActionLink
.
Then inside the POST action you expect a nullable int like below:
public ActionResult Index(int? id, int? status, ...)
{
int pageNumber = (id ?? 1);
if (ModelState.IsValid)
{
using(var connection = new Context())
{
if(status != null)
{
ViewBag.Status = status.value;
model.Tasks = connection.Tasks
.Where(task => task.Status == status.value)
.ToPagedList(pageNumber, 30);
}
else
{
model.Tasks = connection.Tasks
.ToPagedList(pageNumber, 30);
}
}
}
return View(model);
}