C# Corner

TDD for ASP.NET MVC, Part 5: Client-Side JavaScript

Let's wrap up this series on TDD for ASP.NET MVC and talk about the view layer via JavaScript.

Welcome to the fifth and final installment of this series on TDD for ASP.NET MVC. This time, I'll go over the view layer off the application, which includes the client-side JavaScript for the form and AJAX grid. I know this is a C# column, but the majority of the code I'll be showing is JavaScript, as is the nature of front-end Web development. Brace yourself … 

(Read the rest of the series: Part 1, 2, 3, 4.)

To get started, download the code from part 4 of this series. Next, open the Views\Home\Index.cshtml Razor view and paste in the markup from Listing 1.

Listing 1: Initial Index.cshtml Razor View
@{
  ViewBag.Title = "Contacts";
}

@model VSMMvcTDD.Models.ContactIndexModel
           

<div class="row">
  <div class="col-md-12" id="contactsGridContent">
    <fieldset>
      <legend>Contacts</legend>
      @{
        Html.RenderPartial("_ContactsGrid", Model.Contacts);
      }
    </fieldset>
    <div class="row pull-right">
      <button type="button" id="Create" class="btn btn-default ladda-button" 
      data-style="expand-right"><i class="glyphicon glyphicon-plus">
      </i>  Create</button>
    </div>
  </div>
</div>

<div id="contactStatus"></div>
<div id="contactContent">
</div>

Next, create a new Razor partial view named _ContactsGrid.cshtml in the Views\Home folder and paste in the contents from Listing 2.

Listing 2: ContactsGrid.cshtml Partial Razor View
@using GridMvc.Html
@using GridMvc.Sorting
@model Grid.Mvc.Ajax.GridExtensions.AjaxGrid<VSMMvcTDD.Models.ContactViewModel>

@Html.Grid(Model).Columns(columns =>
{
  columns.Add(c => c.Id, true);
  columns.Add(c => c.LastName).Titled(
    "Last Name").SortInitialDirection(GridSortDirection.Ascending);
  columns.Add(c => c.FirstName).Titled("First Name");
  columns.Add(c => c.Email);
}).Named("contactsGrid").Sortable(true).WithPaging(5).Filterable(true).Selectable(true).EmptyText("No contacts found")

You should now be able to see an empty Contacts grid when you run the application, as seen in Figure 1.

[Click on image for larger view.] Figure 1. Initial Contacts Page

Now it's time to bring the Contacts page to life with some client-side JavaScript. Create a new JavaScript file in the Scripts folder named contacts-1.0.0.js and paste in the content from Listing 3.

Listing 3: Initial Contacts-1.0.0.js
var contact = (function (my, $) {
  var constructorSpec = {
    contactsGridAction: '',
    newContactAction: '',
    editContactAction: '',
    deleteContactAction: ''
  };
   
  my.init = function (options) {
    $(function (){
    });
  };

  return my;
}(contact || {}, jQuery));

I'm using the JavaScript module pattern for the contact object, where only one object is ever created that's initialized through its init method, jQuery is passed into the constructor as a dependency. The constructorSpec object contains the necessary URIs for getting data for the contacts grid and inserting, updating, and deleting a contact record. Next, update the BundleConfig for the application to add the contacts-1.0.0.js file as a new bundle:

var contactBundle = new ScriptBundle("~/bundles/Contact.js")
  .Include("~/Scripts/contacts-{version}.js");
  bundles.Add(contactBundle);

Then update the Index.cshtml to include the Contact.js bundle and initialize the contact object:

@section scripts
{
  @Scripts.Render("~/bundles/Contact.js")
  <script type="text/javascript">
    contact.init({
      contactsGridAction: '@Url.Action("ContactsGrid")',
      newContactAction: '@Url.Action("Create")',
      editContactAction: '@Url.Action("Edit")',
      deleteContactAction: '@Url.Action("Delete")'
    });
  </script>
}

Your completed Index.cshtml Razor view should now look like Listing 4.

