Working with Forms - 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 6. Working with Forms

If you experimented in the preceding chapter, you will have noticed that the scaffolded AuthorsController is fully functional in terms of adding, editing, and deleting records from the Author table. This in itself makes it quite useful; however, this chapter will demonstrate how to integrate Knockout and Bootstrap into the form as well as a little jQuery to submit the form via AJAX.

Upgrading Bootstrap

When I introduced the NuGet Package Manager, I mentioned updating the existing packages to their latest versions. The version that is installed with MVC is version 3.0 and some of the features used in this chapter (and future chapters) use the documentation from version 3.3.

If you didn’t update the packages back in Chapter 3, I would encourage you to do it now by following these steps:

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

2. On the left, select the Update option. This will search online for any updates to all of the packages currently installed.

3. If you are comfortable with updating all packages, you can click the Update All button, or you can find just the Bootstrap package and update it individually.

Integrating Knockout with a Form

This chapter will start to demonstrate why I love working with these three technologies together. It’s going to take a nice-looking form and add some usability to it. In fact, no changes are required to the AuthorsController.

Back in Chapter 4 when the Author and Book models were created, I didn’t specify any data validation for them. Because jQuery provides good client-side validation that integrates nicely with MVC, I thought I would go back and add some validation on the Author model.

Example 6-1 demonstrates making both the first and last name required fields before saving to the database.

Example 6-1. Updated AuthorModel

using System;

using System.Collections.Generic;

using System.ComponentModel.DataAnnotations;

using System.Linq;

using System.Web;

namespace BootstrapIntroduction.Models

{

public class Author

{

public int Id { get; set; }

[Required]

public string FirstName { get; set; }

[Required]

public string LastName { get; set; }

public string Biography { get; set; }

public virtual ICollection<Book> Books { get; set; }

}

}

Above the definition of both the first and last name properties, an attribute has been added called Required. This does several things. In the AuthorsController, the create method performs a ModelState.IsValid check, which validates that all properties of the model are valid based upon all of the validation rules identified. And secondly, as mentioned, jQuery validation will perform client-side validations by taking the rules in the model and implementing them via JavaScript.

More Validation

MVC provides a variety of validation options apart from the aforementioned Required attribute, such as minimum string length, regular expressions, minimum and maximum integer values, etc. Throughout this book, we will explore several as they are required. A list of attribute classes can be found at MSDN under the DataAnnotations namespace.

With the validation implemented on the Author model, it’s time to move on to the UI and Knockout data bindings. Example 6-2 contains the Knockout ViewModel called AuthorFormViewModel. To allow for easy management of the various ViewModels, I suggest creating a new folder calledViewModels inside the Scripts folder. Code organization is a very important step in building maintainable code. Right-click the Scripts folder and select the Add submenu item followed by New Folder. Once the new folder is created, create a new JavaScript file calledAuthorFormViewModel.js.

Example 6-2. AuthorFormViewModel

