If you haven't already read the prior article, you should go back and do so. This article is going to add to the project created there. In the last article, you built a new Visual Studio project, added the files from the Angular Quick Start, and added a Product service to retrieve product data from a SQL Server table. This data was then displayed in an HTML table.

In this article, you'll add the appropriate HTML, Angular code, and Web API methods to allow the user to add, edit and, delete product data. To the Web API, you'll add POST, PUT, and DELETE methods, as well as a GET method to retrieve a single product. To the Angular product service, you'll add code to call each of these methods in response to user input.

Angular 4 Update

When I wrote the last article, the current version was Angular 2. Since then, Angular 4 has released. I included both the Angular 2 and 4 versions of this application in the download for the last article but I'll continue in this article using only the Angular 4 version. There aren't too many changes that must be made to upgrade from Angular 2 to Angular 4. Here are the changes I made:

  • Installed TypeScript 2.2
  • Created the project from scratch and downloaded the Angular Quick Start from http://bit.ly/29y5J9i.
  • Followed the instructions for using Angular in Visual Studio located at https://angular.io/guide/visual-studio-2015.
  • All files are now located under the \src folder from the root of the Visual Studio project.
  • Eliminated the moduleId: module.id from all components.

That's all the changes that had to be made. Everything else works just as I explained in the last article.

Add an Add Button to the Product List Page

You need a way to get into an “add” mode in which a user may enter the data necessary to create a new product. You'll create the appropriate HTML (Figure 1) for this page soon, but first, let's create an Add button on the main product listing page to get to the product detail page.

Figure 1: Add new products via a product detail page.
Figure 1: Add new products via a product detail page.

Open the product-list.component.html file, and add the following HTML at the top of the page.

<div class="row">
  <div class="col-xs-12">
    <button class="btn btn-primary" (click)="add()">Add New Product</button>
  </div>
</div>

When you click on the Add New Product button, you want to route to the product detail page shown in Figure 1. In the click event for this button, a method in your Angular controller named Add is called. The Add function is going to use the Angular routing engine to redirect to the product detail page.

Update the Product List Component

Let's update the ProductListComponent class to perform this Add functionality. Open the product-list.component.ts file and add an import to allow you to use the routing service.

import { Router } from '@angular/router';

Locate the constructor in your ProductListComponent and add a second parameter to this constructor. This second parameter tells Angular to inject the Router service into the ProductListComponent.

constructor(
  private productService: ProductService,
  private router: Router) {
}

Create a new function named Add in your ProductListComponent class. This function calls the Navigate method of the injected router service. You pass an array to this Navigate function. The first array element is the route, which must match a route you create in your routing component. The second parameter is the value you wish to pass to the next component. Later in this article, you use this product detail page to display an existing product, so you will pass a real product ID as the second element. For now, because you're just adding a new product, pass a minus one (-1).

add() {
  this.router.navigate(['/productDetail', -1]);
}

Create Detail HTML

Create the product detail page shown in Figure 1. This detail page is used to both add and edit product data. Right mouse-click on the \src\app\product folder and select the Add > HTML page menu. Set the name to product-detail.component.html and click the OK button. The various input fields, shown in Figure 1, are placed into a bootstrap panel. Create the panel by first deleting all of the HTML in the new page you just added, and then typing in the following HTML.

<div class="panel panel-primary" *ngIf="product">
  <div class="panel-heading">
    <h1 class="panel-title">Product Information</h1>
  </div>
  <div class="panel-body">
  </div>
  <div class="panel-footer">
  </div>
</div>

The HTML above creates the panel control. Use the *ngIf directive to only display this panel once there's a valid product object. Now you just need to add a few more pieces of HTML within the body and the footer of the panel. Within the footer, add the Save and Cancel buttons. You haven't created the controller class to respond to the click events yet, but go ahead and add the appropriate method calls anyway.

<button class="btn btn-success" (click)="saveProduct()">Save</button>
<button class="btn btn-primary" (click)="goBack()">Cancel</button>

It's possible that the user won't enter the correct data. Therefore, you need to display error messages to the user. Add an area just below the <div class="panel-body"> to display error messages. No validation is going to be created in this article - you'll learn how to do that in the next article.

