Listing, Sorting, and Paging Through Tables - Working with Data - ASP.NET MVC 5 with Bootstrap and Knockout.js (2015)

ASP.NET MVC 5 with Bootstrap and Knockout.js (2015)

Part II. Working with Data

Chapter 5. Listing, Sorting, and Paging Through Tables

It’s time to fully integrate MVC, Bootstrap, and Knockout.js together in one example. This will be done by creating a new controller, views, and data binding with Knockout using the data that was prepopulated in Chapter 4. To aid in stubbing out the controllers and views, I am going to use Visual Studio’s scaffolding functionality.

Scaffolding is why I choose MVC and Entity Framework in my projects. When I use scaffolding, I find that I can rapidly create websites that provide basic CRUD (Create-Read-Update-Delete) functionality on any model.

This chapter will extend on the Code First DbContext and models created in Chapter 4.

Scaffolding the Author Model

Scaffolding involves creating a new controller. To do this, right-click the Controllers folder and select Add → Controller. This will start a wizard to create the new controller. In the first step, select MVC 5 Controller with views, using Entity Framework, and click Add to continue.

The Add Controller window will now be displayed (shown in Figure 5-1). It requires three pieces of configuration to complete the scaffolding process:

§ For the model class, select Author (BootstrapIntroduction.Models).

§ For the data context class, select BookContext (BootstrapIntroduction.DAL). When you selected the model, the controller name field was automatically populated with AuthorsController. Leaving as-is is perfect.

§ The Use a layout page option is left checked and empty because the default layout will be used unless otherwise specified. Because there is only one default layout in this project, you don’t need to change it.

Figure 5-1. Creating the AuthorsController

Click Add to finish creating the AuthorsController and its related views. Visual Studio will now scaffold the controller and the views for the new controller.

Error Creating Controller?

During my first attempt, I received an error creating the controller because I had the same models twice; once in the Code First and once in the Database First. Because I no longer require the EDMX from the Database First example, I have deleted this file and its child files.

Along with the AuthorsController, Visual Studio also created a new folder called Authors under the Views directory. Inside this directory are five different views. Each view is used for a different piece of the CRUD process (with the exception of Index and Details because they both are used in the “R”).

Example 5-1 contains the scaffolded AuthorsController that Visual Studio created.

Example 5-1. The AuthorsController

using System;

using System.Collections.Generic;

using System.Data;

using System.Data.Entity;

using System.Linq;

using System.Net;

using System.Web;

using System.Web.Mvc;

using BootstrapIntroduction.DAL;

using BootstrapIntroduction.Models;

namespace BootstrapIntroduction.Controllers

{

public class AuthorsController : Controller

{

private BookContext db = new BookContext();

// GET: Authors

public ActionResult Index()

{

return View(db.Authors.ToList());

}

// GET: Authors/Details/5

public ActionResult Details(int? id)

{

if (id == null)

{

return new HttpStatusCodeResult(HttpStatusCode.BadRequest);

}

Author author = db.Authors.Find(id);

if (author == null)

{

return HttpNotFound();

}

return View(author);

}

// GET: Authors/Create

public ActionResult Create()

{

return View();

}

// POST: Authors/Create

[HttpPost]

[ValidateAntiForgeryToken]

public ActionResult Create(

[Bind(Include = "Id,FirstName,LastName,Biography")] Author author)

{

if (ModelState.IsValid)

{

db.Authors.Add(author);

db.SaveChanges();

return RedirectToAction("Index");

}

return View(author);

}

// GET: Authors/Edit/5

public ActionResult Edit(int? id)

{

if (id == null)

{

return new HttpStatusCodeResult(HttpStatusCode.BadRequest);

}

Author author = db.Authors.Find(id);

if (author == null)

{

return HttpNotFound();

}

return View(author);

}

// POST: Authors/Edit/5

[HttpPost]

[ValidateAntiForgeryToken]

public ActionResult Edit(

[Bind(Include = "Id,FirstName,LastName,Biography")] Author author)

{

if (ModelState.IsValid)

{

db.Entry(author).State = EntityState.Modified;

db.SaveChanges();

return RedirectToAction("Index");

}

return View(author);

}

// GET: Authors/Delete/5

public ActionResult Delete(int? id)

{

if (id == null)

{

return new HttpStatusCodeResult(HttpStatusCode.BadRequest);

}

Author author = db.Authors.Find(id);

if (author == null)

{

return HttpNotFound();

}

return View(author);

}

// POST: Authors/Delete/5

[HttpPost, ActionName("Delete")]

[ValidateAntiForgeryToken]

public ActionResult DeleteConfirmed(int id)

{

Author author = db.Authors.Find(id);

db.Authors.Remove(author);

db.SaveChanges();

return RedirectToAction("Index");

}

protected override void Dispose(bool disposing)

{

if (disposing)

{

db.Dispose();

}

base.Dispose(disposing);

}

}

}

