April 2014

Volume 29 Number 4


Data Points : Adding New Life to a 10-Year-Old ASP.NET Web Forms App

Julie Lerman

Julie LermanLegacy code: can’t live with it, can’t live without it. And the better job you do with an app, the longer it will hang around. My very first ASP.NET Web Forms app has been in use for a little more than 10 years. It’s finally getting replaced with a tablet app someone else is writing. However, in the meantime, the client asked me to add a new feature to it that will let the company start collecting right away some of the data the new version will gather.

This isn’t a matter of a simple field or two. In the existing app—a complicated time sheet for tracking employee hours—there’s a dynamic set of checkboxes defined by a list of tasks. The client maintains that list in a separate application. In the Web app, a user can check any number of items on that list to specify the tasks he performed. The list contains a little more than 100 items and grows slowly over time.

Now, the client wants to track the number of hours spent on each selected task. The app will be used for only a few more months, so it didn’t make sense to invest a lot into it, but I had two important goals regarding the change:

  1. Make it really easy for the user to enter the hours, which meant not having to click any extra buttons or cause postbacks.
  2. Add the feature to the code in the least invasive way possible. While it would be tempting to overhaul the 10-year-old app with more modern tools, I wanted to add the new logic in a way that wouldn’t impact existing (working) code, including data access and the database.

I spent some time considering my options. Goal No. 2 meant leaving the CheckBoxList intact. I decided to contain the hours in a separate grid, but Goal No. 1 meant not using the ASP.NET GridView control (thank goodness). I decided to use a table and JavaScript for retrieving and persisting the task-hours data and I explored a few ways to achieve this. AJAX PageMethods to call the codebehind couldn’t be used because my page was retrieved using a Server.Transfer from another page. Inline calls, such as <%MyCodeBehindMethod()%>, worked until I had to do some complex data validation (too difficult to accomplish in JavaScript) that required a mix of client- and server-side objects. The situation also started getting ugly with the need to make everything touched by the inline call static. So I was failing at “least invasive.”

Finally, I realized I should really aim to keep the new logic totally separate and put it into a WebAPI that would be easy to access from JavaScript. This would help keep a clean separation between the new logic and the old.

Still, I had challenges. My prior expe­rience with Web API was to create a new MVC project. I started with that, but calling methods in the Web API from the existing app was causing Cross Origin Resource Sharing (CORS) issues that defied every pattern I could find for avoiding CORS. Finally, I discovered an article by Mike Wasson about adding a Web API directly into a Web Forms project (bit.ly/1jNZKzI) and I was on my way—though I had many bridges yet to cross. I won’t make you relive my pain as I bashed, thrashed and fumbled my way to success. Instead, I’ll walk you through the solution I ultimately reached.

Rather than present my client’s real application to demonstrate how I brought the new functionality into the old app, I’ll use a sample that tracks user preferences via comments about things they like to do: fun stuff. I’ll forgo the list of 100-plus items here; the form shows only a short CheckBoxList, as well as the additional work for data validation. And instead of tracking hours, I’ll track user comments.

Once I committed to the Web API, adding the validation method wasn’t a challenge at all. Because I was creating a new sample, I used the Microsoft .NET Framework 4.5 and Entity Framework 6 (EF) instead of .NET Framework 2.0 and raw ADO.NET. Figure 1 shows the starting point of the sample application: an ASP.NET Web Form with a user name and an editable CheckBoxList of possible activities. This is the page to which I’ll add the ability to track comments for each checked item as shown by the sketched-in grid.

Starting Point: A Simple ASP.NET Web Form with the Planned Addition
Figure 1 Starting Point: A Simple ASP.NET Web Form with the Planned Addition

Step 1: Add the New Class

I needed a class to store the new comments. I concluded that, given my data, it made the most sense to use a key composed of UserId and FunStuffId to determine to which user and fun activity the comment would attach:

namespace DomainTypes{
  public class FunStuffComment{
    [Key, Column(Order = 0)]
    public int UserId { get; set; }
    [Key, Column(Order = 1)]
    public int FunStuffId { get; set; }
    public string FunStuffName { get; set; }
    public string Comment { get; set; }
  }
}

Because I planned to use EF for persisting the data, I needed to specify the properties that would become my composite key. In EF, the trick for mapping composite keys is to add the Column Order attribute along with the Key attribute. I also want to point out the FunStuffName property. Even though I could cross-reference my FunStuff table to get the name of a particular entry, I found it easier to simply surface FunStuffName in this class. It might seem redundant, but keep in mind my goal to avoid messing with the existing logic.

Step 2: Adding Web API to the Web Forms-Based Project