<div class="row" *ngIf="messages && messages.length">
  <div class="col-xs-12">
    <div class="alert alert-warning">
      <ul>
        <li *ngFor="let msg of messages">{{msg}}</li>
      </ul>
    </div>
  </div>
</div>

Within the <div> above is an unordered list that's used to display error messages. This whole row is only displayed if the Messages property is not null and the length of the Messages array is greater than zero. If there are messages in this Message array, they are each displayed as a single item in the list.

It's finally time to create the product input fields. Just below the error message area, add the various input fields, as shown in Listing 1. Each input field is bound to a product property in the controller for this page. Use the ngModel directive to bind each property of the product object to each input field.

Listing 1: Create the input fields and bind them to properties using the ngModel directive.

<div class="form-group">
  <label for="productName">Product Name</label>
  <input id="productName" 
         type="text" 
         class="form-control" 
         autofocus="autofocus" 
         placeholder="Enter the Product Name" 
         title="Enter the Product Name" [(ngModel)]="product.productName" />
</div>
<div class="form-group">
  <label for="introductionDate">Introduction Date</label>
  <input id="introductionDate" 
         type="text" 
         class="form-control" 
         placeholder="Enter the Introduction Date" 
         title="Enter the Introduction Date" [(ngModel)]="product.introductionDate" />
</div>
<div class="form-group">
  <label for="price">Price</label>
  <input id="price" 
         type="number" 
         class="form-control" 
         placeholder="Enter the Price" 
         title="Enter the Price" [(ngModel)]="product.price" />
</div>
<div class="form-group">
  <label for="url">URL</label>
  <input id="url" 
         type="url" 
         class="form-control" 
         placeholder="Enter the URL" 
         title="Enter the URL" [(ngModel)]="product.url" />
</div>

Create Product Detail Component

Now that you have the product detail page created, you need a component to go along with it. Right mouse-click on the \src\app\product folder and select Add > TypeScript file. Set the name to product-detail.component.ts. Add the code shown in Listing 2. Remember from the HTML you created that you need a product object and an array of messages. Because you wish to have a valid product object when the HTML is rendered, you implement the OnInit interface. In the ngOnInit method, you create a new instance of a Product class and fill in a couple of the properties with default values. You are going to add more code to this class later in this article.

Listing 2: The start of the product detail component.

import { Component, OnInit } from "@angular/core";
import { Product } from "./product";

@Component({ templateUrl: "./product-detail.component.html" })

export class ProductDetailComponent implements OnInit {
  product: Product;
  messages: string[] = [];
    
  ngOnInit() {
    this.product = new Product();
    this.product.price = 1;
    this.product.url = "www.pdsa.com";
  }
}

Update Routing

Before you can navigate to the new product detail page, you need to inform the Angular routing service about this new detail component. Open the app-routing.module.ts file and add this new import statement at the top of this file:

import { ProductDetailComponent } from "./product/product-detail.component";

Add a new route object after the other routes you previously created. This new route object references the ProductDetailComponent. The path property is a little different because you want to pass a parameter named id to the ProductDetailComponent class. When you wish to add a new product, you aren't going to do anything with the value you're passing in, so just pass in a minus one. However, for editing, you'll pass in a valid product ID in order to retrieve the product record to edit.

const routes: Routes = [
  {
    path: 'productList',
    component: ProductListComponent
  },
  {
    path: 'productDetail/:id',
    component: ProductDetailComponent
  }
];

Update AppModule

In each input field in the product detail page, you reference the ngModel directive. However, you haven't told your Angular application that you need to use this directive. To register this directive, open the app.module.ts file and add an import statement for the FormsModule package. This package includes the ngModel directive.

import { FormsModule } from '@angular/forms';

While you are in this file, also add an import for your new ProductDetailComponent class you added.

import { ProductDetailComponent } from "./product/product-detail.component";

Add the FormsModule to the imports property on your NgModule decorator. Add the ProductDetailComponent to the declarations property on your NgModule decorator. Your NgModule decorator should now look like the following code.

@NgModule({
  imports: [BrowserModule, AppRoutingModule, HttpModule, FormsModule],
  declarations: [AppComponent, ProductListComponent, ProductDetailComponent], 
  bootstrap: [AppComponent], 
  providers: [ProductService]
})