The AuthorsController, just like the HomeController extends the Controller class. However, unlike the HomeController, each method is not quite so empty. Visual Studio has created all of the necessary methods and functionality to perform CRUD on the authors.

This chapter will focus strictly on the Index function which — using the BookContext — fetches the complete list of authors. The results of this are then passed to the Index.cshtml view.

A private variable called db is instantiated as a new BookContext. This will happen at the start of each request from the web browser. The BookContext opens a connection to the database, so it is important that at the end of the request, the BookContext is disposed of properly, closing all open connections to prevent memory leaks. This is done by overriding the Dispose function from the base Controller class. Inside the function, the db variable is disposed. The Dispose function is called by MVC at the end of each request.

Fetching the list of authors is done by accessing the Authors DbSet from the BookContext and calling ToList.

ToList

The access to the Authors DbSet is followed by a call to the ToList function. This is an important concept when working with Entity Framework. When you are interacting with a DbSet, EF doesn’t execute any database queries until the data is actually accessed in code. By calling ToList, this tells EF to execute the query and populate the list of authors in a list.

Prior to specifying the ToList function, you can add filters to the DbSet to limit the results. Each time you do this, EF updates the query that will be sent to the database server. Once the data has been queried from the database, any further manipulations to the results will be done strictly on the in-memory object and not against the database.

So far, the M and C have been implemented. Let’s complete this example by implementing the V and the VM. The default Index view that was created by Visual Studio uses a Razor foreach function to show the data. Example 5-2 will change this and implement similar functionality by using the Knockout foreach binding to create the table of author data.

Example 5-2. HTML to create table

@model IEnumerable<BootstrapIntroduction.Models.Author>

@{

ViewBag.Title = "Authors";

}

<h2>Authors</h2>

<p>@Html.ActionLink("Create New", "Create")</p>

<table class="table table-bordered table-striped">

<thead>

<tr>

<th>@Html.DisplayNameFor(model => model.FirstName)</th>

<th>@Html.DisplayNameFor(model => model.LastName)</th>

<th>Actions</th>

</tr>

</thead>

<tbody data-bind="foreach: authors">

<tr>

<td data-bind="text: FirstName"></td>

<td data-bind="text: LastName"></td>

<td>

<a data-bind="attr: { href: '@Url.Action("Details")/' + Id }"

class="btn btn-info">Details</a>

<a data-bind="attr: { href: '@Url.Action("Edit")/' + Id }"

class="btn btn-primary">Edit</a>

<a data-bind="attr: { href: '@Url.Action("Delete")/' + Id }"

class="btn btn-danger">Delete</a>

</td>

</tr>

</tbody>

</table>

The view starts with the model binding. In this example, it is an IEnumerable of the Author model. The next important piece is the definition of the table of data. Instead of using a Razor foreach loop to display each author, inside the tbody tag there is an HTML attribute called data-bind that is defined as foreach: authors. When the Knockout bindings are applied, the block of HTML code contained within the tbody will be repeated for each element in the list.

Inside the tbody is a td tag for each column in the table. The td tag is using the text data binding for each column. And finally, three buttons are created allowing you to navigate to the details, edit, and delete of that author. Each button is using another new data binding called attr. This binding lets you create any HTML attribute. In this example, it is setting the href attribute for each link. This is a great example of mixing Razor with Knockout. The UrlHelper is used to generate the link to Authors/Edit and then the author ID is appended to the end of the link from the Knockout binding.

More code is required to complete this example. Example 5-3 should be added to the Authors/Index view after the end of the table.

