Sunday, May 5, 2013

ASP.NET MVC, jQuery, Checkboxes, Tables, JSON

In this post we will be looking at the following:
1). Creating and populating a HTML table.
2). Clicking a button in the table, causing it to check a checkbox.
3). Clicking button outside of table, triggering a modal dialog to be loaded.
4). Send data back to the controller based on which checkbox was checked.
5). Send the data as text and then send it as JSON.

Here is what we are building, the main screen:


When a user clicks the Notify button off to the right, the checkbox in that row should be checked. Note in the screenshot Leonard and Raj are selected.


The user will click the Send Notifications button after they have made all of their selections. This will trigger a modal dialog to appear on the screen. The dialog will be populated with the email address of the selected/checked row in the table. The user can then fill out the body and reason, then have the email sent.


Spin up a new MVC project. Add a PersonViewModel class to the models folder:
public class PersonViewModel
{
   public int PersonId { get; set; }
   public string FirstName { get; set; }
   public string LastName { get; set; }
   public string Age { get; set; }
   public string FavoriteFood { get; set; }
   public string Email { get; set; }
}

In the HomeController, delete the view. We will create a method to give us some data to play with. We will come back to the view in a moment.
public ActionResult Index()
{
   List<PersonViewModel> people = GetPeople();
   return View(people);
}

private static List<PersonViewModel> GetPeple()
{
   return new List<PersonViewModel>()
   {
      new PersonViewModel() { PersonId = 1, FirstName = "Sheldon", LastName = "Cooper", Age = "35", FavoriteFood = "Greek Food", Email = "sheldon@bigbang.com" }, 
      new PersonViewModel() { PersonId = 2, FirstName = "Leonard", LastName = "Hoffstadter", Age = "35", FavoriteFood = "Cheese", Email = "leonard@bigbang.com" },
      new PersonViewModel() { PersonId = 3, FirstName = "Raj", LastName = "Koothrappali", Age = "35", FavoriteFood = "Indian Food", Email = "raj@bigbang.com" },
      new PersonViewModel() { PersonId = 4, FirstName = "Howard", LastName = "Wallowitz", Age = "35", FavoriteFood = "Brisket", Email = "howard@bigbang.com" },
      new PersonViewModel() { PersonId = 5, FirstName = "Stuart", LastName = "Bloom", Age = "35", FavoriteFood = "Can of tuna", Email = "stuart@bigbang.com" }
   };
}

Now let's set up the view. The view will be strongly typed to an IEnumerable of PersonViewModel. Be sure to add script references for jQuery and jQuery UI, either inline or through bundling:
@model IEnumerable<YourProjectName.Models.PersonViewModel>

@{
   ViewBag.Title = "Index";
}

<div id="notificationDiv">
   <input type="button" id="btnSendNotifications" name="btnSendNotifications" value="Send Notifications" class="ui-button ui-button-text-only ui-widget ui-state-default ui-corner-all" />
</div>

<table id="tblPeople">
   <tr class="tableHeader">
      <th></th>
      <th>Id</th>
      <th>First Name</th>
      <th>Last Name</th>
      <th>Age</th>
      <th>Favorite Food</th>
      <th>Email</th>
      <th></th>
   </tr>

   @foreach (var item in Model)
   {
      <tr>
         <td>
            <input type="checkbox" name="ckCheck" id='@("ckCheck"+@item.PersonId)' />
         </td>
         <td id="personId">@item.PersonId</td>
         <td id="firstName">@item.FirstName</td>
         <td id="lastName">@item.LastName</td>
         <td id="age">@item.Age</td>
         <td id="favoriteFood>@item.FavoriteFood</td>
         <td id="email">@item.Email</td>
         <td>
            <input type="button" id='@("btnNotify"+item.PersonId)' name="btnNotify" value="Notify" class="ui-button ui-button-text-only ui-widget ui-state-default ui-corner-all" />
         </td>
      </tr>
   }
<table>

Let's break this down a bit. The notificationDiv at the top of the page is what will take care of sending the notifications. As we will see, the click event will spin through the table rows to find which checkboxes are clicked and grab the email addresses. We are using jQuery UI to style the buttons and make them a little more pretty. Next we go through and create the table header. Then we spin through the model and write out the data.

A few things to note. I am being a little lazy and using foreach here. MVC purists would say to use a partial view as to get rid of the logic in the view. I agree but I admit I am being a little lazy. Note the technique for getting unique ids for the checkboxes and the Notify button. Razor syntax makes this possible. Here we are using it in two places: id='@("ckCheck"+@item.PersonId)' and id='@("btnNotify"+item.PersonId)'. I like this technique as it is an easy way to get unique ids.

On to the next order of business. We are going to set up a modal dialog. The purpose of this dialog is to display to the user all the email addresses to be sent back to the server. This will give a nice visual display of letting them double check their selections. They can then enter the message and reason, and finally, submit the request. Right after the table declaration in the view, add:

<div id="dialogTarget" title="Send Notifications>
   <p>All form fields are required.</p>
   <table id="peeps">
      <tr>
         <td>Bcc Message To:</td>
         <td>@Html.TextBox("ccTo", "", new { style = "width:300px;" })</td>
      </tr>
      <tr>
         <td>Message:</td>
         <td>@Html.TextArea("message", new { style = "width:300px;height:120px;" })</td>
      </tr>
      <tr>
         <td>Reason:</td>
         <td>@Html.TextBox("Reason", "", new { style = "width:300px;" })</td>
      </tr>
   </table>