Thanks to Wasson’s article, I learned I could add a Web API controller directly into the existing project. Just right-click the project in Solution Explorer and you’ll see Web API Controller Class as an option under the Add context menu. The controller that’s created is designed to work with MVC, so the first order of business is to remove all of the methods and add in my Comments method for retrieving existing comments for a particular user. Because I’ll be using the Breeze JavaScript library and have already installed it into my project using NuGet, I use Breeze naming conventions for my Web API Controller class, as you can see in Figure 2. I haven’t hooked the Comments into my data access yet, so I’ll begin by returning some in-memory data.

Figure 2 BreezeController Web API

namespace April2014SampleWebForms{
[BreezeController] 
public class BreezeController: ApiController  {
  [HttpGet]
  public IQueryable<FunStuffComment> Comments(int userId = 0)
    if (userId == 0){ // New user
      return new List<FunStuffComment>().AsQueryable();
    }
      return new List<FunStuffComment>{
        new FunStuffComment{FunStuffName = "Bike Ride",
          Comment = "Can't wait for spring!",FunStuffId = 1,UserId = 1},
        new FunStuffComment{FunStuffName = "Play in Snow",
          Comment = "Will we ever get snow?",FunStuffId = 2,UserId = 1},
        new FunStuffComment{FunStuffName = "Ski",
          Comment = "Also depends on that snow",FunStuffId = 3,UserId = 1}
      }.AsQueryable();    }
  }
}

Wasson’s article guides you to add routing to the global.asax file. But adding Breeze via NuGet creates a .config file with the appropriate routing already defined. That’s why I’m using the Breeze recommended naming in the controller in Figure 2.

Now I can call the Comments method easily from the client side of my FunStuffForm. I like to test my Web API in a browser to make sure things are working, which you can do by running the app and then browsing to https://localhost:1378/breeze/Breeze/Comments?UserId=1. Be sure to use the correct host:port your app is using.

Step 3: Adding Client-Side Data Binding

But I’m not done yet. I need to do something with that data, so I looked back to my previous columns on Knockout.js  (msdn.microsoft.com/magazine/jj133816 for JavaScript data binding) and Breeze (msdn.microsoft.com/magazine/jj863129, which makes the data binding even simpler). Breeze automatically transforms the results of my Web API into bindable objects that Knockout (and other APIs) can use directly, eliminating the need to create additional view models and mapping logic. Adding the data binding is the most intensive part of the conversion, made worse by my still very limited JavaScript and jQuery skills. But I persevered—and also became a semi-pro at JavaScript debugging in Chrome along the way. Most of the new code is in a separate JavaScript file that’s tied to my original Web Form page, FunStuffForm.aspx.

When I was nearly done with this article, someone pointed out that Knockout is now a bit dated (“It’s so 2012,” he said), and many JavaScript developers are using the simpler and richer frameworks such as AngularJS or DurandalJS instead. That’s a lesson for me to learn another day. I’m sure my 10-year-old app won’t mind a 2-year-old tool. But I’ll definitely be taking a look at these tools in a future column.

In my Web Form, I defined a table named comments with columns populated by fields of the data I’ll be binding to it with Knockout (see Figure 3). I’m also binding the UserId and FunStuffId fields, which I’ll need later, but keeping them hidden.

Figure 3 HTML Table Set Up for Binding with Knockout

<table id="comments">
  <thead>
    <tr>
      <th></th>
      <th></th>
      <th>Fun Stuff</th>
      <th>Comment</th>
    </tr>
  </thead>
  <tbody data-bind="foreach: comments">
    <tr>
      <td style="visibility: hidden" data-bind="text: UserId"></td>
      <td style="visibility: hidden" data-bind="text: FunStuffId"></td>
      <td data-bind="text: FunStuffName"></td>
      <td><input data-bind="value: Comment" /></td>
    </tr>
  </tbody>
</table>

The first chunk of logic in the JavaScript file that I called FunStuff.js is what’s known as a ready function and it will run as soon as the rendered document is ready. In my function, I define the viewModel type, shown in Figure 4, whose comments property I’ll use to bind to the comments table in my Web Form.

Figure 4 Beginning of FunStuff.js

var viewModel;
$(function() {
  viewModel = {
    comments: ko.observableArray(),
    addRange: addRange,
    add: add,
    remove: remove,
    exists: exists,
    errorMessage: ko.observable(""),
  };
  var serviceName = 'breeze/Comments';
  var vm = viewModel;
  var manager = new breeze.EntityManager(serviceName);
  getComments();
  ko.applyBindings(viewModel, 
    document.getElementById('comments'));
 // Other functions follow
});