Example 5-3. The ViewModel

@section Scripts {

<script>

function ViewModel(authors) {

var self = this;

self.authors = authors;

};

var viewModel = new ViewModel(@Html.HtmlConvertToJson(Model));

ko.applyBindings(viewModel);

</script>

}

This example creates a new ViewModel that accepts an array of authors in its constructor. This array is then assigned to a local property also called authors. This property is what the view was data bound to in the foreach binding.

The ViewModel is then instantiated with the model that was provided from the controller. The model is converted into a JavaScript array by calling a custom HtmlHelper extension (shown in Example 5-4).

Example 5-4. HtmlHelper extension

using Newtonsoft.Json;

using System.Web;

using System.Web.Mvc;

public static class HtmlHelperExtensions

{

public static HtmlString HtmlConvertToJson(this HtmlHelper htmlHelper,

object model)

{

var settings = new JsonSerializerSettings

{

ReferenceLoopHandling = ReferenceLoopHandling.Ignore,

Formatting = Formatting.Indented

};

return new HtmlString(JsonConvert.SerializeObject(model, settings));

}

}

This example should be placed inside a new file called HtmlHelperExtension inside a new folder called Extensions. This extension method accepts a model object and uses a third-party library to serialize the data into a JavaScript object. The third-party library is called Json.Net and should be installed via the NuGet Package Manager. To install the package, follow these steps:

1. Right-click the Project and select Manage NuGet Packages.

2. If it is not already selected, on the left, choose Online.

3. Select Json.Net. (This is an extremely popular package, and is typically second in the list.)

4. Click the Install button to add this to your project.

Running the completed example will show results similar to Figure 5-2.

Figure 5-2. The list of authors

Sorting the Authors

The default functionality after scaffolding the authors is quite nice, but there are definitely some missing pieces, such as sorting and paging through the results. This section will update the controller and view to provide the ability to sort the authors.

Sorting data involves manipulating the DbSet with LINQ statements. For example, to return the list of authors sorted by first name, you would use db.Authors.OrderBy(a => a.FirstName).ToList().

When using LINQ, it is strongly typed to your model. Many tutorials would then demonstrate sorting your data by creating a switch statement for each field in your model that should be sorted. I find this tedious to maintain and not extremely flexible.

Once again, there is a great third-party library that provides support for dynamic LINQ statements. With a dynamic LINQ statement, a string can be used to define the field to be sorted on. For example, the string "FirstName ASC" would result to the same db.Authors.OrderBy(a => a.FirstName). These next few examples will demonstrate it.

To begin, the new library needs to be installed via NuGet. Open the NuGet Package Manager and search for “dynamic.” The first result should be System.Linq.Dynamic. Click Install to add it to your project.

Example 5-5 creates a new model called QueryOptions. This class will store the sorting and paging options. For now, this class will contain only the fields for sorting.

Example 5-5. QueryOptions model

namespace BootstrapIntroduction.Models

{

public class QueryOptions

{

public QueryOptions()

{

SortField = "Id";

SortOrder = SortOrder.ASC;

}

public string SortField { get; set; }

public SortOrder SortOrder { get; set; }

public string Sort

{

get

{

return string.Format("{0} {1}",

SortField, SortOrder.ToString());

}

}

}

}

The QueryOptions class contains two properties that can be read and written to: SortField and SortOrder. The SortField defines which field from the model should be sorted on. The SortOrder field indicates the direction. There is also a third property that is read-only. This property concatenates the two properties into a single string. This will be used in the dynamic LINQ. This model will be used in the controller to order the authors prior to passing to the view.

The QueryOptions model references an enum called SortOrder. This enum is shown in Example 5-6 and should be added to the Models folder. The SortOrder could be accomplished without the use of an enum; however, then it would involve comparing strings. An enum allows us to use strongly typed comparisons.

Example 5-6. SortOrder enum

namespace BootstrapIntroduction.Models

{

public enum SortOrder

{

ASC,

DESC

}

}

Now the Index in the AuthorsController (shown in Example 5-7) can be updated to accept the QueryOptions class as input. This class is then used to order the authors by leveraging the dynamic LINQ library that was previously added.

Example 5-7. AuthorsController

using System;

using System.Collections.Generic;

using System.Data;