</div>

All of the HTML is now good to go. Let's lay in all the script. Again, make sure your script references are set up the way you like them. We will go through piece by piece. First we will add some CSS to our table rows for the zebra striping effect. Will come back later and fill in the css business:

$('#tblPeople tr:even').addClass("alt");
$('#tblPeople tr:odd').addClass("altOdd");

Each row in the table has a Notify button. We need to handle the click event for this button and find the checkbox in that row, then check that checkbox. Since our buttons and checkboxes have unique ids, we will use the input selector 'input[name^="btnNotify"]'. The name^ syntax means "attribute starts with". We will grab the last character of the button id which will be a number. That number will then tell us which checkbox we should find, then check that checkbox:
$('input[name^="btnNotify"]').click(function (e) {
   var lastChar = e.target.id.substr(e.target.id.length - 1);
   ($(e).closest('td').find($('#ckCheck' + lastChar).attr('checked', true)));
});
The Send Notifications button click event has to go through each row in the table and find which checkboxes have been clicked. When the checkbox state is checked, we have to grab the email address from that row and put it into an array. This click event will also set the value of the ccTo field of the dialog. The last step of the event is to open the dialog.
$('#btnSendNotifications').click(function (e) {
   var emailAddresses = new Array();

   $('#tblPeople tr').each(function () {
      var checkbox = $(this).find($('input[name="ckCheck"]'));

      if (checkbox.is(":checked")) {
         var email = $(this).find('#email');
         emailAddresses.push(email.text());
      }
   });

   $('#ccTo').val(emailAddresses);
   $('#dialogTarget').dialog('open');
});

When the modal dialog pops open it should display the email addresses that were selected. The user can then fill out the message and the reason. Clicking the Notify button will fire off an Ajax request back to the server. When we return from the server we have to make sure that all selected checkboxes are changed to not being selected:
$('#dialogTarget').dialog({
   modal: true,
   autoOpen: false,
   width: 500,
   buttons: {
      "Notify": function (e) {
         var ccTo = $('#ccTo').val();
         var message = $('#message').val();
         var reason = $('#reason').val();
         $.ajax({
            url: '@Url.Action("EmailInfo", "Home")',
            type: "POST",
            dataType: "text",
            data: ({ ccTo: ccTo, message: message, reason: reason }),
            success: function (result) {
               alert(result);
               $('#dialogTarget').dialog('close');
               $('#tblPeople tr').each(function () {
                  var checkBox = $(this).find($('input[name="ckCheck"]'));

                  if (checkBox.is(":checked")) {
                     checkBox.attr('checked', false);
                  }
               }
            },
            error: function (jqXHR) {
               alert('Error: ' + jqXHR.responseText);
            }
         });
      },
      "Cancel": function () {
         $(this).dialog('close');
         $('#tblPeople tr').each(function () {
            var checkBox = $(this).find($('input[name="ckCheck"]'));

            if (checkBox.is(":checked")) {
               checkBox.attr('checked', false);
            }
         });
      }
   }
});

I can already hear the cringing....what the, why are you sending text data back?!?!? Hey now, just showing something different. We will come back and make it JSON. Let's take care of the server side business. We will need an Action called EmailInfo that will be expecting three string parameters. This Action will simple return a content message.
[HttpPost]
public ContentResult EmailInfo(string ccTo, string message, string reason)
{
   string result = "Email has been sent.";
   return Content(result);
}

Ok let's switch it all over to JSON. We will need a view model, EmailViewModel:
public class EmailViewModel
{
   public string EmailAddresses { get; set; }
   public string Message { get; set; }
   public string Reason { get; set; }
}

Change the EmailInfo Action:
[HttpPost]
public ActionResult EmailInfo(EmailViewModel email)
{
   if (ModelState.IsValid)
   {
      return Json(new { Message = "Email has been sent." });
   }
   else
   {
      return Json(new { Message = "Error, email was not sent." });
   }
}

The only change that we need to make in the JavaScript is in the Ajax call, more specifically, the data variable, and the success callback:
$.ajax({
   url: '@Url.Action("EmailInfo", "Home"),
   type: "POST",
   dataType: "json",
   data: {
      emailAddresses: ccTo,
      message: message,
      reason: reason
   },
   success: function (result) {
      alert(result.Message);
      $('#dialogTarget').dialog('close');
      $('#tblPeople tr').each(function () {
         var checkBox = $(this).find($('input[name="ckCheck"]'));

         if (checkBox.is(":checked")) {
            checkBox.attr('checked', false);
         }
      });
   },
   error: function (jqXHR) {
      alert('Error: ' + jqXHR.responseText);
   }
});

The final step is the css garbage. First, we will remove the border that is put around the checkboxes:
input[type="checkbox"] {
   background: transparent;
   border: none;
   width: auto;
}

And the css for the table:
table {
   border: solid 1px #e8eef4;
   border-collapse: collapse;
}

table td {
   padding: 5px;
   border: solid 1px #e8eef4;
   border-color: black;
}

table th {
   padding: 6px 5px;
   text-align: left;
   border: solid 1px #e8eef4;
}

tr.alt td {
   background-color: #F7F7DE;
   border-style: solid;
   border-width: 1px;
   border-color: black;
}

tr.altOdd td {
   border-style: solid;
   border-width: 1px;
   border-color: #000000
}

tr.tableHeader th {
   color: #FFFFFF
   background-color: #6B696B;
   font-weight: bold;
   border-style: solid;
   border-width: 1px;
   border-color: #000000;