The Practical Client
Integrating Updates, Deletes and Inserts with TypeScript and ASP.NET MVC
Peter returns to his AJAX-enabled ASP.NET MVC application to show how Partial Views and TypeScript work together to simplify delivering a Single-page application.
In last month's Practical TypeScript column, I started to build a Single-Page Application (SPA) in ASP.NET MVC, leveraging Partial Views to reduce the amount of HTML and data manipulation code required in my client-side code. This column builds on the groundwork I laid last month to focus on implementing the updates, inserts and deletes in the application.
Implementing Add Mode
Last month, I created a TypeScript class that acts as a ViewModel, handling all of my onscreen interactions (I called the class SalesOrderVM). In the constructor for that class, I wire up the five functions in my ViewModel to matching buttons in my page. Those buttons, through my ViewModel's functions, enable users to add a new sales order to the table, edit an existing sales order, delete a sales order, save changes to a sales order (either for adds or updates), and cancel an add or update.
The add button (which inserts a blank row at the end of the table of sales orders for adding a new sales order) is wired up to the simplest function in my ViewModel: AddRow. In AddRow (shown in Listing 1), all I do is use jQuery's ajax function to call a server-side Action method called GetForAdd. The GetForAdd method (shown last month) just returns the HTML for a table row, generated from a Partial View on the server. I add that able row to the bottom of the table displaying the sales orders (the table has an id attribute set to SalesOrderTable).
The only really interesting code in the AddRow method appears in the ajax function's success function. That code uses the jQuery after function to insert the HTML returned from the server's Partial View after the last row in the table. The function also hides all the buttons on the screen except for buttons in the row I've just added.
Listing 1: Inserting a Partial View into a Table
AddRow()
{
$.ajax({
datatype: "text/html",
type: "get",
url: 'http://MyServer/SO/GetForAdd',
cache: false,
success: function (row: string) {
$("#SalesOrderTable tr:last").after(row);
$("#addSalesOrderButton").hide();
$("[data-buttontype = 'edit']").hide();
$("[data-buttontype = 'remove']").hide();}
});
}
Note what's missing here: any reference to sales order-related HTML. All of that HTML is handled in the Partial View returned by the GetForAdd method. If I need to make any changes in the way sales order data is displayed in the page, I change the HTML in my Partial View without having to rewrite (or re-test) any of my client-side code. The client-side code is only responsible for managing non-data related code (displaying and hiding the addSalesOrderButton, for example).
There is a cost to this division of labor: Returning a simple JSON object from the server with just sales order data would result in a smaller payload than returning the HTML I've generated from my Partial View. It would, I suspect, be hard to measure that difference in performance caused by those two payloads (and by "hard," I mean "impossible"). In return for returning the Partial View already populated with data, though, I've also eliminated the client-side code required to extract the data from the JSON object and insert it into the relevant HTML elements -- code that would need to be revised if any of those elements were changed in any way. It's a trade-off I'm willing to make.
Implementing Add Mode
Putting a row in edit mode is only slightly more complicated than putting a row in add mode. The reason for the added complexity is that I use the opportunity to also get the latest version of the sales order data from the server.
My EditRow function (Listing 2) first hides all the other update buttons on the screen. The code then finds the row in the table for the button that triggered this processing (that button is represented by the JavaScript in this reference). Once that row is found, I use the jQuery find function to retrieve the sales order's ID in the hidden field in the row. I store the ID's value in a SalesOrderDTO object, which I pass to my server-side GetForEdit method (shown in last month's column) through the jQuery ajax function.
The GetForEdit method returns the HTML from a Partial View that displays the sales order's current data in textboxes for the user to update. Once I have the HTML returned from the server, I use it to replace the HTML for the current row, using the jQuery html function.
Listing 2: Putting a Table Row into Edit Mode
EditRow()
{
$("#addSalesOrdersButton").hide();
$("[data-buttontype = 'edit']").hide();
$("[data-buttontype = 'remove']").hide();
var elm: JQuery;
elm = $(this).closest("tr");
this.so = new SalesOrderDTO();
this.so.ID = elm.find("#ID").val();
$.ajax({
data: this.so,
datatype: "text/html",
type: 'POST',
url: 'http://MyServer/SO/GetForEdit',
cache: false,
success: function (row) {
elm.html(row);
}
});
}
Deleting SalesOrders and Table Rows
My DeleteRow function for deleting sales orders (Listing 3) is slightly more complicated than the AddRow function, but simpler than the EditRow function. As with EditRow, DeleteRow finds the current row in the table, retrieves the sales order ID from the hidden field in the row, and (after loading the ID into my SalesOrderDTO object) calls my server-side Delete method. When the method returns, I use jQuery's remove method to delete the row from the table.
Listing 3: Deleting a Row in the Table
DeleteRow()
{
var elm: JQuery;
elm = $(this).closest("tr");
this.so = new SalesOrderDTO();
this.so.ID = elm.find("#ID").val();
$.ajax({
data: this.so,
datatype: "text/html",
type: "Delete",
url: 'http://MyServer/SO/Delete',
cache: false,
success: function () {
elm.remove();
}
});
}
On the server, my Delete method just calls the method that handles deleting sales orders and returns nothing:
public ActionResult Delete(EventSalesOrders em)
{
DeleteSalesOrder(em.EventSalesOrderID);
return new EmptyResult();
}
Saving and Cancelling Changes
The user can do one of two things with a row in edit mode: Click the cancel button to discard their changes or click the save button to send their changes to the server for processing. In both cases, I once again grasp the opportunity to retrieve the latest data from the server and display it to the user.
Of the two functions involved, My CancelEdit function (Listing 4), which is tied to the Cancel button, initially looks like my previous client-side functions: I once again find the current row and grab the ID value from a hidden element in the row. However, if that ID value is zero, it indicates that the user was adding a new sales order and all I need to do is delete the current row from the table. If the ID isn't zero (indicating the user was updating an existing order), I call my server-side GetForDisplay method to retrieve the latest data for the sales order and replace the row in the table with the resulting HTML. Either way, I finish by redisplaying the buttons that let the user initiate some other update activity.
Listing 4: Canceling an Add or Update Operation in the Client
CancelEdit()
{
var elm: JQuery;
elm = $(this).closest("tr");
this.so = new SalesOrderDTO();
this.so.ID = elm.find("#ID").val();
if (this.so.ID == 0)
{
elm.remove();
}
else
{
$.ajax({
data: this.so,
datatype: "text/html",
type: 'POST',
url: 'http://MyServer/SO/GetForDisplay',
cache: false,
success: function (row) {
elm.html(row);
}
}
});
}
$("#addSalesOrdersButton").show();
$("[data-buttontype = 'edit']").show();
$("[data-buttontype = 'remove']").show();
}
The most complicated client-side function is the one that handles saving a row (Listing 5). This function is also the only one that directly references the HTML used to display sales order data. As a result, it is the one method that would have to be rewritten if the data presentation was changed (and there exists jQuery plug-ins that would separate my HTML from my TypeScript code, though I'm not using them here). As with the CancelEdit function, after getting the new row from the server, I re-display the buttons on the rest of the rows in the table.
Listing 5: Saving Inserts or Adds from the Client
SaveRow()
{
var elm: JQuery;
elm = $(this).closest("tr");
this.so = new SalesOrderDTO();
this.so.ID = elm.find("#ID").val();
this.so.CustomerNumber = elm.find('#CustomerNumber').val();
...code to set rest of SalesOrderDTO properties...
$.ajax({
data: this.so,
datatype: "text/html",
type: "POST",
url: 'http://MyServer/SO/Put',
cache: false,
success: function (row: string) {
elm.html(row);
$("#addSalesOrdersButton").show();
$("[data-buttontype = 'edit']").show();
$("[data-buttontype = 'remove']").show();
}
});
}
The SaveRow function calls my Put method in my server-side Controller (Listing 6). In my Put method, I check the SalesOrderDTO's ID property to see if the user is adding or updating a sales order and call the appropriate method (both of which return the latest version of the sales order in case processing either updated any of the user's data or provided default values). I pass that SalesOrderDTO to a Partial View that generates the HTML I return to the client.
Listing 6: Server-Side Code To Update or Add a Sales Order
public ActionResult Put(SalesOrders so)
{
SalesOrderDTO s;
if (so.ID != 0)
{
s = UpdateSalesOrder(so);
}
else
{
s = AddSalesOrder(so);
}
s = GetEventSalesOrder(so);
return PartialView("RowDisplay", s);
}
For all of this to work, I need to start up my SalesOrderVM in the browser (as I discussed in last month's column, the ViewModel's constructor wires up the ViewModel's functions with the buttons on the page). Fortunately, that requires just three lines of code outside of my SalesOrderDTO and SalesOrderVM classes:
$(function() {
var soVM: SalesOrderVM;
soVM = new SalesOrderVM();
});
One last bonus of this strategy: The structure for this application will work for almost any page that displays a table of updateable items. In fact, I rolled this design out over five separate pages in my client's application, just changing the names of the DTO/VM classes and putting different HTML in my Partial Views.
There are probably better solutions out there (had my client not insisted on avoiding any third-party libraries, I would have been tempted to use Knockout, for example). But if you're rolling your own ASP.NET MVC solution, leveraging Partial Views will help you meet the original goals of the MVC design pattern. More important, meeting those goals will simplify your application.
About the Author
Peter Vogel is a system architect and principal in PH&V Information Services. PH&V provides full-stack consulting from UX design through object modeling to database design. Peter tweets about his VSM columns with the hashtag #vogelarticles. His blog posts on user experience design can be found at http://blog.learningtree.com/tag/ui/.