Run the application, click on the Add New Product button, and the detail page appears. Nothing else works at this point, but verify that you can get to the detail page.

Handling Validation Exceptions on the Server

When you attempt to add or update a product, business rules can fail because the user didn't fill out the fields correctly. For instance, the product name is a required field, but if the user doesn't fill in a product name, the ProductName property gets passed to the server as a blank string. If you attempt to add this product to the database table, the code generated by the Entity Framework will raise a DbEntityValidationException exception.

If this type of exception is thrown, take the validation errors and bundle them into a ModelStateDictionary object. This dictionary object is passed back to the client by returning the BadRequest method with the dictionary object as the payload. To build the ModelStateDictionary object, you must iterate over the collection of validation errors contained in the DbEntityValidationException object.

Open the ProductController.cs file and add a using statement at the top of the file. One note on this using statement: The ModelStateDictionary used by the Web API is different from the one used by MVC controllers. Make sure you're using the ModelStateDictionary class from this namespace and not the one used by MVC.

using System.Web.Http.ModelBinding;

Next, add the method shown in Listing 3 to your ProductController class. This method is called from both the POST and PUT methods if any validation errors occur when adding or updating the product data.

Listing 3: Add a method to convert validation errors into a Model State Dictionary.

protected ModelStateDictionary ValidationErrorsToMessages(DbEntityValidationException ex) 
{
    ModelStateDictionary ret = new ModelStateDictionary();
    
    foreach (DbEntityValidationResult result in ex.EntityValidationErrors) 
    {
        foreach (DbValidationError item in result.ValidationErrors) 
        {
            ret.AddModelError(item.PropertyName, item.ErrorMessage);
        }
    }
    
    return ret;
}

Add a POST Method in the Controller

An HTTP POST verb is used to inform the server that the data sent from the client is to be added to your underlying data store. In the POST method, you're going to write in your ProductController class attempts to add the product data. However, either a validation exception or a database exception can be thrown. Therefore, it's important to wrap calls to the Entity Framework within a C# try/catch structure. Before writing the POST method, add a using statement so you can use the Product class from the Entity Framework.

using ProductApp.Models;

Write the POST method, shown in Listing 4, to accept a Product object from your Angular client code. The POST method creates a new instance of the Entity Framework's DbContext object, called ProductDB. The new product object is added to the Products collection, then the SaveChanges method is invoked. If the product is successfully added to the SQL Server table, return a status code of 201 by calling the Created method. If a validation exception occurs, then a status code of 400 is returned by calling BadRequest and passing in the ModelStateDictionary object created with the call to the ValidationErrorsToMessages method. If any other exception occurs, a status of 500 is returned to the client.

Listing 4: The Post method adds a new product or returns a set of validation errors.

[HttpPost]
public IHttpActionResult Post(Product product) 
{
    IHttpActionResult ret = null;
    ProductDB db = null;
    
    try 
    {
        db = new ProductDB();
        
        // Insert the new entity
        db.Products.Add(product);
        db.SaveChanges();
        
        ret = Created<Product>(Request.RequestUri + product.ProductId.ToString(), product);
    }
    catch (DbEntityValidationException ex) 
    {
        ret = BadRequest(ValidationErrorsToMessages(ex));
    }
    catch (Exception ex) 
    {
        ret = InternalServerError(ex);
    }
    
    return ret;
}

Modify the Product Service to Add a Product

Now that you have a POST Web API method to which you can send new product data, write the code in the ProductService class to call this POST method. Open the product.service.ts file and import two new classes; Headers and RequestOptions from the @angular/http library that you already have at the top of this file. These two new classes are needed to call the Web API method.

import { Http, Response, Headers, RequestOptions } from '@angular/http';

Add an addProduct method to this class to pass a new product object from the client to the POST method of the ProductController class. When you post data, as opposed to getting data, set the content type as JSON data. You do this by creating a new Headers object and setting the Content-Type property to application/json. Create a RequestOptions object and set the headers property to this new Headers object you created. Next, call the POST method on the HTTP service passing in the product object and the RequestOptions object.

