[.NETWorld] Implementing CRUD Activities in a TypeScript ViewModel


In this column I’m going to show how to integrate create, read, update and delete (CRUD) functionality into your client-side application using TypeScript. But first, I need to address how to integrate your entity objects with the requirements that might be imposed by a JavaScript library.

Integrating Entities by Extending Interfaces
It isn’t unusual for a JavaScript library to add new functionality to objects or classes you create. For example, I use the IdeaBlade’s free Breeze library to manage the client-side objects retrieved from methods on my server-side services. To support managing client-side objects, Breeze adds an entityAspect property to my objects as it retrieves objects from the server. That entityAspect property contains state information about the object. To notify Breeze that an object’s to be deleted, I call the setDeleted method on the object’s entityAspect property.

In TypeScript, when working with server-side objects after they’re downloaded to the browser as JSON objects, I don’t have access to those objects’ C# or Visual Basic definitions. To provide a definition of those objects that my client-side code can use, I create a TypeScript interface that duplicates my Visual Basic or C# definition. With Breeze, however, I need that TypeScript interface to also include the methods and properties that Breeze adds to my objects as part of downloading them. Unlike the server-side objects I created, however, I don’t know what the Breeze properties and methods look like. Fortunately, TypeScript provides extended interfaces to handle this issue.

To have my client-side interface include the methods and properties that Breeze adds to my client-side objects, I define the interfaces for my objects as extensions to interfaces exposed by Breeze. The Breeze interfaces I need, for example, are defined in a class called breeze.Entity. This means the interface that describes my Customer object, with its three properties and including the Breeze extensions, looks like this:

module PHVEntities 
{
  export interface ICustomer extends breeze.Entity 
  {
    Id: number;
    FirstName: string;
    LastName: string;
  }

Now, my Customer interface includes the Breeze entityAspect property defined in breeze.Entity. I can mark a Customer as deleted with code like this:

cust: PHVEntities.ICustomer;
...download customer object...
cust.entityAspect.setDeleted();

Implementing CRUD
With that interface in place, I can use Breeze to retrieve Customer objects from my server-side services. Listing 1 shows the start of my ViewModel (written with Knockout) with the code that does the retrieval and puts the objects in an array called customers defined in the ViewModel constructor.

Listing 1: A Knockout ViewModel with Breeze

import b = breeze;
import ent = PHVEntities;  

export class SalesOrderVM
{
  constructor(
    private em: b.EntityManager = new
      b.EntityManager("...url for services..."),
    public customers: KnockoutObservableArray<ent.ICustomer> = 
      ko.observableArray<ent.ICustomer>()) { }

  fetchAllCustomers()
  {
    b.EntityQuery
     .from("Customers")
     .using(this.em)
     .execute()
     .then(dt => {
                    this.customers(<ent.ICustomer[]> dt.results);
                 })
     .fail(err => { "Problem:" + alert(err.message); });
   }

I have a button on the page that runs this method through a Knockout binding:

<input type="button" value="Get Customers" data-bind="event: { click: fetchAllCustomers }" />

After retrieving a collection of Customer objects, I display those Customers in a table. The HTML that binds to my customers collection generates a table that includes a button that binds to another method in my ViewModel:

<table>
  <tbody data-bind='foreach: customers'>
    <tr>
      <td><input type="text"   data-bind='value: Id' /></td>
      <td><input type="text"   data-bind='value: FirstName' /></td>
      <td><input type="text"   data-bind='value: LastName' /></td>
      <td><input type="button" data-bind='click: $root.removeCustomer' 
                 value="Delete" /></td>
    </tr>
  </tbody>
</table>

Each row in the table represents a single customer object. If I were to just bind my button to “removeMethod” then Knockout would look for the method on the customer object. Using $root tells Knockout that I want it to look for the method on the ultimate parent: my Knockout ViewModel.

Deletes, Inserts and Updates
In the removeCustomer method tied to the button in each row, I must do two things. First, I need to mark the Customer in the row as deleted to cause Breeze to trigger deletes back at the server. Second, I need to remove the Customer from my client-side customers collection so that it will no longer appear in the table.

Knockout will automatically pass the Customer in the row to a method called from that row, so the removeCustomer method looks like this:

removeCustomer(cust: ent.ICustomer)
{
  cust.entityAspect.setDeleted();
  this.customers.remove(cust);
}

Following my table, I have another button that allows the user to add a new row. That button also binds to a method on my ViewModel but, because the button is outside the context of the repeating rows in the table, I don’t need to qualify the method name:

<input type="button" data-bind='click: insertCustomer' value='Add Customer' />

My insertCustomer method also does two things. First, it uses Breeze to create a new customer entity and places it in a variable I’ve called newCust. The method then adds that newCust object to my customers collection:

insertCustomer()
{
  var newCust: ent.ICustomer;
  newCust = <ent.ICustomer> this.em.createEntity("Customer");
  this.customers.push(newCust);
}

Adding newCust to my Knockout-enabled collection causes Knockout to refresh the table and display the new blank row for the user to enter data.

I don’t need to do anything to support updates: The user can update the textboxes in the table that displays the customer’s properties. Thanks to Knockout, those changes are saved back to my collection of Breeze-enabled customers. All that’s necessary to have those changes (including inserts and deletes) sent back to my service is to call the Breeze SaveChanges method. I put that code in a method in my ViewModel called saveChanges:

saveChanges()
{
  this.em.saveChanges()
         .then(sr => {alert("Saved " + sr.entities.length + " Customer(s)")})
         .fail(err => {alert(err.message)});      
}

Therefore, it’s just a matter of binding this method to a button on my page to let the user save all of their deletes, inserts and updates:

<input type="button" data-bind='click: saveChanges' value='Save Changes' />

All that’s missing on the client is the code to run at startup to cause Knockout to bind my HTML to my ViewModel with its methods and collections. That code looks like this:

$(function () {
  ko.applyBindings(new PhvVM.SalesOrderVM());
});

A Breeze-Enabled Web API Service

See my earlier column for more details on creating a Breeze-enabled Web API service:

[BreezeController]
public class CustomerManagementController : ApiController
{
  EFContextProvider<SalesOrderContext> cm;

  public CustomerManagementController()
  {
    cm = new EFContextProvider<SalesOrderContext>();
  }

  [HttpGet]
  public string Metadata()
  {
    return cm.Metadata();
  }

  [HttpGet]
  public IQueryable<Customer> Customers()
  {
    return from cust in cm.Context.Customers
           orderby cust.LastName, cust.FirstName
           select cust;
  }

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

When you add Breeze to your project, it will add a BreezeWebAPIConfig class to your project that holds the Breeze routing rules for directing requests to your service.

That brings this column up-to-date with the current version of my ongoing TypeScript project. In my next column, I’m going to look at retrieving the child records associated with each Customer.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s