function AuthorFormViewModel() {

var self = this;

self.saveCompleted = ko.observable(false);

self.sending = ko.observable(false);

self.author = {

firstName: ko.observable(),

lastName: ko.observable(),

biography: ko.observable(),

};

self.validateAndSave = function (form) {

if (!$(form).valid())

return false;

self.sending(true);

// include the anti forgery token

self.author.__RequestVerificationToken = form[0].value;

$.ajax({

url: 'Create',

type: 'post',

contentType: 'application/x-www-form-urlencoded',

data: ko.toJS(self.author)

})

.success(self.successfulSave)

.error(self.errorSave)

.complete(function () { self.sending(false) });

};

self.successfulSave = function () {

self.saveCompleted(true);

$('.body-content').prepend(

'<div class="alert alert-success">

<strong>Success!</strong> The new author has been saved.</div>');

setTimeout(function () { location.href = './'; }, 1000);

};

self.errorSave = function () {

$('.body-content').prepend(

'<div class="alert alert-danger">

<strong>Error!</strong> There was an error creating the author.</div>');

};

}

This file introduces a new concept with Knockout called observable variables. When you define a JavaScript variable as an observable, Knockout will begin tracking changes to the variable. This means that if you data bind the observable in your form, Knockout will update the JavaScript variable in your ViewModel; and vice versa, if you update the property in your ViewModel and it is data bound to an HTML element it will be automatically updated when it changes.

Example 6-2 contains five properties marked as observables. The saveCompleted observable will be used to alter the page once the new author has been saved successfully. The sending observable will be used to show a progress bar when the author is being saved via AJAX and hide the submit button while it’s saving. The final three observables are contained within the author structure that are bound to the author form elements. The author property with its observables will be submitted via AJAX to save the author.

After the observable variables are three functions: validateAndSave, successfulSave, and errorSave. The first function introduces the submit data binding and is called by Knockout when the authors form is submitted.

The validateAndSave function is doing three important things:

§ Not submitting the form if it doesn’t pass the jQuery validation.

§ Dynamically adding an antiforgery token from the form to the AJAX request.

§ Sending the author object via an AJAX form post.

The final two functions are called from the validateAndSave function upon success or failure. If saving the author is successful, a new success alert message is added with jQuery at the top of the form, and after one second, the user is redirected back to the list of authors page. Similarly if an error occurs, an error alert is displayed indicating that the author was not saved.

setTimout

Example 6-2 contains a setTimout function, which is contained within the successfulSave function. It is used to display a success alert message to the user, and then after one second, redirect back to the list of authors.

The final piece of the puzzle is the Create.cshtml view. The complete source is displayed in Example 6-7. The next few examples will display the important pieces of implementing Knockout within the view and the creation of a progress bar with Bootstrap. Figure 6-1 demonstrates what the final form looks like, including error messages, because no first and last names were entered.

Figure 6-1. Creating author with error handling

The form that was created when the view was first scaffolded has been updated to set a data binding for the submit event. Example 6-3 indicates that when the form is submitted, Knockout should call the validateAndSave function.

Example 6-3. Submit data binding

@using (Html.BeginForm("Create", "Authors", FormMethod.Post,

new { data_bind = "submit: validateAndSave" }))

All three of the form fields (first name, last name, and biography) have been updated to include a data-binding attribute of value (as shown in Example 6-4). Each is bound to the appropriate observable property inside the author variable. When the user types into the form field, Knockout will update the corresponding observable property.

Example 6-4. Value data binding

@Html.EditorFor(model => model.FirstName, new { htmlAttributes =

new { @class = "form-control", data_bind = "value: author.firstName" } })

In Example 6-5, the submit button is updated to include the visible data binding. When the sending observable variable is false, the button is visible. When it is true, the button will be hidden so that the user cannot click it multiple times while the author is being saved via AJAX.

Beneath the submit button is where the progress bar is included with the appropriate Bootstrap classes. The div tag used to define the progress bar is also decorated with the visible data binding. It is using the opposite of the button, meaning it is only visible when the author is being saved via AJAX and hidden when it is not.

Example 6-5. Submit button and progress bar

<div class="form-group">

<div class="col-md-offset-2 col-md-10" data-bind="visible: !sending()">

<input type="submit" value="Create" class="btn btn-default" />

</div>

<div class="progress" data-bind="visible: sending">

<div class="progress-bar progress-bar-info progress-bar-striped active"

role="progressbar" aria-valuenow="100"

aria-valuemin="0" aria-valuemax="100"

style="width: 100%">

<span class="sr-only"></span>

</div>

</div>

</div>

Accessing Observables

When you create an observable property, Knockout converts the variable into a function to track its changes. This means when you want to access the value to the property, you need to add brackets after its name to execute the function.

In Example 6-5, you might have noticed that the submit button has brackets after sending and the progress bar does not. Knockout is intelligent enough that when it is comparing to true, brackets are not required because Knockout will detect if it is an observable and automatically add the brackets. On the other hand, when you are saying not (!) sending, you need to access the observable variable’s value prior to checking if it is false.

The final change made to the Create view is to include the Scripts section. Example 6-6 includes two JavaScript files: the jQuery Validation bundle (for the unobtrusive form validation) and the AuthorFormViewModel that was shown in Example 6-2. After these files are included, theAuthorFormViewModel is instantiated and ko.applyBindings is called with it.

Example 6-6. Scripts section

@section Scripts {

@Scripts.Render("~/bundles/jqueryval",

"/Scripts/ViewModels/AuthorFormViewModel.js")

<script>

var viewModel = new AuthorFormViewModel();

ko.applyBindings(viewModel);

</script>

}

Example 6-7 shows the full Views/Authors/Create.cshtml file.

Example 6-7. Authors Create view

@model BootstrapIntroduction.Models.Author

@{

ViewBag.Title = "Create";

}

<div data-bind="visible: !saveCompleted()">

<h2>Create</h2>

@using (Html.BeginForm("Create", "Authors", FormMethod.Post,

new { data_bind = "submit: validateAndSave" }))

{

@Html.AntiForgeryToken()

<div class="form-horizontal">

<h4>Author</h4>

<hr />

@Html.ValidationSummary(true, "", new { @class = "text-danger" })

<div class="form-group">

@Html.LabelFor(model => model.FirstName, htmlAttributes:

new { @class = "control-label col-md-2" })

<div class="col-md-10">

@Html.EditorFor(model => model.FirstName, new { htmlAttributes =

new { @class = "form-control",

data_bind = "value: author.firstName" } })

@Html.ValidationMessageFor(model => model.FirstName, "",

new { @class = "text-danger" })

</div>

</div>

<div class="form-group">

@Html.LabelFor(model => model.LastName, htmlAttributes:

new { @class = "control-label col-md-2" })

<div class="col-md-10">

@Html.EditorFor(model => model.LastName, new { htmlAttributes =

new { @class = "form-control", data_bind = "value:

author.lastName" } })

@Html.ValidationMessageFor(model => model.LastName, "",

new { @class = "text-danger" })

</div>

</div>

<div class="form-group">

@Html.LabelFor(model => model.Biography, htmlAttributes:

new { @class = "control-label col-md-2" })

<div class="col-md-10">

@Html.EditorFor(model => model.Biography, new { htmlAttributes =

new { @class = "form-control", data_bind = "value:

author.biography" } })

@Html.ValidationMessageFor(model => model.Biography, "",

new { @class = "text-danger" })

</div>

</div>

<div class="form-group">

<div class="col-md-offset-2 col-md-10" data-bind="visible: !sending()">

<input type="submit" value="Create" class="btn btn-default" />

</div>

<div class="progress" data-bind="visible: sending">

<div class="progress-bar progress-bar-info progress-bar-striped active"

role="progressbar" aria-valuenow="100"

aria-valuemin="0" aria-valuemax="100"

style="width: 100%">

<span class="sr-only"></span>

</div>

</div>

</div>

</div>

}

</div>

<div>

@Html.ActionLink("Back to List", "Index")

</div>

@section Scripts {

@Scripts.Render("~/bundles/jqueryval",

"/Scripts/ViewModels/AuthorFormViewModel.js")

<script>

var viewModel = new AuthorFormViewModel();

ko.applyBindings(viewModel);

</script>

}

Sharing the View and ViewModel

It’s decision time now. The create author view has been updated and integrated with Knockout and some Bootstrap; however, the edit author view is still using the old way. To be consistent (and consistency in a website is important), the edit view should be updated to match the create.

It would be pretty easy to copy all of the changes from the create form to the edit form, making the subtle adjustments where applicable. I personally try to avoid this whenever possible. I instead like to share the View and ViewModel between the two. Sharing the code makes updates easier to maintain. For example, if a new field were added to the Author model, there is less work because once it is added, both the create and edit forms will have it.

Use Caution

While I strongly recommend sharing the View and ViewModel, there are many times when this is not easy or even possible. If the structure of the two are very different or contain different rules, it makes more sense to maintain separate Views and ViewModels. This would be less complicated than a single View and ViewModel with many conditional statements identifying the differences.

Sharing the View and ViewModel involves updating several different things:

1. The AuthorsController needs updating to load the same view (shown in Example 6-8) for the Create and Edit actions.

2. The Create function needs to instantiate a new Author model and provide this to the view (also shown in Example 6-8). This will be explained in more detail with the following code examples.

3. The Create view will be renamed to Form to help better identify that it is not specifically for create or edit.

4. The newly renamed Form view will contain several conditional statements to change the wording when an author is being added or edited. It will also serialize the Author model bound to the view. This will then be passed into the AuthorFormViewModel to prepopulate the author when it is being edited. This is shown in Example 6-9.

5. The AuthorFormViewModel author variable contains a new id property to distinguish whether the author is being added or edited. This will also be used to update the jQuery AJAX request to either go to the Create or Edit action (shown in Example 6-10).

6. The Author model is updated (shown in Example 6-11) to leverage a new data annotation called JsonProperty that will allow the properties to be camelCased when used in JavaScript, but remain PascalCase in C#.

7. The previous model changes also have an effect on the Index view because the previously PascalCase variable references now need to be camelCased as shown in Example 6-12.

There are a total of seven things to do, so let’s get started. Example 6-8 contains an abbreviated AuthorsController with the required updates to the Create and Edit functions.

Example 6-8. 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();

// Abbreviated controller

// GET: Authors/Create

public ActionResult Create()

{

return View("Form", new 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("Form", author);

}

// Abbreviated controller

}

This example completes the first two items on the list. You will notice that the return View at the end of each method has been updated to pass two parameters. The first parameter is the name of the view to load, in this case, Form. The second parameter is the model that is bound to the view. Previously, the Create method did not pass this in, even though it was bound to it. However, it is now instantiated as an empty model, because the model will be serialized and passed as a JavaScript object to the AuthorFormViewModel. If the Create function did not instantiate it, the model would be null, and the JavaScript ViewModel would be unable to parse out the properties.

The Edit view that was automatically scaffolded can be safely deleted. The Create view should now be renamed to Form. This can be done by selecting the view in Visual Studio and pressing F2.

Example 6-9 contains the full Form view. The added/altered lines are highlighted to identify them easily.

Example 6-9. Form view

@model BootstrapIntroduction.Models.Author

@{

var isCreating = Model.Id == 0;

ViewBag.Title = (isCreating) ? "Create" : "Edit";

}

<div data-bind="visible: !saveCompleted()">

<h2>@ViewBag.Title</h2>

@using (Html.BeginForm("Create", "Authors", FormMethod.Post,

new { data_bind = "submit: validateAndSave" }))

{

@Html.AntiForgeryToken()

<div class="form-horizontal">

<h4>Author</h4>

<hr />

@Html.ValidationSummary(true, "", new { @class = "text-danger" })

<div class="form-group">

@Html.LabelFor(model => model.FirstName, htmlAttributes:

new { @class = "control-label col-md-2" })

<div class="col-md-10">

@Html.EditorFor(model => model.FirstName, new { htmlAttributes =

new { @class = "form-control",

data_bind = "value: author.firstName" } })

@Html.ValidationMessageFor(model => model.FirstName, "",

new { @class = "text-danger" })

</div>

</div>

<div class="form-group">

@Html.LabelFor(model => model.LastName, htmlAttributes:

new { @class = "control-label col-md-2" })

<div class="col-md-10">

@Html.EditorFor(model => model.LastName, new { htmlAttributes =

new { @class = "form-control",

data_bind = "value: author.lastName" } })

@Html.ValidationMessageFor(model => model.LastName, "",

new { @class = "text-danger" })

</div>

</div>

<div class="form-group">

@Html.LabelFor(model => model.Biography, htmlAttributes:

new { @class = "control-label col-md-2" })

<div class="col-md-10">

@Html.EditorFor(model => model.Biography, new { htmlAttributes =

new { @class = "form-control",

data_bind = "value: author.biography" } })

@Html.ValidationMessageFor(model => model.Biography, "",

new { @class = "text-danger" })

</div>

</div>

<div class="form-group">

<div class="col-md-offset-2 col-md-10" data-bind="visible: !sending()">

<input type="submit" value="@if (isCreating) {

@Html.Raw("Create")

} else { @Html.Raw("Update") }"

class="btn btn-default" />

</div>

<div class="progress" data-bind="visible: sending">

<div class="progress-bar progress-bar-info progress-bar-striped active"

role="progressbar" aria-valuenow="100"

aria-valuemin="0" aria-valuemax="100"

style="width: 100%">

<span class="sr-only"></span>

</div>

</div>

</div>

</div>

}

</div>

<div>

@Html.ActionLink("Back to List", "Index")

</div>

@section Scripts {

@Scripts.Render("~/bundles/jqueryval",

"/Scripts/ViewModels/AuthorFormViewModel.js")

<script>

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

ko.applyBindings(viewModel);

</script>

}

Example 6-10 is an updated AuthorFormViewModel. It does similar things to the view by determining whether the author is being added or edited to perform minor conditional differences.

Example 6-10. AuthorFormViewModel

function AuthorFormViewModel(author) {

var self = this;

self.saveCompleted = ko.observable(false);

self.sending = ko.observable(false);

self.isCreating = author.id == 0;

self.author = {

id: author.id,

firstName: ko.observable(author.firstName),

lastName: ko.observable(author.lastName),

biography: ko.observable(author.biography),

};

self.validateAndSave = function (form) {

if (!$(form).valid())

return false;

self.sending(true);

// include the anti forgery token

self.author.__RequestVerificationToken = form[0].value;

$.ajax({

url: (self.isCreating) ? 'Create' : 'Edit',

type: 'post',

contentType: 'application/x-www-form-urlencoded',

data: ko.toJS(self.author)

})

.success(self.successfulSave)

.error(self.errorSave)

.complete(function () { self.sending(false) });

};

self.successfulSave = function () {

self.saveCompleted(true);

$('.body-content').prepend(

'<div class="alert alert-success">

<strong>Success!</strong> The author has been saved.</div>');

setTimeout(function () {

if (self.isCreating)

location.href = './';

else

location.href = '../';

}, 1000);

};

self.errorSave = function () {

$('.body-content').prepend(

'<div class="alert alert-danger">

<strong>Error!</strong> There was an error saving the author.</div>');

};

}

The id is now added to the author object. It is not an observable property because it won’t change during the lifetime of the request. This is used by the Edit method to indicate which author is being edited. Similar to the view, a variable called isCreating is defined to place the logic identifying whether the author is being added or edited. This variable is used within the validateAndSave function to change the URL of the AJAX request. When isCreating is true, it will continue to go to the Create method. When it is false, it will change and go to the Edit action. This variable is also used in the successfulSave function to properly redirect back to the authors listing page.

Both the successful and error messages have been updated to remove the word creating and replace it with saving. This could also be updated to leverage the isCreating variable; however, I like the ambiguous term of “saving.”

Creating Observables

In Example 6-9, the id property in the author variable was not created as an observable. Because Knockout needs to track changes to all observables, it’s important to be conscientious of how many observables get created. My general rule of thumb is, if the user cannot change it and if the UI doesn’t require updating if it is changed via code, then it doesn’t need to be observed. If either of these are true, then it probably should be an observed property.

It’s quite common in JavaScript for variables and property names to be camelCased. I like to follow this rule when I can. As part of the Json.Net library, a data annotation is available that lets us do exactly that. The C# properties can remain PascalCase, and the JavaScript can be camelCased.Example 6-11 contains an updated Author model reflecting this.

Example 6-11. Updated Author model

using Newtonsoft.Json;

using System;

using System.Collections.Generic;

using System.ComponentModel.DataAnnotations;

using System.Linq;

using System.Web;

namespace BootstrapIntroduction.Models

{

public class Author

{

[JsonProperty(PropertyName="id")]

public int Id { get; set; }

[Required]

[JsonProperty(PropertyName = "firstName")]

public string FirstName { get; set; }

[Required]

[JsonProperty(PropertyName = "lastName")]

public string LastName { get; set; }

[JsonProperty(PropertyName = "biography")]

public string Biography { get; set; }

[JsonProperty(PropertyName = "books")]

public virtual ICollection<Book> Books { get; set; }

}

}

The change to the Author model now breaks the previous data bindings in the authors Index view and should be updated as shown in Example 6-12.

Example 6-12. Updated 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>

@Html.BuildNextPreviousLinks(queryOptions, "Index")

@section Scripts {

<script>

function ViewModel(authors) {

var self = this;

self.authors = authors;

};

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

ko.applyBindings(viewModel);

</script>

}

The required changes are now completed, and the Create and Edit actions are now sharing the same View and ViewModel.

Deleting with a Modal

The scaffolded delete functionality is quite nice. I like that it contains a confirmation page allowing the users to back out and change their mind. However, I do not like the fact that users are redirected to a new page for this simple option. This section will demonstrate how to implement the same functionality within a modal window (shown in Figure 6-2).

Converting the existing functionality into a modal involves a few different steps:

1. The delete button in the Views/Authors/Index.cshtml view needs to change from a regular link to a Knockout click data binding.

2. The resulting click event from the delete button will be implemented in the authors ViewModel to fetch the existing delete confirmation page and display it with a Bootstrap modal.

3. To avoid adding additional markup to the Index view, the scaffolded Views/Authors/Delete.cshtml view has been updated to contain the required markup for a Bootstrap modal.

4. The previous inline ViewModel has been moved into a new AuthorIndexViewModel inside the newly created ViewModels folder for better code organization.

Two changes are required in the authors Index view. First, the delete link needs updating to include the new click data binding. This data binding accepts a function that will be executed by Knockout when the user clicks this button. This is shown in Example 6-13.

Figure 6-2. Delete author modal

Example 6-13. Updated delete button

<a data-bind="

click: $parent.showDeleteModal, attr: { href: '@Url.Action("Delete")/' + id }"

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

Because this code is inside a Knockout foreach binding, the function to be called is prefixed with $parent. When you are inside a foreach binding, you are no longer in the context of the ViewModel. In this example, you are now in the context of an individual author object and only its properties are available. Knockout provides the ability to access other properties outside the current context with $parent.

The second change (shown in Example 6-14) updates the Scripts section at the bottom of the view. Previously, the ViewModel was contained in the view. It has now been moved to a new file called AuthorIndexViewModel. This file is included and then instantiated with the list of authors as before.

Example 6-14. Updated Scripts section

@section Scripts {

@Scripts.Render("/Scripts/ViewModels/AuthorIndexViewModel.js")

<script>

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

ko.applyBindings(viewModel);

</script>

}

Example 6-15 contains the enhanced AuthorIndexViewModel. It contains two new functions: showDeleteModal and deleteAuthor.

Example 6-15. New AuthorIndexViewModel

function AuthorIndexViewModel(authors) {

var self = this;

self.authors = authors;

self.showDeleteModal = function (data, event) {

self.sending = ko.observable(false);

$.get($(event.target).attr('href'), function (d) {

$('.body-content').prepend(d);

$('#deleteModal').modal('show');

ko.applyBindings(self, document.getElementById('deleteModal'));

});

};

self.deleteAuthor = function (form) {

self.sending(true);

return true;

};

};

The showDeleteModal function is called when the user clicks the delete button. It contains two parameters: data and event. The data parameter contains the current author with all of its properties. The second parameter, event, contains the HTML element that the click binding is attached to. This parameter is used in the AJAX call to specify the URL of the request.

When the AJAX request completes, the resulting HTML is prepended to the body-content class. Once the HTML is prepended, the modal is shown to the user by accessing the newly added HTML element with the id of deleteModal and calling the modal with the value of show.

The updated Delete view (shown in Example 6-16) contains a couple of Knockout bindings. For these to be processed by Knockout, the ko.applyBindings needs to be executed with a ViewModel — in this case, the current ViewModel. An optional second parameter is provided that limits the scope of the binding to the newly inserted delete modal.

The deleteAuthor function is called when the user confirms the deletion of the author. This function sets the sending observable that was created in the showDeleteModal to true. In the delete modal, this will hide the submit button options. The function returns true, so the form will be submitted as usual. Normally, Knockout automatically returns false to prevent the submission of the form.

Example 6-16 contains an updated Delete view. The initial view contained a preview of the author being deleted and the creation of a new form with a submit button to delete. This has been maintained, but the markup is now wrapped within a modal.

Example 6-16. Updated Delete view

@model BootstrapIntroduction.Models.Author

@{

ViewBag.Title = "Delete";

Layout = null;

}

<div class="modal fade" id="deleteModal">

<div class="modal-dialog">

<div class="modal-content">

<div class="modal-header">

<button type="button" class="close" data-dismiss="modal">

<span aria-hidden="true">×</span>

<span class="sr-only">Close</span>

</button>

<h4 class="modal-title">Author Delete Confirmation</h4>

</div>

<div class="modal-body">

<h3>Are you sure you want to delete this author?</h3>

<div>

<hr />

<dl class="dl-horizontal">

<dt>

@Html.DisplayNameFor(model => model.FirstName)

</dt>

<dd>

@Html.DisplayFor(model => model.FirstName)

</dd>

<dt>

@Html.DisplayNameFor(model => model.LastName)

</dt>

<dd>

@Html.DisplayFor(model => model.LastName)

</dd>

<dt>

@Html.DisplayNameFor(model => model.Biography)

</dt>

<dd>

@Html.DisplayFor(model => model.Biography)

</dd>

</dl>

</div>

<div class="modal-footer">

@using (Html.BeginForm("Delete", "Authors", FormMethod.Post,

new { data_bind = "submit: deleteAuthor" }))

{

@Html.AntiForgeryToken()

<div class="form-actions no-color text-center"

data-bind="visible: !sending()">

<input type="submit" value="Delete" class="btn btn-danger" />

<button type="button" class="btn btn-default"

data-dismiss="modal">Close</button>

</div>

<div class="progress" data-bind="visible: sending">

<div class="progress-bar progress-bar-info progress-bar-striped active"

role="progressbar" aria-valuenow="100"

aria-valuemin="0" aria-valuemax="100"

style="width: 100%">

<span class="sr-only"></span>

</div>

</div>

}

</div>

</div><!-- /.modal-content -->

</div><!-- /.modal-dialog -->

</div><!-- /.modal -->

</div>

Creating a modal consists of including a wrapper div with the class of modal. The modal is then divided into three separate sections: the header, the body, and the footer. In the delete modal, the header contains a title indicating that the user needs to confirm the deletion of this author. The body contains the preview of the author’s information. And the footer contains the form that will submit the author for deletion.

This form has been updated to include the submit data binding, which calls the aforementioned deleteAuthor function. The progress bar that was included when adding or editing an author is also included here and shown once the user has clicked the delete button.

Once the user clicks the delete button, it performs a regular form post. In the AuthorsController, the results of a successful author deletion redirect the user back to the authors listing page. This will hide the modal and cause the list of authors to be updated with the deleted author removed.

Empty Table Listings

Once I started to delete and add authors, I noticed that an empty table is shown when there are zero authors. Also, because there are Knockout data bindings contained within the first table, there is a flicker of an empty table row and buttons. This, of course, looks a little awkward.

This next example will solve it by applying a visible binding to the table. An alert message will also be shown when there are no authors. Example 6-17 contains an updated Views/Authors/Index.cshtml views with the subtle changes.

Example 6-17. Updated Authors 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>

<div style="display: none" data-bind="visible: authors.length > 0">

<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="click: $parent.showDeleteModal,

attr: { href: '@Url.Action("Delete")/' + id }"

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

</td>

</tr>

</tbody>

</table>

@Html.BuildNextPreviousLinks(queryOptions, "Index")

</div>

<div style="display: none" data-bind="visible: authors.length == 0"

class="alert alert-warning alert-dismissible" role="alert">

<button type="button" class="close" data-dismiss="alert">

<span aria-hidden="true">×</span>

<span class="sr-only">Close</span>

</button>

There are no authors to display.

Click @Html.ActionLink("here", "Create") to create one now.

</div>

@section Scripts {

@Scripts.Render("/Scripts/ViewModels/AuthorIndexViewModel.js")

<script>

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

ko.applyBindings(viewModel);

</script>

}

The table and pagination links are now wrapped in a div tag. This div contains a data binding that will make it hidden when the length of the authors array is 0. There is also an inline style of display: none on this element. This means by default it will be hidden until the Knockout bindings are executed.

An alert message has also been added (as shown in Figure 6-3). The div tag for the alert contains the inverse data binding, meaning that it will only be shown when there are no authors; otherwise, it will remain hidden. This div also contains an inline style with display: none.

Figure 6-3. Empty authors listing

Without the inline style to make both elements hidden by default, prior to Knockout being executed and hiding one of the two properties, both would be temporarily visible to the user. The side effect of this is that the page will be temporarily empty until Knockout executes the data bindings and shows the appropriate element. I prefer the empty page look to the on and off flicker of elements.

Summary

Chapter 5 provided the ability to add, edit, delete, and view the authors in the database. It introduced some nice functionality on the index page to sort and page through the authors. This chapter focused on the managing portion of it. The add and edit forms were updated to share the view and ViewModel and submit the author via AJAX. The delete was then updated to show the confirmation in a modal instead of going to a new page to perform the delete confirmation.

As a good learning exercise, I would suggest that you attempt the same changes on the Books table. Begin by scaffolding a BooksController and then perform the similar steps completed throughout this chapter and Chapter 5.