Nov 23

Written by: Michael Washington
11/23/2016 10:18 PM  RssIcon

Note: This tutorial is part of a series of articles that create a complete Angular 2 application using OData 4 and ASP.NET 4:

  1. Hello World! in Angular 2 using Visual Studio 2015 and ASP.NET 4
  2. Implement ASP.NET 4 MVC Application Security in Angular 2 Using OData 4
  3. Tutorial: Creating An Angular 2 CRUD Application Using MVC 5 and OData 4 (this article)
  4. Tutorial: An End-To-End Angular 2 Application Using MVC 5 and OData 4

This article shows how you can create an Angular 2 CRUD (Create Read Update Delete) application using OData 4 and ASP.NET 4 MVC Application security.

image

We will create a Data layer, the Angular 2 code, and display everything on the MVC View.

Creating The Application

image

For this demonstration, we start with the application created in Implement ASP.NET 4 MVC Application Security in Angular 2 Using OData 4.

Create The Data Layer

image

The first thing to do is to create the data layer.

Open the project in Visual Studio.

In the Solution Explorer, click the Show All Files button.

image

Right-click on the database file and select Include in Project.

This file was created automatically in the previous tutorial when the project was created using the Visual Studio template.
That template automatically creates a database when you enable authentication.

image

Click on the Show All Files button again to hide the other files.

Right-Click on the database file and select Open.

Note: If you don’t have this option, Install the latest SQL Server Data Tools (SSDT) from: https://msdn.microsoft.com/library/mt204009.aspx

image

The Server Explorer will open.

Under Data Connections, expand the DefaultConnection, and right-click on the Tables node and select Add New Table.

(the existing tables you see in the database were created by Visual Studio automatically when authentication was enabled)

image

Paste the following script in and click the Update button:

 

CREATE TABLE [dbo].[Products] (
    [Id]           INT            IDENTITY (1, 1) NOT NULL,
    [ProductName]  NVARCHAR (255) NOT NULL,
    [ProductPrice] MONEY          NOT NULL,
    CONSTRAINT [PK_dbo.Products] PRIMARY KEY CLUSTERED ([Id] ASC)
);

image

Click the Update Database button.

image

The table will be created.

image

In the Server Explorer, click the refresh button to see the Products table show.

Create The Entity Framework Data Model

image

Return to the Solution Explorer.

Right-click on the Models folder and select Add then New Item…

image

Add an ADO.NET Entity Data Model called Angular2QuickStartDAL.

image

Select EF Designer from database.

image

Select the DefaultConnection.

image

Select the Products table.

(we don’t select the other tables because we do not need to read or write to them at this time)

image

The .edmx file will be created.

Close the file.

image

Right-click on the Solution node and select Build Solution.

OData Layer

The OData layer sits on top of the Entity Framework layer and is what the Angular 2 code will use to communicate with the database.

First we need to create a Data Transfer Object (DTO). This is a class that is passed between the Angular 2 code and the OData methods that we will create.

After that, we will create an ODataProductsController that will contain the OData methods that will provide the CRUD (Create, Read, Update, and Delete) functionality.

Finally, we will register the ODataProductsController with the OData Entity Data Model so that the methods can be called by the Angular 2 code.

image

In the Models folder, create a file, DTOProduct.cs, using the following code:

 

using System.ComponentModel.DataAnnotations;
namespace Angular2QuickStart.Models
{
    public class DTOProduct
    {
        [Key]
        public int Id { get; set; }
        public string ProductName { get; set; }
        public string ProductPrice { get; set; }
    }
}

 

image

In the Controllers/OData4Controllers folder, create a file, ODataProductsController.cs, using the following code:

 