The ready function also specifies some startup code:

  • serviceName defines the Web API uri
  • vm is a short alias for viewModel
  • manager sets up the Breeze EntityManager for the Web API
  • getComments is a method that calls the API and returns data
  • ko.applyBinding is a Knockout method to bind the viewModel to the comments tables

Notice that I’ve declared viewModel outside of the function. I’ll need access to it from a script in the .aspx page later, so it had to be scoped for external visibility.

The most important property in viewModel is an observableArray named comments. Knockout will keep track of what’s in the array and update the bound table when the array changes. The other properties just expose additional functions I’ve defined below this startup code through the viewModel.

Let’s start with the getComments function shown in Figure 5.

Figure 5 Querying Data Through the Web API Using Breeze

function getComments () {
  var query = breeze.EntityQuery.from("Comments")
    .withParameters({ UserId: document.getElementById('hiddenId').value });
  return manager.executeQuery(query)
    .then(saveSucceeded).fail(failed);
}
function saveSucceeded (data) {
  var count = data.results.length;
  log("Retrieved Comments: " + count);
  if (!count) {
    log("No Comments");
    return;
  }
  vm.comments(data.results);
}
function failed(error) {
  vm.errorMessage(error);
}

In the getComments function, I use Breeze to execute my Web API method, Comments, passing in the current UserId from a hidden field on the Web page. Remember I’ve already defined the uri of Breeze and Comments in the manager variable. If the query succeeds, the saveSucceeded function runs, logging some info on the screen and pushing the results of the query into the comments property of the viewModel. On my laptop, I can see the empty table before the asynchronous task is complete and then suddenly the table is populated with the results (see Figure 6). And remember, this is all happening on the client side. No postbacks are occurring so it’s a fluid experience for the user.

Comments Retrieved from Web API and Bound with the Help of Knockout.js
Figure 6 Comments Retrieved from Web API and Bound with the Help of Knockout.js

Step 4: Reacting to Boxes Being Checked and Unchecked

The next challenge was to make that list respond to the user’s selections from the Fun Stuff List. When an item is checked, it needs to be added or removed from the viewModel.comments array and the bound table depending on whether the user is adding or removing a checkmark. The logic for updating the array is in the JavaScript file, but the logic for alerting the model about the action resides in a script in the .aspx. It’s possible to bind functions such as a checkbox onclick to Knockout, but I didn’t take that route.

In the markup of the .aspx form, I added the following method to the page header section:

$("#checkBoxes").click(function(event) {
  var id = $(event.target)[0].value;
  if (event.target.nodeName == "INPUT") {
    var name = $(event.target)[0].parentElement.textContent;
    // alert('check!' + 'id:' + id + ' text:' + name);
    viewModel.updateCommentsList(id, name);  }
});

This is possible thanks to the fact that I have a div named checkBoxes surrounding all of the dynamically generated CheckBox controls. I use jQuery to grab the value of the CheckBox that’s triggering the event and the name in the related label. Then I pass those on to the updateCommentsList method of my viewModel. The alert is just for testing that I had the function wired properly.

Now let’s take a look at the updateCommentsList and related functions in my JavaScript file. A user might check or uncheck an item, so it needs to be either added or removed.  Rather than worry about the state of the checkbox, in my exists method I just let the Knockout utils function help me see if the item is already in the array of comments. If it is, I need to remove it. Because Breeze is tracking changes, I remove it from the observableArray but tell the Breeze change tracker to consider it deleted. This does two things. First, when I save, Breeze sends a DELETE command to the database (via EF in my case). But if the item is checked again and needs to be added back into the observableArray, Breeze simply restores it in the change tracker. Otherwise, because I’m using a composite key for the identity of comments, having both a new item and a deleted item with the same identity would create a conflict.  Notice that while Knockout responds to the push method for adding items, I must notify it that the array has mutated in order for it to respond to removing an item. Again, because of the data binding, the table changes dynamically as checkboxes are checked and unchecked.

Notice that when I create a new item, I’m grabbing the user’s userId from the hidden field in the form’s markup. In the original version of the form’s Page_Load, I set this value after grabbing the user. By tying the UserId and FunStuffId to each item in the comments, I can store all of the necessary data along with the comments to associate them with the correct user and item.

With oncheck wired up and the comments observableArray modified in response, I can see that, for example, toggling the Watch Doctor Who checkbox causes the Watch Doctor Who row to display or disappear based on the state of the checkbox.

Step 5: Saving Comments