Listing 4: Completed Index.cshtml Contact Razor View
@{ ViewBag.Title = "Contacts"; }
@model VSMMvcTDD.Models.ContactIndexModel
           

<div class="row">
  <div class="col-md-12" id="contactsGridContent">
    <fieldset>
      <legend>Contacts</legend>
      @{
        Html.RenderPartial("_ContactsGrid", Model.Contacts);
      }
    </fieldset>
    <div class="row pull-right">
      <button type="button" id="Create" class="btn btn-default ladda-button" 
        data-style="expand-right">
        <i class="glyphicon glyphicon-plus">  Create</button>
    </div>
  </div>
</div>

<div id="contactStatus"></div>
<div id="contactContent">
</div>

@section scripts
{
  @Scripts.Render("~/bundles/Contact.js")
  <script type="text/javascript">
    contact.init({
      contactsGridAction: '@Url.Action("ContactsGrid")',
      newContactAction: '@Url.Action("Create")',
      editContactAction: '@Url.Action("Edit")',
      deleteContactAction: '@Url.Action("Delete")'
    });
  </script>
}

Now that the infrastructure is fully in place, continue with implementing the contact JavaScript module. In the inner function in the init method I first get a reference to this and set it to a local variable named self:

var self = this;

Then I set the constructorSpec to the passed-in options from the init method:

constructorSpec = options;
Next, I tell jQuery not to cache any AJAX requests:
$.ajaxSetup({
  cache: false
});

Then I set up the contacts grid to retrieve and to perform everything through AJAX requests:

pageGrids.contactsGrid.ajaxify({
  getPagedData: constructorSpec.contactsGridAction,
  getData: constructorSpec.contactsGridAction,
});

At this point you can run the application and the grid will make AJAX requests for sorting, paging and filtering contact records.

Next, I add the loadNewContact method, which calls the constructorSpec.newContactAction method to load the new contact into the contactContent div via jQuery.get:

var loadNewContact = function () {
  self.selectedId = null;
  self.dirtyAccount = false;
  $.get(constructorSpec.newContactAction, {}, function (result) {
    $("#contactContent").html(result);
  });
};

Then I add the loadContact method that loads an existing contact record through the constructorSpec.editContactAction method via jQuery.get into the contactContent div:

var loadContact = function (id) {
  self.selectedId = id;
  $.get(constructorSpec.editContactAction, { id: id }, function (result) {
    $("#contactContent").html(result);
  });
};

Next, I get the first grid row if there is one, and select and load it with the loadContact method. If there aren't any items, I load a new contact item through loadNewContact;

var initialRowToSelect = $(
  '#contactsGridContent .grid-mvc table tbody tr:not(.grid-empty-text):first');
if (initialRowToSelect.length > 0) {
  self.selectedId = $("#contactsGridContent 
    .grid-mvc table tbody tr:not(.grid-empty-text):first td[data-name='Id']").text();
  pageGrids.contactsGrid.markRowSelected(initialRowToSelect);
  loadContact(self.selectedId);
} else {
  loadNewContact();
}

Then I wire up the Create button that's displayed below the grid to call loadNewContact and to clear the selected grid row:

$("#Create").on('click', function (e) {
  e.preventDefault();
  loadNewContact();
  self.selectedId = null;
  try { pageGrids.contactsGrid.markRowSelected(null); } catch (e) { } finally { }
});

Next, I set up the AJAX grid onGridLoaded even to maintain the selected row and load the contact record after a grid sort, filter or page change:

pageGrids.contactsGrid.onGridLoaded(function (result) {
  var rowToSelect = $(
    "#contactsGridContent .grid-mvc table tbody tr td[data-name='Id']").filter(function () 
    return $(this).text() == self.selectedId;
  });
  pageGrids.contactsGrid.markRowSelected(rowToSelect.parent());
  if (self.selectedId) {
    loadContact(self.selectedId);
  }
});

Then I set up the contact grid onRowSelect event to load the selected contact record:

pageGrids.contactsGrid.onRowSelect(function (e) {
  self.selectedId = e.row.Id;
  loadContact(self.selectedId);
});

Next, I add the saveContact, which either inserts or updates the contact record by serializing the contactForm element. After the contact is saved, the selectedId is set if a new contact was created. The contact grid is refreshed regardless of a successful create or update.

If the save failed, then the contactContent div is updated with a resulting view with the model state errors displayed, as shown in Listing 5.

Listing 5: saveContact Method
var saveContact = function () {
  var dfd = new $.Deferred();

  var form = $("#contactForm");
  $.ajax({
    url: form.prop('action'),
    type: form.prop('method'),
    data: form.serialize()
  }).done(function(result) {
    if (result.Success) {
      if (result.Object) {
          self.selectedId = result.Object;
      }
      pageGrids.contactsGrid.refreshPartialGrid();
      dfd.resolve({ Success: true });
    } else {
      $("#contactContent").html(result);
      dfd.resolve({ Success: false });
    }
  });

  return dfd.promise();
};

Then I wire up the click event on the Save button to invoke the saveContact method:

$("body").on('click', '#Save', function (e) {
  e.preventDefault();
  saveContact();
});

Next, I add the deleteContact method, which confirms and deletes the contact record by the given id. If the delete was successful, then the selectedId is cleared and the grid is refreshed.

If the grid doesn't have items, then a new contact is loaded; otherwise, the updated contact is reselected in the grid and loaded. If the delete failed then the contactStatus div is updated with the error message shown in Listing 6.