using System;
using System.Data;
using System.Data.Entity;
using System.Linq;
using System.Net;
using System.Web.Http;
using System.Web.OData;
using Angular2QuickStart.Models;
namespace Angular2QuickStart.Controllers
{
    public class ODataProductsController : ODataController
    {
        // This connects to the database
        private Entities db = new Entities();
        // Note: All methods are decorated with [Authorize]
        // This means they can only be called if the user is logged in
        #region public IQueryable<DTOProduct> GetODataProducts()
        // GET: odata/ODataProducts
        [Authorize]
        [EnableQuery(PageSize = 100)]
        public IQueryable<DTOProduct> GetODataProducts()
        {
            // This returns all products in the database
            var result = (from product in db.Products
                          select new DTOProduct
                          {
                              Id = product.Id,
                              ProductName = product.ProductName,
                              ProductPrice = product.ProductPrice.ToString()
                          });
            return result.AsQueryable();
        }
        #endregion
        #region public IHttpActionResult Post(DTOProduct dtoProduct)
        // POST: odata/ODataProducts
        [Authorize]
        public IHttpActionResult Post(DTOProduct dtoProduct)
        {
            // Create a new Product
            var NewProduct = new Product();
            NewProduct.ProductName = dtoProduct.ProductName;
            NewProduct.ProductPrice = Convert.ToDecimal(dtoProduct.ProductPrice);
            // Save the Product
            db.Products.Add(NewProduct);
            db.SaveChanges();
            // Populate the ID that was created and pass it back
            dtoProduct.Id = NewProduct.Id;
            // Return the Product
            return Created(dtoProduct);
        }
        #endregion
        #region public IHttpActionResult Put([FromODataUri] int key, DTOProduct dtoProduct)
        // PUT: odata/ODataProducts(1)
        [Authorize]
        public IHttpActionResult Put([FromODataUri] int key, DTOProduct dtoProduct)
        {
            // Get the existing Product using the key that was passed
            Product ExistingProduct = db.Products.Find(key);
            // Did we find a Product?
            if (ExistingProduct == null)
            {
                // If not return NotFound
                return StatusCode(HttpStatusCode.NotFound);
            }
            // Update the Product 
            ExistingProduct.ProductName = dtoProduct.ProductName;
            ExistingProduct.ProductPrice = Convert.ToDecimal(dtoProduct.ProductPrice);
            // Save changes
            db.Entry(ExistingProduct).State = EntityState.Modified;
            db.SaveChanges();
            // Return the Updated Product
            // Return that the Product was Updated
            return Updated(ExistingProduct);
        }
        #endregion
        #region public IHttpActionResult Delete([FromODataUri] int key)
        // DELETE: odata/ODataProducts(1)
        [Authorize]
        public IHttpActionResult Delete([FromODataUri] int key)
        {
            // Get the existing Product using the key that was passed
            Product ExistingProduct = db.Products.Find(key);
            // Did we find a Product?
            if (ExistingProduct == null)
            {
                // If not return NotFound
                return StatusCode(HttpStatusCode.NotFound);
            }
            // Delete the Product
            // (and any Product Detail(s))
            db.Products.Remove(ExistingProduct);
            // Save changes
            db.SaveChanges();
            // Return a success code
            return StatusCode(HttpStatusCode.NoContent);
        }
        #endregion
        // Utility
        #region protected override void Dispose(bool disposing)
        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                // Dispose of the database object
                db.Dispose();
            }
            base.Dispose(disposing);
        }
        #endregion
    }
}

 

image

Open the WebApiConfig.cs file in the App_Start folder, and add the following code to the GenerateEntityDataModel method:

 

    // Register ODataProducts
    builder.EntitySet<DTOProduct>("ODataProducts");

 

Add ng2-Bootstrap

image

The user interface will use ng2-Bootstrap to make the buttons and other elements look nice, and to enable the modal popups.

to install it, open the Package.json file and add the following lines under the dependencies section:

 

    "bootstrap": "^3.3.7",
    "ng2-bootstrap": "1.1.14-1"

 

image

Save the file, then right-click on it and select Restore Packages.

image

The packages will install.

image

Open the _Layout.cshtml file in the View/Shared folder and add the following code to the file:

 

    <!-- Bootstrap -->
    <link rel="stylesheet" href="node_modules/bootstrap/dist/css/bootstrap.min.css">

 

image

Select the Show All Files button.

image

Include the Systemjs.config.js file (in the root directory).

Select the Show All Files button again to hide the other files.

image

Open the Systemjs.config.js file.

Add a comma at the end of the last line in the “map” section.

Add the following lines after the comma:

      // Bootstrap
      'moment': 'node_modules/moment/moment.js',
      'ng2-bootstrap/ng2-bootstrap': 'node_modules/ng2-bootstrap/bundles/ng2-bootstrap.umd.js',

 

Save the file and close it.