using System.Data.Entity;

using System.Linq;

using System.Linq.Dynamic;

using System.Net;

using System.Web;

using System.Web.Mvc;

using BootstrapIntroduction.DAL;

using BootstrapIntroduction.Models;

using System.Web.ModelBinding;

namespace BootstrapIntroduction.Controllers

{

public class AuthorsController : Controller

{

private BookContext db = new BookContext();

// GET: Authors

public ActionResult Index([Form] QueryOptions queryOptions)

{

var authors = db.Authors.OrderBy(queryOptions.Sort);

ViewBag.QueryOptions = queryOptions;

return View(authors.ToList());

}

// other functions removed for an abbreviated example

}

}

As you’ll see in Example 5-8, the view will build a URL that contains the SortField and SortOrder. By adding the [Form] attribute in front of the QueryOptions parameter, MVC will automatically parse the URL parameters and create the QueryOptions class for us. If the URL contains no fields, it will simply create a new QueryOptions class without setting the SortField and SortOrder properties. In Example 5-5, these are instantiated in the constructor to be “Id” and ascending.

The QueryOptions are passed to the view by using the ViewBag property. The view will use this to help build the URL by inversing the sort direction for the current field being sorted.

Example 5-8 contains an updated Index view.

Example 5-8. Authors Index view

@using BootstrapIntroduction.Models

@model IEnumerable<Author>

@{

ViewBag.Title = "Authors";

var queryOptions = (QueryOptions)ViewBag.QueryOptions;

}

<h2>Authors</h2>

<p>@Html.ActionLink("Create New", "Create")</p>

<table class="table table-bordered table-striped">

<thead>

<tr>

<th>@Html.BuildSortableLink("First Name", "Index", "FirstName"

, queryOptions)</th>

<th>@Html.BuildSortableLink("Last Name", "Index", "LastName"

, queryOptions)</th>

<th>Actions</th>

</tr>

</thead>

<tbody data-bind="foreach: authors">

<tr>

<td data-bind="text: FirstName"></td>

<td data-bind="text: LastName"></td>

<td>

<a data-bind="attr: { href: '@Url.Action("Details")/' + Id }"

class="btn btn-info">Details</a>

<a data-bind="attr: { href: '@Url.Action("Edit")/' + Id }"

class="btn btn-primary">Edit</a>

<a data-bind="attr: { href: '@Url.Action("Delete")/' + Id }"

class="btn btn-danger">Delete</a>

</td>

</tr>

</tbody>

</table>

@section Scripts {

<script>

function ViewModel(authors) {

var self = this;

self.authors = authors;

};

var viewModel = new ViewModel(@Html.HtmlConvertToJson(Model));

ko.applyBindings(viewModel);

</script>

}

The QueryOptions that was passed from the controller in the ViewBag property is casted from the dynamic property and stored in a local variable that the view can access. The other major change converts the static text in the th tag to build a link back to the index page with the URL parameters defining the sort direction.

The link is built using another custom HtmlHelper extension. Example 5-9 contains an updated HtmlHelperExtension that creates a new function called BuildSortableLink that accepts four parameters:

fieldName

This is the name of the link (e.g., First Name).

actionName

This is the name of the action to link to (e.g., Index).

sortField

This is the name of the model field to sort on (e.g., FirstName).

queryOptions

This contains the QueryOptions currently used to sort the authors. This is used to determine if the current field is being sorted, in which case the direction should be inversed.

This function returns an MvcHtmlString with the link that, when selected, will reload the page and sort the authors by the specified field in the specified order.

Example 5-9. HtmlHelperExtension

using BootstrapIntroduction.Models;

using Newtonsoft.Json;

using System.Web;

using System.Web.Mvc;

using System.Web.Mvc.Html;

public static class HtmlHelperExtensions

