The Practical Client
Simplifying Single-Page Applications with ASP.NET MVC Partial Views
Partial Views can make creating Single-Page Applications dramatically easier by better achieving the goals of the MVC design pattern. Here, in TypeScript, is how to leverage Partial Pages to create an AJAX-enabled application in ASP.NET MVC.
In the MVC pattern, Views are supposed to be so brain-dead simple that they don't require testing: all your logic should be in your Controller, which can be tested using test-driven development (TDD) tools. In addition, in the MVC pattern, program logic and presentation are supposed to be loosely coupled. In ASP.NET MVC, changes to the way the application presents itself should only require changes to the HTML in the relevant View -- not changes to the application's program logic. In another column I showed how to leverage ASP.NET Partial Views to achieve those goals.
However, that column delivered an extremely traditional solution -- one that depended on posting back the whole page back to the server to retrieve or update data. These days, developers want to create Single Page Applications (SPAs) that engage in AJAX-enabled conversations with the server for all retrieval and update activities. Unfortunately, the resulting Views in these SPAs often contain client-side code with lots and lots of embedded HTML, violating the separation of presentation and program logic.
It doesn't have to be that way. As I'll demonstrate in this column and the next one, Partial Views provide a simpler way to create an "AJAXified" application by leaving your HTML generation where it belongs: in ASP.NET Views that contain nothing but HTML and references to Model properties. This column shows how to structure your client-side code (using the MVVM pattern), by looking at what server-side and client-side code is required by the application and how those two sets of code can start to be integrated (next month's column will look at the details of implementing update activities). Because I'm assuming this is an enterprise application, my client-side code is written in TypeScript (see "Configuring Visual Studio 2012" for more on how to configure a Visual Studio 2012 ASP.NET MVC project for TypeScript and how to get the Visual Studio 2012 toolkit to work with jQuery).
The SPA I'm building initially displays a list of sales orders in a table (that initial display is generated entirely on the server). Each sales order row in that table has two buttons: one to enable editing for the sales order in the row, the other to delete the sales order in the row. At the top of the table there's an add button that users can click to insert a blank row at the end of the table for adding a new sales order. When the table is in either edit or add mode, the add button at the top of the table and the buttons on the rows not being edited are hidden -- only the row in edit or add mode has any buttons: One to save changes and one to discard any changes and return to the initial display. Returning to the initial display also redisplays the buttons on every row and the add button at the top of the table.
First Server-Side Code
My Controller needs three methods to deliver the rows I use in my sales order table. The first method returns a Partial View that displays sales order information as plain text. The method accepts a SalesOrderDTO object (which may just hold a sales order's ID value) and passes that object to another method that returns a complete SalesOrderDTO object. I use the resulting SalesOrderDTO object to generate a Partial View that I return to the user:
public ActionResult GetForDisplay(SalesOrderDTO so)
{
SalesOrderDTO s;
s = BuildSalesOrderDO(so);
return PartialView("RowDisplay", s);
}
The RowDisplay Partial View used in the method has no logic -- only the HTML for a single row, beginning with the edit/remove buttons in the row's first cell. As you can see in Listing 1 each button is flagged with an attribute called data-buttontype that indicates whether the button is used to trigger edits or deletes. The last cell in the row holds the SalesOrderDTO's ID property in a hidden element.
Listing 1: Start of a Display-Only Row
<td>
<div id="buttonseditgroup">
<button type="button" data-buttontype="edit">
<span class="glyphicon glyphicon-pencil" />
</button>
<button type="button" data-buttontype="remove">
<span class="glyphicon glyphicon-remove" />
</button>
</div>
</td>
<td>
@Html.DisplayFor(m => m.CustomerName)
</td>
...cells for other SalesOrderDTO properties...
<td>
@Html.HiddenFor(m => m.ID)
</td>
My second Controller method is very similar to my first one, except that the method returns a Partial View called RowUpdate:
public ActionResult GetForEdit(SalesOrderDTO so)
{
SalesOrderDTO s;
s = BuildSalesOrderDTO(so);
return PartialView("RowUpdate", s);
}
The RowUpdate Partial View (Listing 2) is similar to RowDisplay, the only differences being the buttons in the first cell ("save" and "cancel" instead of "edit" and "remove") and the cells contain textboxes to support updating the data:
Listing 2: A Row for Updating SalesOrderDTO Properties
<td>
<div id="buttonseditgroup">
<button type="button" data-buttontype='save'>
<span class="glyphicon glyphicon-floppy-disk" />
</button>
<button type="button" data-buttontype='cancel' >
<span class="glyphicon glyphicon-ban-circle" />
</button>
</div>
</td>
<td>
<div id="buttonseditgroup">
<button type="button" data-buttontype="save">
<span class="glyphicon glyphicon-disk" />
</button>
<button type="button" data-buttontype="cancel">
<span class="glyphicon glyphicon-circle" />
</button>
</div>
</td>
<td>
@Html.EditorFor(m => m.CustomerName)
</td>
...cells for other SalesOrderDTO properties...
<td>
@Html.HiddenFor(m => m.ID)
</td>
The third method is the simplest because it doesn't have to retrieve a SalesOrderDTO. This method simply calls a Partial View that returns a row of empty textboxes for adding a new SalesOrderDTO:
public ActionResult GetForAdd()
{
return PartialView("RowNew", new SalesOrderDTO());
}
The RowNew Partial View is almost identical to the RowUpdate Partial View. The only real difference is that the RowUpdate's HTML is used to create a band-new row so it includes tr tags (the RowUpdate's HTML is used to replace the contents of an existing row and, as a result, doesn't require tr tags of its own):
<tr>
<td>
<div id="buttonseditgroup">
...button tags...
</div>
</td>
...cells for other SalesOrderDTO properties...
</tr>
First Client-Side Code
In my client-side code, I first define a Data Transfer Object (called SalesOrderDTO) that holds all the data for a sales order:
class SalesOrderDTO
{
ID: number;
CustomerNumber: string;
...remaining SalesOrderDTO properties...
}
My next class is a View Model object (SalesOrderVM) to hold all of the code for handling my screen and communicating with the server. Right at the top, I declare a variable to hold my SalesOrderDTO object:
class SalesOrderVM {
so: SalesOrderDTO;
In my VM's constructor, I wire up the functions I'll be creating in my VM to the click event of buttons on my page. For the add button at the top of the table, which is always on the page, I just use the jQuery click function to wire up my add button to its function:
constructor()
{
$("#addSalesOrderButton").click(this.AddRow);
However, I keep adding and removing the other buttons on the page. Rather than repeatedly using the jQuery click function to keep those buttons wired up correctly as I add and remove them, I use the jQuery on function. The on function monitors the page (or part of it; in my case, my SalesOrderTable) and wires up events to my buttons as they appear. In this code I use my data-buttontype attribute to identify which buttons are tied to each of the functions in my VM:
$("#SalesOrderTable").on("click", "[data-buttontype ='edit']", this.EditRow);
$("#SalesOrderTable").on("click", "[data-buttontype='save']", this.SaveRow)
$("#SalesOrderTable").on("click", "[data-buttontype='cancel']", this.CancelEdit)
$("#SalesOrderTable").on("click", "[data-buttontype ='remove']", this.DeleteRow);
}
}
As this code implies, I have five functions in my client-side VM:
- AddRow: Fetches the RowNew Partial View from my Controller and inserts the row at the bottom of the page to allow adding a new sales order.
- EditRow: Fetches the RowUpdate Partial View from my Controller and replaces the current to allow updating a sales order.
- SaveRow: Gathers the data from an updateable row and sends it to the server for processing. This function also replaces the updateable version of the row (from RowUpdate) with the row returned by the RowDisplay Partial View.
- CancelEdit: Handles exiting an updateable row. If the row was in edit mode, the function replaces the row with the RowDisplay Partial View; if the row was in add mode, the function removes the row from the table.
- DeleteRow: Deletes the sales order in the current row and removes the row from the table.
Next time, I'll show how those functions work and provide the corresponding server-side functions that add, delete and update sales orders.
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/.