(Note: You can get the full directions to install ng2-Bootstrap from here: http://valor-software.com/ng2-bootstrap/#/)

Angular 2 Code

image

Create a folder under the app folder called product.

Create a TypeScript file in the folder called product.ts using the following code:

 

/* Defines the product entity */
export interface IProduct {
    Id: number;
    ProductName: string;
    ProductPrice: string;
}

 

This will be mapped to the DTOProduct class that the OData methods will use to pass data to the Angular 2 code.

image

Create a TypeScript file in the folder called product.service.ts using the following code:

 

// Angular Imports
import {
    Injectable
} from '@angular/core';
import {
    Http, Response, RequestOptions,
    Request, RequestMethod, Headers
} from '@angular/http';
// Imports the product class used to map to the 
// OData DTOProduct class
import {
    IProduct
} from './product';
// This service uses rxjs Observable
import {
    Observable
} from 'rxjs/Observable';
import 'rxjs/add/operator/do';
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/map';
// This is marked Injectable because it will be 
// consumed in the component class
@Injectable()
export class ProductService {
    // This is the URL to the OData end point
    private _productUrl = 'odata/ODataProducts';
    // Pass the Http object to the class through the constructor
    constructor(private _http: Http) { }
    // ** Get all Products **
    getProducts(): Observable<IProduct[]> {
        // Make the Angular 2 Get
        // Note that we must use .value
        // because the OData response is wrapped in an object
        // and the data we want to map is at .value
        return this._http.get(this._productUrl)
            .map((response: Response) => <IProduct[]>response.json().value)
            .catch(this.handleError);
    }
    // ** Create a Product **
    createProduct(paramProduct: IProduct): Observable<IProduct> {
        // This is a Post so we have to pass Headers
        let headers = new Headers({ 'Content-Type': 'application/json' });
        let options = new RequestOptions({ headers: headers });
        // Make the Angular 2 Post
        return this._http.post(this._productUrl, JSON.stringify(paramProduct), options)
            .map((response: Response) => <IProduct>response.json())
            .catch(this.handleError);
    }
    // ** Update a Product **
    updateProduct(paramProduct: IProduct): Observable<void> {
        // This is a Put so we have to pass Headers
        let headers = new Headers({ 'Content-Type': 'application/json' });
        let options = new RequestOptions({ headers: headers });
        // Make the Angular 2 Put
        return this._http.put(
            this._productUrl + "(" + paramProduct.Id + ")",
            JSON.stringify(paramProduct), options)
            .catch(this.handleError);
    }
    // ** Delete a Product **
    deleteProduct(id: number): Observable<void> {
        // A Delete does not return anything
        return this._http.delete(this._productUrl + "(" + id + ")")
            .catch(this.handleError);
    }
    // ** Called when there are any errors **
    private handleError(error: Response) {
        console.error(error);
        return Observable.throw(error.json().error || 'Server error');
    }
}

 

This code calls the OData methods and returns Observables that will be consumed by the component code.

image

Create a TypeScript file in the folder called product.component.ts using the following code:

 

import {
    Component, OnInit, OnDestroy, Input, Output,
    ViewContainerRef, EventEmitter, ViewChild, trigger
} from '@angular/core';
import {
    Router, ActivatedRoute
} from '@angular/router';
import {
    Subscription
} from 'rxjs/Subscription';
import {
    IProduct
} from './product';
import {
    ProductService
} from './product.service';
import {
    IUser
} from '../user/user';
import {
    UserService
} from '../user/user.service';
import {
    ModalDirective
} from 'ng2-bootstrap/ng2-bootstrap';
@Component({
    moduleId: module.id,
    selector: 'product-form',
    templateUrl: 'product.component.html'
})
export class ProductComponent implements OnInit {
    // Required for the Modal popup
    @ViewChild('childModal') public childModal: ModalDirective;
    // The current User
    user: IUser;
    // The currently selected Product
    productSelected: IProduct;
    // The list of Products
    products: IProduct[];
    // Any error messages
    errorMessage: string;
    // Controls if the Product list is to be shown
    showProductList: boolean = false;
    // Contructor is called when the class is created
    constructor(private _productService: ProductService,
        private _userService: UserService) {
        // Set ProductList to be hidden
        // Only show is user is logged in
        this.showProductList = false;
    }
    // Handle data related tasks in ngOnInit
    ngOnInit() {
        // Call the Current User method
        this.getCurrentUser();
        // Initialize productSelected
        this.productSelected = this.createNewProduct();
    }
    getCurrentUser() {
        // Call the getCurrentUser service
        this._userService.getCurrentUser().subscribe(
            user => {
                // Assign the user
                this.user = user;
                // See if we are logged in and have a user
                if (this.user.UserName != "[Not Logged in]") {
                    // Get the Products
                    this.getProducts();
                }
            },
            error => this.errorMessage = <any>error);
    }
    // ** Calls the service to retrieve all the Products **
    getProducts() {
        // Call the service
        this._productService.getProducts()
            .subscribe((products) => {
                // Set the products collection to the 
                // Products returned
                this.products = products;
                // Are any Products returned?
                if (products.length > 0) {
                    // Set the first Product as selected
                    this.productSelected = products[0];
                }
            },
            error => this.errorMessage = <any>error);
        // Show the Products list
        this.showProductList = true;
    }
    // ** Called when the Edit button is pressed ** 
    selectProduct(paramProduct: IProduct) {
        // Set the selected Product
        this.productSelected = paramProduct;
        // Open the Popup
        this.childModal.config.backdrop = false // workaround for Angular bug
        this.childModal.show();
    }
    // ** Called when the Delte button is pressed **
    deleteProduct(paramProduct: IProduct) {
        // Call the service to delete the Product
        this._productService.deleteProduct(paramProduct.Id)
            .subscribe(() => {
                // Refresh list - Get the Products
                this.getProducts();
            },
            error => this.errorMessage = <any>error);
    }
    // ** Called when the Add Product buton is pressed **
    newProduct() {
        // Set productSelected to a new Product
        this.productSelected = this.createNewProduct();
        // Open the Popup
        this.childModal.config.backdrop = false // workaround for Angular bug
        this.childModal.show();
    }
    // ** Called by the newProduct method to return 
    // a new Product **
    createNewProduct() {
        // Create a new Product
        let newProduct: IProduct = {
            Id: 0,
            ProductName: '',
            ProductPrice: '',
        }
        return newProduct;
    }
    // ** Called when saving a Product **
    onSubmit() {
        // Is this a new Product?
        if (this.productSelected.Id == 0) {
            // Call the service to Insert the Product
            this._productService.createProduct(this.productSelected)
                .subscribe((createdProduct) => {
                    // Add the Product to the collection
                    this.products.push(createdProduct);
                    // Close the Popup
                    this.childModal.hide();
                },
                error => this.errorMessage = <any>error);
        }
        else {
            // Call the service to update the Product
            this._productService.updateProduct(this.productSelected)
                .subscribe(() => {
                    // Close the Popup
                    this.childModal.hide();
                },
                error => this.errorMessage = <any>error);
        }
    }
}

 

image

Create a file in the folder called product.component.html using the following code:

 

<!-- Error (if any) -->
<div class='panel panel-danger' *ngIf="errorMessage">
    <alert type="info">{{ errorMessage }}</alert>
</div>
<!-- Add Product Button -->
<button class='btn btn-success'
        (click)="newProduct()"
        *ngIf='showProductList'>
    Add Product
</button><br /><br />
<!-- Products list -->
<div class='panel panel-primary' *ngIf='showProductList && products'>
    <div class="table-responsive">
        <table class="table">
            <thead>
                <tr>
                    <th>&nbsp;</th>
                    <th>Product Name</th>
                    <th>Product Price</th>
                </tr>
            </thead>
            <tbody>
                <tr *ngFor="let product of products">
                    <td>
                        <div>
                            <button class="btn btn-primary btn-xs glyphicon glyphicon-edit"
                                    (click)="selectProduct(product)">
                            </button>
                            &nbsp;
                            <button class="btn btn-danger btn-xs glyphicon glyphicon-trash"
                                    (click)="deleteProduct(product)">
                            </button>
                        </div>
                    </td>
                    <td>{{ product.ProductName }}</td>
                    <td>{{ product.ProductPrice | currency:'USD':true }}</td>
                </tr>
            </tbody>
        </table>
    </div>
</div>

 

image

This creates the main markup that displays the Products and the buttons.

Add the following code to the file:

 

<!-- Edit Product -->
<div class="modal fade" bsModal #childModal="bs-modal" [config]="{backdrop: 'static'}"
     tabindex="-1" role="dialog" aria-labelledby="mySmallModalLabel" aria-hidden="true">
    <div class="modal-dialog modal-sm">
        <div class="modal-content">
            <div class="modal-header">
                <button type="button" class="close" aria-label="Close"
                        (click)="childModal.hide()">
                    <span aria-hidden="true">&times;</span>
                </button>
                <h4 class="modal-title">Edit Product</h4>
            </div>
            <div class="modal-body">
                <form (ngSubmit)="onSubmit()" #productForm="ngForm">
                    <div class="form-group">
                        <label for="ProductName">Product Name</label>
                        <input type="text" class="form-control" id="ProductName"
                               required
                               [(ngModel)]="productSelected.ProductName"
                               name="ProductName"
                               #name="ngModel">
                    </div>
                    <div class="form-group">
                        <label for="ProductPrice">Product Price</label>
                        <input type="text" class="form-control" id="ProductPrice"
                               required
                               [(ngModel)]="productSelected.ProductPrice"
                               name="ProductPrice"
                               #name="ngModel">
                    </div>
                    <button type="submit" class="btn btn-default"
                            [disabled]="!productForm.form.valid">
                        Submit
                    </button>
                </form>
            </div>
        </div>
    </div>
</div>

 

image

This contains the template to handle the Bootstrap modal popup.

Update The Angular Module Code

image

Open the app.component.ts file and replace all the code with the following code:

 

import { Component, ViewContainerRef } from '@angular/core';
import { AlertComponent } from 'ng2-bootstrap/ng2-bootstrap';
import { NgModel } from '@angular/forms';
@Component({
    selector: 'my-app',
    template: ` 
      <user-detail>Loading...</user-detail>
      <product-form>Loading...</product-form>
   `
})
export class AppComponent {
    private viewContainerRef: ViewContainerRef;
    public constructor(viewContainerRef: ViewContainerRef) {
        // You need this small hack in order to catch application 
        // root view container ref
        this.viewContainerRef = viewContainerRef;
    }
}

 

This adds support for the ng2-Bootstrap and displays the Product component.

image

Open the app.module.ts file and replace all the code with the following code:

 

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HttpModule } from '@angular/http';
import { FormsModule } from '@angular/forms';
import { Ng2BootstrapModule } from 'ng2-bootstrap/ng2-bootstrap';
import { UserService } from './user/user.service';
import { ProductService } from './product/product.service';
import { AppComponent } from './app.component';
import { UserComponent } from './user/user.component';
import { ProductComponent } from './product/product.component';
@NgModule({
    imports: [
        BrowserModule,
        HttpModule,
        FormsModule,
        Ng2BootstrapModule 
    ],
    declarations: [
        AppComponent,
        UserComponent,
        ProductComponent
    ],
    providers: [
        UserService,
        ProductService
    ],
    bootstrap: [
        AppComponent
    ]
})
export class AppModule { }

 