{

public static HtmlString HtmlConvertToJson(this HtmlHelper htmlHelper

, object model)

{

var settings = new JsonSerializerSettings

{

ReferenceLoopHandling = ReferenceLoopHandling.Ignore,

Formatting = Formatting.Indented

};

return new HtmlString(JsonConvert.SerializeObject(model, settings));

}

public static MvcHtmlString BuildSortableLink(this HtmlHelper htmlHelper,

string fieldName, string actionName, string sortField, QueryOptions queryOptions)

{

var urlHelper = new UrlHelper(htmlHelper.ViewContext.RequestContext);

var isCurrentSortField = queryOptions.SortField == sortField;

return new MvcHtmlString(string.Format("<a href=\"{0}\">{1} {2}</a>",

urlHelper.Action(actionName,

new {

SortField = sortField,

SortOrder = (isCurrentSortField

&& queryOptions.SortOrder == SortOrder.ASC)

? SortOrder.DESC : SortOrder.ASC

}),

fieldName,

BuildSortIcon(isCurrentSortField, queryOptions)));

}

private static string BuildSortIcon(bool isCurrentSortField

, QueryOptions queryOptions)

{

string sortIcon = "sort";

if (isCurrentSortField)

{

sortIcon += "-by-alphabet";

if (queryOptions.SortOrder == SortOrder.DESC)

sortIcon += "-alt";

}

return string.Format("<span class=\"{0} {1}{2}\"></span>",

"glyphicon", "glyphicon-", sortIcon);

}

}

As a nice little touch, Example 5-9 contains a private function called BuildSortIcon that leverages three of the many glyphicons provided by Bootstrap. When the field is not currently being sorted, it uses the glyphicon-sort. When the field is being sorted ascending, it uses the glyphicon-sort-by-alphabet. And when the field is being sorted descending, it uses glyphicon-sort-by-alphabet-alt.

Why Inside the HtmlHelperExtension?

It is not necessary to create an HtmlHelper extension method to implement sorting in your view. This approach provides two nice benefits:

§ The multiline logic to create the link with sorting icon does not convolute the view with a complicated mixture of HTML and Razor.

§ Creating multiple sortable links is easily reusable both within the same view and future views in your project.

Figure 5-3 demonstrates the sorting in action.

Figure 5-3. Authors sorted by last name descending

When you run this example, it will be difficult to see the sorting in action. However, this can be solved by clicking the link to create a new author (as I’ve shown in Figure 5-3) and use the previously scaffolded view and controller action to create a new author. Once you’ve created additional authors, you can click the first and last name links to sort them. Clicking the same link twice will alter the direction from ascending to descending, and vice versa.

Paging the Authors

Paging through the authors is quite similar to sorting. LINQ will be used on the DbSet to Skip and Take a specific number of records. The previously created QueryOptions model will be updated (shown in Example 5-10) to include three new properties: CurrentPage, TotalPages, and PageSize.CurrentPage and PageSize have been defaulted to 1 in the constructor. One is a very small number for the page size; however, when dealing with a small number of authors, it makes it easier to test the functionality. Ten is a more common number for paging.

Example 5-10. Updated QueryOptions model

namespace BootstrapIntroduction.Models

{

public class QueryOptions

{

public QueryOptions()

{

CurrentPage = 1;

PageSize = 1;

SortField = "Id";

SortOrder = SortOrder.ASC;

}

public int CurrentPage { get; set; }

public int TotalPages { get; set; }

public int PageSize { get; set; }

public string SortField { get; set; }

public SortOrder SortOrder { get; set; }

public string Sort

{

get

{

return string.Format("{0} {1}",

SortField, SortOrder.ToString());

}

}

}

}

In Example 5-11, the AuthorsController Index function has been updated to implement the LINQ to skip and take the specific number of records. Inside this updated function, the TotalPages from the QueryOptions model (the one that was not defaulted in the constructor) is set by calculating the number of pages from the count of authors divided by the page size, rounded up. This will be used in the view to disable the Next button.

Example 5-11. Updated AuthorsController

using System;

using System.Collections.Generic;

using System.Data;

using System.Data.Entity;

using System.Linq;

using System.Linq.Dynamic;

using System.Net;

using System.Web;

using System.Web.Mvc;

using BootstrapIntroduction.DAL;

using BootstrapIntroduction.Models;

using System.Web.ModelBinding;

namespace BootstrapIntroduction.Controllers