Listing 6: The Error Message Shown If Delete Fails
var deleteContact = function (id) {
  if (confirm('Are you sure you want to delete this contact record?')) {
    $.post(constructorSpec.deleteContactAction, { id : id })
      .done(function(result) {
        if (result.Success) {
          // Clear selection
          self.selectedId = null;
          pageGrids.contactsGrid.refreshPartialGrid().done(function (response) {
            if (!response.HasItems) {
              $("#Create").click();
            } else {
              // Row is not already loaded
              if (self.selectedId == null) {
                // Load first record in the grid
                $("#contactsGridContent 
                  .grid-mvc table tbody tr:not(.grid-empty-text):first").click();
              }
            }
          });
        } else {
          $("#contactStatus").html(result.ErrorMessage);
        }
      });
  }
};

Last, I wire up the Delete button's click event:

$("body").on('click', '#Delete', function (e) {
  e.preventDefault();
  deleteContact(self.selectedId);
});
Listing 7 shows the completed contact JavaScript module implementation.
Listing 7: Completed Contacts-1.0.0.js Contact Module
var contact = (function (my, $) {
  var constructorSpec = {
    contactsGridAction: '',
    newContactAction: '',
    editContactAction: '',
    deleteContactAction: ''
  };
   
  my.init = function (options) {
    $(function () {
      var self = this;
           
      constructorSpec = options;

      $.ajaxSetup({
        cache: false
      });

      pageGrids.contactsGrid.ajaxify({
        getPagedData: constructorSpec.contactsGridAction,
        getData: constructorSpec.contactsGridAction,
      });

      var loadNewContact = function () {
        self.selectedId = null;
        self.dirtyAccount = false;
        $.get(constructorSpec.newContactAction, {}, function (result) {
          $("#contactContent").html(result);
        });
      };

      var loadContact = function (id) {
        self.selectedId = id;
        $.get(constructorSpec.editContactAction, { id: id }, function (result) {
          $("#contactContent").html(result);
        });
      };

      /* mark first row selected initially */
      var initialRowToSelect = 
        $('#contactsGridContent .grid-mvc table tbody tr:not(.grid-empty-text):first');
      if (initialRowToSelect.length > 0) {
        self.selectedId = $("#contactsGridContent 
          .grid-mvc table tbody tr:not(.grid-empty-text):first td[data-name='Id']").text();
        pageGrids.contactsGrid.markRowSelected(initialRowToSelect);
        loadContact(self.selectedId);
      } else {
        loadNewContact();
      }

      $("#Create").on('click', function (e) {
        e.preventDefault();
        loadNewContact();
        self.selectedId = null;
        try { pageGrids.contactsGrid.markRowSelected(null); } catch (e) { } finally { }
      });

      pageGrids.contactsGrid.onGridLoaded(function (result) {
        var rowToSelect = $("#contactsGridContent 
          .grid-mvc table tbody tr td[data-name='Id']").filter(function () {
          return $(this).text() == self.selectedId;
        });
        pageGrids.contactsGrid.markRowSelected(rowToSelect.parent());
        if (self.selectedId) {
          loadContact(self.selectedId);
        }
      });

      pageGrids.contactsGrid.onRowSelect(function (e) {
        self.selectedId = e.row.Id;
        loadContact(self.selectedId);
      });

      var saveContact = function () {
        var dfd = new $.Deferred();

        var form = $("#contactForm");
        $.ajax({
          url: form.prop('action'),
          type: form.prop('method'),
          data: form.serialize()
        }).done(function(result) {
          if (result.Success) {
            if (result.Object) {
              self.selectedId = result.Object;
            }
            pageGrids.contactsGrid.refreshPartialGrid();
            dfd.resolve({ Success: true });
          } else {
            $("#contactContent").html(result);
            dfd.resolve({ Success: false });
          }
        });

        return dfd.promise();
      };

      $("body").on('click', '#Save', function (e) {
        e.preventDefault();
        saveContact();
      });

      var deleteContact = function (id) {
        if (confirm('Are you sure you want to delete this contact record?')) {
          $.post(constructorSpec.deleteContactAction, { id : id })
            .done(function(result) {
              if (result.Success) {
                // Clear selection
                self.selectedId = null;
                pageGrids.contactsGrid.refreshPartialGrid().done(function (response) {
                  if (!response.HasItems) {
                    $("#Create").click();
                  } else {
                    // Row is not already loaded
                    if (self.selectedId == null) {
                      // Load first record in the grid
                        $("#contactsGridContent 
                          .grid-mvc table tbody tr:not(.grid-empty-text):first").click();
                    }
                  }
                });
              } else {
                $("#contactStatus").html(result.ErrorMessage);
              }
            });
        }
      };

      $("body").on('click', '#Delete', function (e) {
        e.preventDefault();
        deleteContact(self.selectedId);
      });
    });
  };

  return my;
}(contact || {}, jQuery));

The project is now completed and when you load the page you should be able to create a new contact record as seen in Figure 2.

[Click on image for larger view.] Figure 2. Contact Creation

You should then be able to update the created record as seen in Figure 3.

[Click on image for larger view.] Figure 3. Contact Update

Last, you should be able to delete the record as seen in Figure 4.

[Click on image for larger view.] Figure 4. Contact Deletion

As you can see with some effort you can test an ASP.NET MVC application's model, service and controller layers. I hope you enjoyed your journey implementing a full ASP.NET MVC application using test-driven development.

comments powered by Disqus

Featured

  • See Prompts Microsoft Engineers Use for Bleeding-Edge Multimodal RAG AI Research

    Vision-centric queries show how front-line experts are prompting LLMs these days.

  • AI Explains Expressions in Update to Java on VS Code

    "The Spring Tools now show code lenses above these expressions that allow you to quickly let GitHub Copilot explain those statements for you."

  • Microsoft Eases Integration with Semantic Kernel AI SDK

    The basic idea is to provide unified API abstractions, especially for idiomatic C# code, to help platform developers and others work with any provider with standard implementations for caching, telemetry, tool calling and other common tasks.

  • Final .NET 9 Preview Ships with Go-Live License

    Visual Studio developers can now download the SDK for .NET 9 Release Candidate 2 with a go-live license, meaning devs get Microsoft support for production applications even before the framework reaches general availability next month.

Subscribe on YouTube