The Practical Client

Creating a Master/Detail Page with Backbone and TypeScript

There are two strategies for downloading multiple objects from your service with Backbone: The obvious one and the fast one. Peter implements the fast one.

In this column, I'm going to go beyond creating a basic Backbone application with TypeScript that retrieves data and show how to create a master/detail page where the user selects a single entry which triggers a display of related data that includes multiple rows (the typical example is a sales order header with multiple rows of sales order details). In this application, after the user selects a customer from a dropdown list, the top of my page will display that single customer while the bottom of the page will display all of the sales orders for that customer.

And, before I begin, my usual caveat for these columns: Before I started working on this part of the case study, I updated all of my NuGet packages. That may make some of the code you see here incompatible with the downloads I've provided with previous columns. If you've been using those downloads, your safest bet is to abandon them and start again with this month's.

Two Potential Solutions
On the page I've built so far, I have a dropdown list of customers that allows the user to select a customer. When the user selects a customer, I retrieve details for that customer and display it (see my October column, "Support Updates in a Page with TypeScript and Backbone"). In this column, I'm going to add a table below the customer information that lists key information about each sales order made by the customer (by the way, I'm using the standard AdventureWorks database for this case study).

There are two ways I could handle this new feature. First, when the user selects a customer, I could make two trips to the Web server: One trip to retrieve customer data and a second trip to retrieve the related sales order objects. There are a lot of good things to be said about this design, including segregating the business functionality into two well-defined services. I've already created a CustomerService that handles customer processing, so now I can create a SalesOrderService to handle sales order processing.

The only problem with this solution is that I'd be making two separate trips to the server and two separate trips to my database to retrieve the data. I'd rather not do that. Instead, I'd like to retrieve a customer's orders when I retrieve the Customer object, so I'll only make one trip to the server and one trip to the database. This will almost certainly give the user a faster response and it will certainly reduce demand on both my Web server and the database server, improving my application's scalability. If there were a number of scenarios where I retrieved customer data without sales order data, I might reconsider this solution and value having well-defined services over a faster, more scalable set of services.

On the Server
Because of this decision, the method on my service that returns data from the Customer table is now also going to be returning data from the SalesOrderHeader table, with all the data retrieved from using Entity Framework (EF). Rather than send every column from these tables to the client, I transfer the properties I want from the EF entity objects to Data Transfer Objects (DTOs) and send those DTOs to the client. Using DTOs also simplifies converting the data to a JSON format (which is what my client-side Backbone code is expecting). The Customer entity in my EF model has a link to my SalesOrderHeaders entity, which has a separate link back to the Customer entity. The Microsoft .NET Framework default JSON serializer sees these two relationships as circular and won't, by default, generate JSON from them: Creating a DTO without that circular relationship finesses that problem.

I begin by defining my DTOs. My CustomerDTO looks like this and has a property called Orders that holds a collection of SalesOrderHeaderDTOs:

public class CustomerLong: CustomerDTO
{
  public CustomerLong()
  {
    Orders = new List<SalesOrderHeaderDTO>();
  }

  public string FirstName { get; set; }
  public string LastName { get; set; }
  public string CompanyName { get; set; }
  public List<SalesOrderHeaderDTO> Orders { get; set; }  
}

My SalesOrderHeaderDTO is simpler and holds just the information this part of my application needs:

public class SalesOrderHeaderDTO
{
  public int SalesOrderID { get; set; }
  public DateTime OrderDate { get; set; }
  public int Status { get; set; }
}

Now, on my server, I update my Web API service to retrieve Customer and the corresponding SalesOrderHeaders data using LINQ against an EF model. I use an Include method in my LINQ query to force eager loading of all of the Customer's SalesOrderHeader objects (see the "Defining Templates" section of my previous column for more details on my server-side code):

public CustomerDTO Get(string type, int id)
{
  var cust = (from c in ade.Customers.Include("SalesOrderHeaders")
    where c.CustomerID == id
    orderby c.CustomerID
    select c).SingleOrDefault();

Once I've retrieved the entity classes, I populate my DTOs and send the result to the client, as shown in Listing 1.

Listing 1: Populating the Data Transfer Objects and Sending the Result to the Client
CustomerLong cl = new CustomerLong {
                                	Id = cust.CustomerID,
                                        FirstName = cust.FirstName,
       		      			LastName = cust.LastName,
       	       		               CompanyName = cust.CompanyName
                            	      };

SalesOrderHeaderDTO sohDTO;
foreach (SalesOrderHeader soh in cust.SalesOrderHeaders)
{
  sohDTO = new SalesOrderHeaderDTO();
  sohDTO.SalesOrderID = soh.SalesOrderID;
  sohDTO.OrderDate = soh.OrderDate;
  sohDTO.Status = soh.Status;
  cl.Orders.Add(sohDTO);
}

On the Client
In my client-side TypeScript code, I begin by declaring an interface that describes the DTO with order data that I'll be retrieving:

export interface ISalesOrderHeader
{
  SalesOrderID: number;
  OrderDate: Date;
  Status: number;
}

The first place I use my interface is to define a SalesOrderHeader TypeScript class that extends the BackBone Model class (I'll also use the interface to define any variables or parameters that work with my SalesOrderHeader). Within the SalesOrderHeader, I declare properties that will read and update the data held by the underlying BackBone Model that actually holds the data in a Backbone application:

export class SalesOrderHeader extends bb.Model implements ISalesOrderHeader
{
  get SalesOrderID(): number
  {
    return this.get( 'SalesOrderID' );
  }
  set SalesOrderID( value: number )
  {
    this.set( 'SalesOrderID', value );
  } 
    
  ...more properties...

}

Backbone keeps these classes ("Models" in Backbone-speech) in Collections. I need to declare a collection to hold my SalesOrderHeaders (I declared my Customer's collection in an earlier column):

export class SalesOrderHeaderList extends bb.Collection<SalesOrderHeader>
{
}

I defined the interface for my CustomerDTO object in earlier columns, but now I need to extend it to include the SalesOrderHeaders. I do that using the TypeScript Array class typed to hold my SalesOrderHeader interface:

export interface ICustomerLong extends ICustomerDTO
{
  FirstName: string;
  LastName: string;
  CompanyName: string;
  Orders: Array<ISalesOrderHeader>; 
}

A side note: I suspect I could also have declared my Orders property as an array of SalesOrderHeaders and it wouldn't have made any difference to the rest of my code:

Orders: ISalesOrderHeader[];

Similarly, I extend my Customer class (which implements that interface) with the getter and setter for my new collection, as shown in Listing 2.

Listing 2: Extending the Customer Class with the Getter and Setter of the New Collection
export class CustomerLong extends bb.Model implements ICustomerLong
{
  constructor()
  {
    this.url = "CustomerService/long/"
    super();
  }

  get Id(): number
  {
    return this.get('id');
  }
  set Id( value: number )
  {
    this.set( 'id', value );
  }

  // ...Other properties defined in earlier columns

  set Orders( value: Array<ISalesOrderHeader> )
  {
    this.set( 'Orders', value )
  }
  get Orders(): Array<ISalesOrderHeader>
  {
    return this.get( 'Orders')
  }
}

Setting up the HTML
In the Web page that holds my application, I have an HTML template that displays the customer information. I add a table to that template to display the customer's list of sales orders. I also give the body of the table an id attribute (set to OrderBody) so I can find the table's body element when it's time to insert my sales order data (see Listing 3).

Listing 3: Adding a Table and Setting OrderBody in Customer Template
<script type="text/template" id="customerdetailtemplate">
  <!-- existing tags displaying customer data -->
  <br />
  <table>
    <thead>
      <tr>
        <th>Order Id</th><th>Status</th><th>Order Date</th>
      </tr>
    </thead>
    <tbody id="OrderBody">
    </tbody>            
  </table>
</script>

My next step is to create a template defining the HTML that will surround the data from one SalesOrderHeader. I'm using the Backbone default syntax for data binding to an object's properties (actually, this is Underscore syntax because Backbone uses Underscore to manage its data binding). This template displays a SalesOrderHeader object's SalesOrderId, Status and OrderDate properties:

<script type="text/template" id="salesOrdertemplate">
  <tr>
    <td> <%= SalesOrderID %> </td><td> <%= Status %> </td><td> <%= OrderDate %> </td>
  </tr>
</script>

Generating the Page
Now everything is in place to actually retrieve the data and generate the page's HTML. I prefer to have multiple Backbone Views, each dedicated to displaying a specific Model (I keep hoping this will let me build new pages just by combining existing Views). That means my next step is to create a new View for displaying my SalesOrderHeader objects. Right now, I only need the View to display SalesOrderHeaders, but, because I'm assuming this View will also, someday, display SalesOrderDetails, I name the View SalesOrderView.

In Backbone, a View's render method is responsible for generating the HTML for the Model object stored in the View's model property. This code uses Underscore to handle generating the HTML for the order using the template I showed earlier in this article. Once I've generated the HTML, I make it available through the View's el property by calling the View's setElement property, passing the generated HTML. As is the convention with Backbone Views, I finish the render method by returning a reference to the View:

export class SalesOrderView extends bb.View<cms.SalesOrderHeader>
{  
  render(): bb.View<cms.SalesOrderHeader>
  {
    var templateProcessor: ( ...parms: any[] ) => string;
    templateProcessor = _.template( $( "#salesOrdertemplate" ).html() );
    var html: string;
    html = templateProcessor( this.model.toJSON() );
    this.setElement( html );
    return this;
  }
}

The next step is to incorporate this View into the rest of my page. Thanks to the work described in previous columns, I already have a View that displays the details for a single Customer object. I just need to extend that View to display the sales order data I'm now sending with the Customer data. The code I add to my Customer view first uses the View's elm property to tie the View to the element I want to add my orders to (in this case, that's the body of the table in my page):

elm = $( "#OrderBody" );

Next, I extract the Orders property for the Customer being displayed in the View. The current object for a View is held in the View's model property so I get my Array of SalesOrderHeaders from the model's Orders property:

var Orders: Array<cms.SalesOrderHeader>;
Orders = this.model.get( "Orders" );

If I had used Backbone to retrieve the SalesOrderHeaderDTO objects through a separate call to my Web Service, Backbone would have automatically loaded the SalesOrderHeaderList with those objects. Because I retrieved my SalesOrderHeaders through the Customer object, I need to create the list myself and use its reset method to load the collection with my Array of SalesOrderHeaders:

var ol: cms.SalesOrderHeaderList;
ol = new cms.SalesOrderHeaderList();
ol.reset( Orders );

Now, I loop through the orders in my collection, creating a SalesOrderView for each order. In the loop, I first create the View and then set its model property to the current order that the View is to display. Finally, I call the View's render method, which returns the HTML to display the SalesOrderHeader. I pass that HTML to the append method of the View's elm property to add the HTML to the spot on the page that the elm property is bound to:

var ov: SalesOrderView;
  ol.each( ord =>
  {
    ov = new SalesOrderView();
    ov.model = ord;
    elm.append( ov.render().el );
  }, null );
}

Where We Are Now
Over the last few columns, I've built a relatively sophisticated page: The user is initially given a dropdown list where they can select a customer. Once a customer is selected, the page makes a single trip to the server (and the database) to retrieve the information for the customer along with all of the customer's sales orders (and, of course, displays all of that information).

Next month, I'll finish this project by giving the user the ability to delete, add and change sales orders (in previous columns I showed how to delete and add customers). I'll also show how to extend Backbone to simplify Backbone's data binding.

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/.

comments powered by Disqus

Featured

  • Compare New GitHub Copilot Free Plan for Visual Studio/VS Code to Paid Plans

    The free plan restricts the number of completions, chat requests and access to AI models, being suitable for occasional users and small projects.

  • Diving Deep into .NET MAUI

    Ever since someone figured out that fiddling bits results in source code, developers have sought one codebase for all types of apps on all platforms, with Microsoft's latest attempt to further that effort being .NET MAUI.

  • Copilot AI Boosts Abound in New VS Code v1.96

    Microsoft improved on its new "Copilot Edit" functionality in the latest release of Visual Studio Code, v1.96, its open-source based code editor that has become the most popular in the world according to many surveys.

  • AdaBoost Regression Using C#

    Dr. James McCaffrey from Microsoft Research presents a complete end-to-end demonstration of the AdaBoost.R2 algorithm for regression problems (where the goal is to predict a single numeric value). The implementation follows the original source research paper closely, so you can use it as a guide for customization for specific scenarios.

  • Versioning and Documenting ASP.NET Core Services

    Building an API with ASP.NET Core is only half the job. If your API is going to live more than one release cycle, you're going to need to version it. If you have other people building clients for it, you're going to need to document it.

Subscribe on YouTube