image

Finally, open the Index.cshtml file (in the Views/Home directory) and replace all the code with the following code:

 

@{
    ViewBag.Title = "Home Page";
}
<div>
    <h4>Angular 2 QuickStart</h4>
</div>
<div class="row">
    <div class="col-md-4">
        <!-- Angular2 Code -->
        <my-app>Loading...</my-app>
        <!-- Angular2 Code -->
    </div>
</div>

 

image

Hit F5 to run the application.

image

Remember you have to Register and Log in to use the application.

Links

Hello World! in Angular 2 using Visual Studio 2015 and ASP.NET 4

Implement ASP.NET 4 MVC Application Security in Angular 2 Using OData 4

Resources to Learn Angular 2

Angular 2: Getting Started (Pluralsight – Paid)

Introduction to Angular 2.0 (Microsoft Virtual Academy – Free)

Download

The project is available at http://lightswitchhelpwebsite.com/Downloads.aspx

You must have Visual Studio 2015 Update 3 (or higher) and TypeScript 2.0 (or higher) installed to run the code.


Your name:
Gravatar Preview
Your email:
(Optional) Email used only to show Gravatar.
Your website:
Title:
Comment:
Security Code
CAPTCHA image
Enter the code shown above in the box below
Add Comment   Cancel 
Microsoft Visual Studio is a registered trademark of Microsoft Corporation / LightSwitch is a registered trademark of Microsoft Corporation