addProduct(product: Product):
  Observable<Product> {
    let headers = new Headers({'Content-Type': 'application/json'});
    let options = new RequestOptions({headers: headers});
    return this.http.post(this.url, product, options)
      .map(this.extractData)
      .catch(this.handleError);
}

Check for Validation Errors

One of three things can happen when you call the POST method: one, the data will be successfully added to the back-end database table; two, a set of validation errors is returned via a 400 error; or three, you may get a general exception, in which case, a 500 error is sent back. When you wrote the handleError method, you handled a 404 and a 500 error, but you didn't account for a 400. Add a new case statement to handle a 400 in the handleError method.

case 400:  // Model State Error
  let valErrors = error.json().modelState;
  for (var key in valErrors) {
    for (var i = 0; i < valErrors[key].length; i++) {
      errors.push(valErrors[key][i]);
    }
  }
  break;

In this new case statement, retrieve the modelState property and loop through all the key values and retrieve the message from the properties returned. Each of these messages is pushed onto the errors array, which is then sent back to the caller via the Observable.throw method.

Modify Product Detail Component

It's now time to call the addProduct method in the product service class by writing code in the ProductDetailComponent class. Add three new import statements at the top of the product-detail.component.ts file.

import { ActivatedRoute, Params } from '@angular/router';
import { Location } from '@angular/common';
import { ProductService } from "./product.service";

The ActivatedRoute and Params services are needed to work with the ID parameter you passed in to this component. The parameter is not used for the add method, but will be used shortly for updating a product. The Location service is used to navigate back from the detail page to the list page.

Add a constructor to the ProductDetailComponent class. This constructor is injected with the ProductService class you wrote. The ActivatedRoute and the Location services are injected by Angular as well.

constructor(
  private productService: ProductService,
  private route: ActivatedRoute,
  private location: Location
) { }

Add a method to allow the user to go back to the previous page if they click on the cancel button. This method is also going to be called if a product is successfully added.

goBack() {
  this.location.back();
}

Add a method to your class named handleErrors. This method is called if the call to the addProduct in the Product Service fails. In this method, you loop through the string array of errors and add them to the messages property.

private handleErrors(errors: any) {
  this.messages = [];
  for (let msg of errors) {
    this.messages.push(msg);
  }
}

There are three methods you're eventually going to need in this component: saveProduct, addProduct, and updateProduct. You're going to write the updateProduct method soon in this article, but for now, go ahead and add a stub for the function.

private updateProduct(product: Product) {
}

The addProduct method is responsible for calling the addProduct method you just created in the ProductService class. As you can see, if this call is successful, the goBack method is called in order to return to the product list page so you can see that the new product has been added.

private addProduct(product: Product) {
  this.productService.addProduct(product).subscribe(() => this.goBack(),
  errors => this.handleErrors(errors));
}

The saveProduct method is called from the HTML button you added earlier on the product detail page. This method checks to see if the productId property of the product object is null or not. If this value is not null, then the updateProduct method is called. If this value is null, then call the addProduct method.

saveProduct() {
  if (this.product) {
    if (this.product.productId) {
      this.updateProduct(this.product);
    }
    else {
      this.addProduct(this.product);
    }
  }
}

See the Validation Errors

Run the application and click on the Add New Product button. Immediately click on the Save button and you should see a set of validation errors appear on the screen, as shown in Figure 2. NOTE: Since I wrote the last article, I decided it would be better to have all fields on the Product table defined as NOT NULL instead of just the ProductName field. Please make the appropriate adjustments on your Product table.

Figure 2: Validation errors show up at the top of your detail page.
Figure 2: Validation errors show up at the top of your detail page.

Add a New Product

Now, go ahead and add some good data for the product. Click the Save button and you should be redirected back to the list page where you'll see the new product you just added within the list.

Get a Single Product

Now that the add functionality is working, you'll add the ability for the user to update a product. To update a product, you must first retrieve all of the product data from the server. Add an Edit button on each row of the product HTML table, as shown in Figure 3. When the user clicks on this Edit button, call the Web API on the server to retrieve the full product record for editing. This ensures that you're getting the latest product data.

Figure 3: Edit and Delete buttons are added to each row of product data
Figure 3: Edit and Delete buttons are added to each row of product data

Add GET to Controller