{

public class AuthorsController : Controller

{

private BookContext db = new BookContext();

// GET: Authors

public ActionResult Index([Form] QueryOptions queryOptions)

{

var start = (queryOptions.CurrentPage - 1) * queryOptions.PageSize;

var authors = db.Authors.

OrderBy(queryOptions.Sort).

Skip(start).

Take(queryOptions.PageSize);

queryOptions.TotalPages =

(int)Math.Ceiling((double)db.Authors.Count() / queryOptions.PageSize);

ViewBag.QueryOptions = queryOptions;

return View(authors.ToList());

}

// other functions removed for an abbreviated example

}

}

Just like when sorting the authors, the logic to generate the HTML for paging the authors has been done inside another custom HtmlHelper method. Example 5-12 creates one public function called BuildNextPreviousLinks that accepts the QueryOptions and the actionName as input parameters. Four private functions are also created to build the necessary HTML to generate the next and previous links shown in Figure 5-4.

Example 5-12. Updated HtmlHelperExtension

using BootstrapIntroduction.Models;

using Newtonsoft.Json;

using System.Web;

using System.Web.Mvc;

using System.Web.Mvc.Html;

public static class HtmlHelperExtensions

{

public static MvcHtmlString BuildNextPreviousLinks(

this HtmlHelper htmlHelper, QueryOptions queryOptions, string actionName)

{

var urlHelper = new UrlHelper(htmlHelper.ViewContext.RequestContext);

return new MvcHtmlString(string.Format(

"<nav>" +

" <ul class=\"pager\">" +

" <li class=\"previous {0}\">{1}</li>" +

" <li class=\"next {2}\">{3}</li>" +

" </ul>" +

"</nav>",

IsPreviousDisabled(queryOptions),

BuildPreviousLink(urlHelper, queryOptions, actionName),

IsNextDisabled(queryOptions),

BuildNextLink(urlHelper, queryOptions, actionName)

));

}

private static string IsPreviousDisabled(QueryOptions queryOptions)

{

return (queryOptions.CurrentPage == 1)

? "disabled" : string.Empty;

}

private static string IsNextDisabled(QueryOptions queryOptions)

{

return (queryOptions.CurrentPage == queryOptions.TotalPages)

? "disabled" : string.Empty;

}

private static string BuildPreviousLink(

UrlHelper urlHelper, QueryOptions queryOptions, string actionName)

{

return string.Format(

"<a href=\"{0}\"><span aria-hidden=\"true\">←</span> Previous</a>",

urlHelper.Action(actionName, new

{

SortOrder = queryOptions.SortOrder,

SortField = queryOptions.SortField,

CurrentPage = queryOptions.CurrentPage - 1,

PageSize = queryOptions.PageSize

}));

}

private static string BuildNextLink(

UrlHelper urlHelper, QueryOptions queryOptions, string actionName)

{

return string.Format(

"<a href=\"{0}\">Next <span aria-hidden=\"true\">→</span></a>",

urlHelper.Action(actionName, new

{

SortOrder = queryOptions.SortOrder,

SortField = queryOptions.SortField,

CurrentPage = queryOptions.CurrentPage + 1,

PageSize = queryOptions.PageSize

}));

}

// other functions removed for an abbreviated example

}

The generated next and previous links are leveraging more Bootstrap components for pagination. When you are on the first page, the Previous link is disabled. Similarly, when you are on the last page, the Next link is disabled.

Because the QueryOptions are passed into the custom HtmlHelper function, the next and previous links include the current sorting options into the URL. The sorting links are not updated because when the sorting direction changes, the current page should be reset back to 1.

Figure 5-4. Next/Previous links

To complete this example, the Index view of the AuthorsController needs to be updated to execute the BuildNextPreviousLinks from the HtmlHelper. Example 5-13 contains the one-liner that can be added to the Index view below the end table tag.

Example 5-13. Create the next/previous links

@Html.BuildNextPreviousLinks(queryOptions, "Index")

Summary

This chapter focused on a single part of the CRUD operation, but it introduced several new things from MVC, Bootstrap, and Knockout.

In MVC, LINQ was used to sort a collection and limit the number of results returned. The HtmlHelper was extended multiple times to create reusable links to sort the data or navigate between pages.

For Bootstrap, several of the table classes were used to provide alternating row colors, borders, etc. The pagination component was also used to create nicely aligned next and previous links, including disabling the link when on the first and last page.

And finally, Knockout introduced a new foreach data binding that repeated a block of HTML code for each element in the array.