My page already has a Save feature for saving the checkboxes marked true, but now I want to save the comments at the same time using another Web API method. The existing save method executes when the page posts back in response to the SaveThatStuff button click. Its logic is in the page codebehind. I can actually make a client-side call to save the comments prior to the server-side call using the same button click. I knew this was possible with Web Forms using an old-school onClientClick attribute, but in the timesheet application I was modifying, I also had to perform a validation that would determine if the task hours and time sheet were ready to be saved. If the validation failed, not only did I have to forget about the Web API save, but I had to prevent the postback and server-side save method from executing as well. I was having a hard time working this out using onClientClick, which encouraged me to modernize again with jQuery. In the same way I can respond to the CheckBox clicks in the client, I can have a client-side response to btnSave being clicked. And it will happen prior to the postback and server-side response. So I get to have both events on one click of the button, like so: 

$("#btnSave").click(function(event) {
  validationResult = viewModel.validate();
  if (validationResult == false) {
    alert("validation failed");
    event.preventDefault();
  } else {
    viewModel.save();
  }
});

I have a stub validation method in the sample that always returns true, though I tested to be sure things behave properly if it returns false. In that case, I use the JavaScript event.preventDefault to stop further processing. Not only will I not save the comments, but the postback and server-side save will not occur. Otherwise, I call viewModel.save and the page continues with the button’s server-side behavior, saving the user’s FunStuff choices. My saveComments function is called by viewModel.save, which asks the Breeze entityManager to execute a saveChanges:

function saveComments() {
  manager.saveChanges()
    .then(saveSucceeded)
    .fail(failed);
}

This in turn finds my controller SaveChanges method and executes it:

[HttpPost]
  public SaveResult SaveChanges(JObject saveBundle)
  {
    return _contextProvider.SaveChanges(saveBundle);
  }

For this to work, I added Comments into the EF6 data layer and then switched the Comments controller method to execute a query against the database using the Breeze server-side component (which makes a call to my EF6 data layer). So the data returned to the client will be data from the database, which SaveChanges can then save back to the database. You can see this in the download sample, which uses EF6 and Code First and will create and seed a sample database.

Figure 7 JavaScript for Updating the Comments List in Response to a User Clicking on the Checkboxes

function updateCommentsList(selectedValue, selectedText) {
  if (exists(selectedValue)) {
    var comment = remove(selectedValue);
    comment.entityAspect.setDeleted();
  } else {
  var deleted = manager.getChanges().filter(function (e) {
    return e.FunStuffId() == selectedValue
  })[0];  // Note: .filter won't work in IE8 or earlier
  var newSelection;
  if (deleted) {
    newSelection = deleted;
    deleted.entityAspect.rejectChanges();
  } else {
    newSelection = manager.createEntity('FunStuffComment', {
      'UserId': document.getElementById('hiddenId').value,
      'FunStuffId': selectedValue,
      'FunStuffName': selectedText,
      'Comment': ""
    });
  }
  viewModel.comments.push(newSelection);    }
  function exists(stuffId) {
    var existingItem = ko.utils.arrayFirst(vm.comments(), function (item) {
      return stuffId == item.FunStuffId();
    });
    return existingItem != null;
  };
  function remove(stuffId) {
    var selected = ko.utils.arrayFirst
    (vm.comments(), function (item) {
    return stuffId == item.FunStuffId;
    });
    ko.utils.arrayRemoveItem(vm.comments(), selected);
    vm.comments.valueHasMutated();
  };

JavaScript with a Little Help from My Friends

Working on this project and on the sample built for this article, I wrote more JavaScript than I ever had before. It’s not my area of expertise (as I’ve pointed out frequently in this column), though I was quite proud of what I had accomplished. However, knowing that many readers might be seeing some of these techniques for the first time, I leaned on Ward Bell from IdeaBlade (the creators of Breeze) for an in-depth code review, along with some pair programming to help me clean up some of my Breeze work as well as JavaScript and jQuery. Except perhaps for the now “dated” use of Knockout.js, the sample you can download should provide some good lessons. But, remember, the focus is about enhancing an old Web Forms project with these more modern techniques that make the end-user experience so much more pleasant.


Julie Lerman is a Microsoft MVP, .NET mentor and consultant who lives in the hills of Vermont. You can find her presenting on data access and other Microsoft .NET topics at user groups and conferences around the world. She blogs at thedatafarm.com/blog and is the author of “Programming Entity Framework” (2010) as well as a Code First edition (2011) and a DbContext edition (2012), all from O’Reilly Media. Follow her on Twitter at twitter.com/julielerman and see her Pluralsight courses at juliel.me/PS-Videos.

Thanks to the following Microsoft technical experts for reviewing this article: Damian Edwards (dedward@microsoft.com) and Scott Hunter (Scott.Hunter@microsoft.com)