To retrieve a single product, add a GET method to your ProductController class. This GET method is different from the other one in this class in that it accepts a product ID of the product you wish to retrieve. Open your ProductController.cs file and add the new GET method shown in Listing 5.

Listing 5: Use the Find method to locate a specific product based on the primary key passed into the Get method.

[HttpGet]
public IHttpActionResult Get(int id) 
{
    IHttpActionResult ret;
    ProductDB db = new ProductDB();
    Product product = new Product();
    
    product = db.Products.Find(id);
    if (product != null) 
    {
        ret = Ok(product);
    }
    else 
    {
        ret = NotFound();
    }

    return ret;
}

Add GET to Angular Product Service

Now that you have the Web API method created, write a getProduct method in the ProductService component. Open the product.service.ts file and add the following method.

getProduct(id: number): Observable<Product> {
  let url = this.url + "/" + id;
  return this.http.get(url)
    .map(response => response.json() as Product)
    .catch(this.handleError);
}

This method builds a URL that looks like the following: api/productApi/2. The number 2 on the end is what gets passed to the ID parameter in the GET method in your ProductController.

Add a Select Button to HTML Table

As you saw in Figure 3, you need an Edit column on your HTML table. Open the product-list.component.html file and insert a new <td> element within the <thead> element.

<td>Edit</td>

Move down to the <tbody> element and insert a new <td> element in the same position.

<td>
  <button class="btn btn-default btn-sm" (click)="selectProduct(product.productId)">
    <i class="glyphicon glyphicon-edit"></i>
  </button>
</td>

Modify the Product List Component

The click event on this button calls a method named selectProduct. Code in the product ID to the call of this method to pass the ID's value to the detail page so it can load the product data associated with that ID. Add the selectProduct function to the ProductListComponent class by writing the code below. This function calls the navigate function and passes the ID to the product detail page.

selectProduct(id: number) {
  this.router.navigate(['/productDetail', id]);
}

Retrieve a Passed Parameter

Modify the ngOnInit method in the ProductDetailComponent to handle an ID parameter being passed to this controller. When you created the add functionality earlier, you didn't do anything with the parameter that was passed. Now, because you're passing an ID to be edited, use that ID to call the getProduct method in the product service to retrieve the product. Open the product-detail.component.ts file and modify the ngOnInit function to look like Listing 6.

Listing 6: Modify the ngOnInit method to retrieve a specific product from the server.

ngOnInit() {
  this.route.params.forEach((params: Params) => {
    if (params['id'] !== undefined) {
      if (params['id'] != "-1") {
        this.productService.getProduct(params['id']).subscribe(
          product => this.product = product, 
          errors => this.handleErrors(errors));
      }
      else {
        this.product = new Product();
        this.product.price = 1;
        this.product.url = "www.pdsa.com";
      }
    }
  });
}

The ngOnInit method loops through the route.params array and retrieves a Params object. Check to see if the id parameter is defined on that Params object. If the id value exists, check that value to see if it's equal to a -1. If so, then you're adding a product. If the value is anything else, then it's a valid product ID. Call the getProduct method on the product service to retrieve a single product object. Run the application and click on the Edit button. If you did everything correctly, you should see product data in all the input fields.

Update a Product

Now that you have the current product data displayed in the input fields, the user may update them. To update a product, you must write a few more pieces of code. First, create a PUT method in the Web API controller. Second, add an updateProduct method in the ProductService class. Third, modify the updateProduct method in the ProductDetailComponent to call the product service.

Add a PUT Method in Controller

Open the ProductController.cs file and add the code shown in Listing 7. This method is very similar to the POST method in that it uses the Entity Framework to modify the Product table. If the product data is able to be updated successfully, a status code of 200 is returned by calling the OK method. If an exception occurs, then either a 400 or a 500 status code is returned based on what type of exception is thrown when attempting to update the table.

Listing 7: The Put method allows you to update a product.

[HttpPut()]
public IHttpActionResult Put(int id, Product product) 
{
    IHttpActionResult ret = null;
    ProductDB db = null;
    
    try 
    {
        db = new ProductDB();
        
        // Update the entity
        db.Entry(product).State = EntityState.Modified;
        db.SaveChanges();
        
        ret = Ok(product);
    }
    catch (DbEntityValidationException ex) 
    {
        ret = BadRequest(ValidationErrorsToMessages(ex));
    }
    catch (Exception ex) 
    {
        ret = InternalServerError(ex);
    }
    
    return ret;
}

Add an Update Method to Product Service

Add a method named updateProduct to your Angular product service class. This method is similar to the addProduct method in that you must set the headers to pass to the PUT method. The URL passed to the http.put method is different from the POST method, as you add the productId value to the URL.

updateProduct(product: Product): Observable<Product> {
  let headers = new Headers({'Content-Type': 'application/json'});
  let options = new RequestOptions({headers: headers});
  return this.http.put(this.url + "/" + product.productId, product, options)
    .map(this.extractData)
    .catch(this.handleError);
}

Modify the Update Method in the Product Detail Component

Open the product-detail.component.ts file and modify the updateProduct method to call the updateProduct method in the product service. As you did in the addProduct method, if the call is successful, call the goBack() method to return to the HTML product list to show that the updates have taken place.

updateProduct(product: Product) {
  this.productService.updateProduct(product).subscribe(() => this.goBack(),
  errors => this.handleErrors(errors));
}

Delete a Product

The last piece of functionality to add to your product page is the ability to delete a product. To add this functionality, you need to perform a few steps. First, add a DELETE method to the Web API controller. Second, add a deleteProduct method to the Angular product service. Third, add a Delete button to each row of the product HTML table. Lastly, add a deleteProduct method in the product list component to call the deleteProduct method in the product service class.

Add a DELETE Method to the Web API Controller

Add a DELETE method to your ProductController to which you pass in the product ID to delete. Open the ProductController.cs file and add the DELETE method shown in Listing 8.

Listing 8: The Delete method first locates the product to delete, then removes it from the database.

[HttpDelete()]
public IHttpActionResult Delete(int id)
{
    IHttpActionResult ret = null;
    ProductDB db = null;
    
    try 
    {
        db = new ProductDB();
        
        // Get the product
        Product product = db.Products.Find(id);
        
        // Delete the product
        db.Products.Remove(product);
        db.SaveChanges();
        
        ret = Ok(product);
    }
    catch (Exception ex) 
    {
        ret = InternalServerError(ex);
        
    }
    
    return ret;
}

Add a DELETE Method to the Product Service

Add a method to your Angular product service to call the DELETE method in the Web API. Open the product.service.ts file and add the deleteProduct method shown below. The http.delete method expects you to pass a URL with a single ID parameter that's the primary key of the record you wish to delete.

deleteProduct(id: number): Observable<Product> {
  return this.http.delete(this.url + "/" + id)
    .map(() => null)
    .catch(this.handleError);
}

Add a Delete Button to the HTML Table

As you saw in Figure 3, you need to add a Delete column to your HTML table. Open the product-list.component.html file and insert a new <td> element within the <thead> element. Make this the last element in the <thead> element.

<td>Delete</td>

Add a <td> as the very last element in the <tbody> tag as well. Add a button with a click event that calls a method in your ProductListComponent class. Pass the current product ID in the table to this method.

<td>
  <button class="btn btn-default btn-sm" (click)="deleteProduct(product.productId)">
    <i class="glyphicon glyphicon-trash"></i>
  </button>
</td>

Add a DELETE Method in the List Component

Now that you have a button to call a deleteProduct method, go ahead and add that method. Open the product-list.component.ts file and add the code shown below.

deleteProduct(id: number) {
  if (confirm("Delete this product?")) {
    this.productService.deleteProduct(id).subscribe(() => this.getProducts(),
    errors => this.handleErrors(errors));
  }
}

This method first confirms with the user that they really wish to delete this product. If they respond affirmatively, the deleteProduct method on the Angular product service is called. If the deletion is successful, the getProducts method is called to refresh the collection of products from the server and redisplay the list of products.

Summary

In this article, you added a detail page to add new, or modify existing, product data. A new route was added to navigate to this detail page. A new component was created to handle the processing of new and existing product data. You also created POST, PUT, and DELETE methods in your Web API controller. The appropriate code to handle all this modification of product data was added to the Angular product service and component classes. You also saw how to handle validation errors returned from the server. In the next article, you'll learn to validate product data on the client-side using Angular.