Dec 4

Written by: Michael Washington
12/4/2016 6:49 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
  4. Tutorial: An End-To-End Angular 2 Application Using MVC 5 and OData 4 (this article)

This tutorial shows how you can create a complete End-To-End Angular 2 CRUD (Create Read Update Delete) application using OData 4 and ASP.NET 4 MVC Application security.

The key technologies that will be covered are:

  • Angular 2A popular JavaScript framework for building modern web applications.
  • OData 4 – This is used to provide server-side data endpoints that are called by the Angular 2 code.

image

You can try out the live online application at: https://angular2crud.lightswitchhelpwebsite.com/ (you may have to create a new account and log in to use it).

This application recreates the Online Ordering System (An End-To-End LightSwitch Example) that has been used on the LightSwitchHelpWebsite.com for years.

The sample application has the following features:

  • Products
    • Add Products
    • Edit Products 
    • Delete Products
  • Orders
    • Add Orders
    • Edit Orders
    • Add Order Details
    • Edit Order Details
    • Delete Order Details
    • Delete Orders
  • Business Rules
    • Allow the current user to only see their own Orders
    • Administrator(s) can see and edit all Orders

Creating The Application

image

For this demonstration, we will start with the application created in: Tutorial: Creating An Angular 2 CRUD Application Using MVC 5 and OData 4.

You can obtain the project from the downloads page on this site at the following link: http://lightswitchhelpwebsite.com/Downloads.aspx.

Download and open the project in Visual Studio 2015 (or higher).

Add Administration

image

To facilitate the business rules that specify special features for users in the Administration role, we will implement pages to administer users and roles as well as a method to create the initial administrator account.

We will copy code from the article: Creating User and Roles Administration Pages for an MVC5 Application. See that article for a complete explanation of the code.

Add PagedList

image

A Nuget package to support paging on the Administration screen is required.

Select Tools then NuGet package Manager, then Package Manager Console.

image

Click to the right of the PM> symbol and paste:

Install-Package PagedList.Mvc

Press enter and the package will be installed.

Enable Roles

Even though the Roles tables have been created when we enabled user authentication (in a earlier tutorial), we cannot use them because Roles are not enabled when we initially created the project.

We add the following code to the App_Start\Startup.Auth.cs file:

image

To support this code, we add the following to the App_Start\IdentityConfig.cs file:

image

We also need to copy and overwrite the following files from the Creating User and Roles Administration Pages for an MVC5 Application project:

image

  • Controllers\AdminController.cs
  • Controllers\AccountController.cs
  • Models\UserRolesDTO.cs

image

Also create a Admin directory in the View folder and copy the corresponding files from the Creating User and Roles Administration Pages for an MVC5 Application project to that directory.

Create Administrator (if needed)

To create an Administrator of the application, add the following lines to the Web.config (note that the application has two files named Web.config), in the root of the application:

image

These settings will be used by code in the AccountController.cs file to create an administrator if one does not already exist.

We now have added user administration and roles to the application.

image

Build the solution.

The project should build without errors.

The Data Layer

image

We now need to add the Order and OrderDetail tables to the database.

In the Visual Studio Solution Explorer, right-click on the database file, in the App_Data folder, and select Open.

image

This will open the database in the Server Explorer window.

Right-click on the database and select New Query.

image

Paste the following script in the query window and select Execute:

 

CREATE TABLE [dbo].[Orders] (
    [Id]        INT            IDENTITY (1, 1) NOT NULL,
    [UserName]  NVARCHAR (255) NOT NULL,
    [OrderDate] DATE           NOT NULL,
    CONSTRAINT [PK_dbo.Orders] PRIMARY KEY CLUSTERED ([Id] ASC)
);
CREATE TABLE [dbo].[OrderDetails] (
    [Id]         INT IDENTITY (1, 1) NOT NULL,
    [Quantity]   INT NOT NULL,
    [OrderId]    INT NOT NULL,
    [Product_Id] INT NOT NULL,
    CONSTRAINT [PK_dbo.OrderDetails] PRIMARY KEY CLUSTERED ([Id] ASC),
    CONSTRAINT [FK_dbo.OrderDetails_dbo.Products_Product_Id] FOREIGN KEY ([Product_Id]) 
	REFERENCES [dbo].[Products] ([Id]) ON DELETE CASCADE,
    CONSTRAINT [FK_dbo.OrderDetails_dbo.Orders_OrderId] FOREIGN KEY ([OrderId]) 
	REFERENCES [dbo].[Orders] ([Id]) ON DELETE CASCADE
);
GO
CREATE NONCLUSTERED INDEX [IX_OrderId]
    ON [dbo].[OrderDetails]([OrderId] ASC);
GO
CREATE NONCLUSTERED INDEX [IX_Product_Id]
    ON [dbo].[OrderDetails]([Product_Id] ASC);

 

image

You will see a message that the script executed successfully.

Update The DataContext

We now need to update the DataContext so that we have programmatic access to the tables.

image

Return to the Solution Explorer.

Open the Angular2QuickStartDAL.edmx file

Right-click on the design surface and select Update Model from Database.

image

Select the Add tab.

Select the OrderDetails and Orders tables.

Click Finish.

image

The entities will be added, save and close the file.

Create DTO Classes

We now need to create Data Transfer Object (DTO) classes that we will use to pass the data from the OData controllers to the Angular 2 code.

image

Add the following files, (using the following code), to the Models directory:

 

DTOOrder.cs:

 

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace Angular2QuickStart.Models
{
    public class DTOOrder
    {
        [Key]
        public int Id { get; set; }
        public string UserName { get; set; }
        public string OrderDate { get; set; }
        public ICollection<DTOOrderDetail> colOrderDetails { get; set; }
    }
}

 

DTOOrderDetail.cs:

 

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Angular2QuickStart.Models
{
    public class DTOOrderDetail
    {
        [Key]
        public int Id { get; set; }
        public int Quantity { get; set; }
        public int ProductId { get; set; }
    }
}

 

Create OData Methods

We now need to create the OData methods that will pass the data to the Angular 2 code.

This code will handle all the data access for the Order and OrderDetail tables.

image

Add a file, ODataOrdersController.cs, to the Controllers/OData4Controllers directory 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 System.Web.OData.Routing;
using System.Collections.Generic;
using System.Data.Entity.SqlServer;
using System.Data.SqlClient;
using System.Web.OData.Query;
using Angular2QuickStart.Models;
namespace Angular2QuickStart.Controllers
{
    public class ODataOrdersController : ODataController
    {
        private Entities db = new Entities();
        #region public IQueryable<DTOOrder> GetODataOrders()
        // GET: odata/ODataOrders
        [Authorize]
        [EnableQuery(PageSize = 100, AllowedQueryOptions = AllowedQueryOptions.All)]
        public IQueryable<DTOOrder> GetODataOrders()
        {
            var result = (from Orders in db.Orders
                          select new DTOOrder
                          {
                              Id = Orders.Id,
                              UserName = Orders.UserName,
                              OrderDate =
                              SqlFunctions.StringConvert((double)Orders.OrderDate.Month).TrimStart()
                              + "/"
                              + SqlFunctions.DateName("day", Orders.OrderDate)
                              + "/"
                              + SqlFunctions.DateName("year", Orders.OrderDate),
                              colOrderDetails = (from OrderDetails in db.OrderDetails
                                                 where OrderDetails.Order.Id == Orders.Id
                                                 select new DTOOrderDetail
                                                 {
                                                     Id = OrderDetails.Id,
                                                     ProductId = OrderDetails.Product.Id,
                                                     Quantity = OrderDetails.Quantity
                                                 }).ToList()
                          });
            // Only Administrators can see all records
            if (!this.User.IsInRole("Administrator"))
            {
                result = result
                         .Where(x => x.UserName == this.User.Identity.Name);
            }
            return result.AsQueryable();
        }
        #endregion
        #region public IHttpActionResult Post(DTOOrder dtoOrder)
        // POST: odata/ODataOrders
        [Authorize]
        public IHttpActionResult Post(DTOOrder dtoOrder)
        {
            // Create a new Order
            var NewOrder = new Order();
            NewOrder.OrderDate = Convert.ToDateTime(dtoOrder.OrderDate);
            NewOrder.UserName = this.User.Identity.Name;
            NewOrder.OrderDetails = new List<OrderDetail>();
            // Add the Order Details
            // Loop through all the Order Details passed
            foreach (var orderDetail in dtoOrder.colOrderDetails)
            {
                // Create a new Order Detail
                var NewOrderDetail = new OrderDetail();
                // Set the Quanity
                NewOrderDetail.Quantity = orderDetail.Quantity;
                // Get the Product from the ProductId
                NewOrderDetail.Product =
                    db.Products.Where(x => x.Id == orderDetail.ProductId)
                    .FirstOrDefault();
                // Add the Product to the Order
                NewOrder.OrderDetails.Add(NewOrderDetail);
            }
            // Save the Order
            db.Orders.Add(NewOrder);
            db.SaveChanges();
            // Populate the ID that was created and pass it back
            dtoOrder.Id = NewOrder.Id;
            return Created(dtoOrder);
        }
        #endregion
        #region public IHttpActionResult Put([FromODataUri] int key, DTOOrder dtoOrder)
        // PUT: odata/ODataOrders(1)
        [Authorize]
        public IHttpActionResult Put([FromODataUri] int key, DTOOrder dtoOrder)
        {
            Order ExistingOrder = db.Orders.Find(key);
            if (ExistingOrder == null)
            {
                return StatusCode(HttpStatusCode.NotFound);
            }
            // Only Administrators or the orginal user
            // can edit the Order
            if (!this.User.IsInRole("Administrator"))
            {
                if (ExistingOrder.UserName != this.User.Identity.Name)
                {
                    throw new Exception(
                        String.Format("Record belongs to {0} and the curent user is {1}",
                        ExistingOrder.UserName,
                        this.User.Identity.Name));
                }
            }
            // Update the Order Date
            ExistingOrder.OrderDate = Convert.ToDateTime(dtoOrder.OrderDate);
            // Delete all existing Order Details
            db.Database.ExecuteSqlCommand("Delete from OrderDetails where OrderId = @OrderId",
                new SqlParameter("@OrderId", ExistingOrder.Id));
            db.SaveChanges();
            // Add the Order Details
            // Loop through all the Order Details passed
            foreach (var orderDetail in dtoOrder.colOrderDetails)
            {
                // Create a new Order Detail
                var NewOrderDetail = new OrderDetail();
                // Set the Foreign key to Order
                NewOrderDetail.OrderId = ExistingOrder.Id;
                // Set the Quanity
                NewOrderDetail.Quantity = orderDetail.Quantity;
                // Get the Product from the ProductId
                NewOrderDetail.Product =
                    db.Products.Where(x => x.Id == orderDetail.ProductId)
                    .FirstOrDefault();
                // Add the Product to the Order
                ExistingOrder.OrderDetails.Add(NewOrderDetail);
            }
            // Save changes
            db.Entry(ExistingOrder).State = EntityState.Modified;
            db.SaveChanges();
            return Updated(ExistingOrder);
        }
        #endregion
        #region public IHttpActionResult Delete([FromODataUri] int key)
        // DELETE: odata/ODataOrders(1)
        [Authorize]
        public IHttpActionResult Delete([FromODataUri] int key)
        {
            Order ExistingOrder = db.Orders.Find(key);
            if (ExistingOrder == null)
            {
                return StatusCode(HttpStatusCode.NotFound);
            }
            // Only Administrators or the orginal user
            // can delete the Order
            if (!this.User.IsInRole("Administrator"))
            {
                if (ExistingOrder.UserName != this.User.Identity.Name)
                {
                    throw new Exception(
                        String.Format("Record belongs to {0} and the curent user is {1}",
                        ExistingOrder.UserName,
                        this.User.Identity.Name));
                }
            }
            // Delete the Order
            // (and any Order Detail(s))
            db.Orders.Remove(ExistingOrder);
            // Save changes
            db.SaveChanges();
            return StatusCode(HttpStatusCode.NoContent);
        }
        #endregion
        // Utility
        #region protected override void Dispose(bool disposing)
        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                db.Dispose();
            }
            base.Dispose(disposing);
        }
        #endregion
    }
}

 

image

Next, we add the following code to the WebApiConfig.cs file:

 

    // This is required to make OData paging and filtering work
    config.Select().Expand().Filter().OrderBy().MaxTop(null).Count();

 

Note: This is required to allow us to retrieve the OrderDetails along with the Order in a single call (See OData Error: The query specified in the URI is not valid. The property cannot be used in the query option).

Finally, to register the OData route for the new OData methods we just added, add the following code to the GenerateEntityDataModel() method in the WebApiConfig.cs file:

 

    // Register ODataOrders
    builder.EntitySet<DTOOrder>("ODataOrders");

 

Update The User Interface

We need to update the user interface to show an Administration link if a user is an Administrator.

image

Open the Index.cshtml file in the Views/Home directory and replace all the contents with the following code:

 

@{
    ViewBag.Title = "Home Page";
}
@if (!User.Identity.IsAuthenticated)
{
    <div class="jumbotron">
        <p class="lead">An End To End Angular 2/OData4 Sample Application</p>
        <p class="lead">
            Click <a href="Account/Register"><b>Register</b></a>
            to create an account to see the application.
        </p>
        <p>Use Admin@Admin.com / Password#1 to log in as Admin.</p>
        <p>
            <a href="http://LightSwitchHelpWebsite.com"
               class="btn btn-primary btn-lg">By LightSwitchHelpWebsite.com &raquo;</a>
        </p>
    </div>
}
else
{
    <script>
        System.import('app').catch(function (err) { console.error(err); });
    </script>
    <!-- Angular2 Code -->
    <my-app>Loading...</my-app>
    <!-- Angular2 Code -->
}

 

Open the _Layout.cshtml file in the Views directory and replace all the contents with the following code:

 

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <!-- Bootstrap -->
    <link rel="stylesheet" href="node_modules/bootstrap/dist/css/bootstrap.min.css">
    <title>@ViewBag.Title - Angular2 CRUD Sample</title>
    @Styles.Render("~/Content/css")
    @Scripts.Render("~/bundles/modernizr")
    <!-- Angular2 Code -->
    <base href="/">
    <link rel="stylesheet" href="styles.css">
    <!-- Polyfill(s) for older browsers -->
    <script src="https://cdn.polyfill.io/v2/polyfill.min.js?features=Intl.~locale.en"></script>
    <script src="node_modules/core-js/client/shim.min.js"></script>
    <script src="node_modules/zone.js/dist/zone.js"></script>
    <script src="node_modules/reflect-metadata/Reflect.js"></script>
    <script src="node_modules/systemjs/dist/system.src.js"></script>
    <script src="systemjs.config.js"></script>
</head>
<body>
    <div class="navbar navbar-inverse navbar-fixed-top">
        <div class="container">
            <div class="navbar-header">
                <button type="button" class="navbar-toggle" data-toggle="collapse" 
                        data-target=".navbar-collapse">
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                </button>
                @Html.ActionLink("Angular2 CRUD Sample", "Index",
               "Home", new { area = "" }, new { @class = "navbar-brand" })
            </div>
            <div class="navbar-collapse collapse">
                <ul class="nav navbar-nav">
                    <li>@Html.ActionLink("Home", "Index", "Home")</li>
                    @if (User.IsInRole("Administrator"))
                    {
                        <li>@Html.ActionLink("Administration", "Index", "Admin")</li>
                    }
                </ul>
                @Html.Partial("_LoginPartial")
            </div>
        </div>
    </div>
    <div class="container body-content">
        @RenderBody()
        <hr />
        <footer>
            <p>&copy; @DateTime.Now.Year - Michael Washington (LightSwitchHelpWebsite.com)</p>
        </footer>
    </div>
    @Scripts.Render("~/bundles/jquery")
    @Scripts.Render("~/bundles/bootstrap")
    @RenderSection("scripts", required: false)
</body>
</html>

 

Add Angular 2 Routing

We now want to update the Angular 2 code to show a link to get to the original Products administration, and a link to add and remove Orders.

To start we will create skeleton pages so that we can just see what an update to the routing looks like.

image

Create a folder named order to the app directory.

Add the following files, (using the following code), to the order directory:

 

order.component.html:

 

<p>This is order.component</p>

 

order.component.ts:

 

import {
    Component, OnInit, OnDestroy, Input, Output,
    ViewContainerRef, EventEmitter, ViewChild, trigger
} from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
@Component({
    moduleId: module.id,
    selector: 'order-form',
    templateUrl: 'order.component.html'
})
export class OrderComponent {
  
}

order.service.ts:

 

import { Injectable } from '@angular/core';
@Injectable()
export class OrderService {
    
}

 

Again, this is just placeholder code that we will replace later.

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 { RouterModule } from '@angular/router';
import { UserService } from './user/user.service';
import { ProductService } from './product/product.service';
import { OrderService } from './order/order.service';
import { AppComponent } from './app.component';
import { UserComponent } from './user/user.component';
import { ProductComponent } from './product/product.component';
import { OrderComponent } from './order/order.component';
@NgModule({
    imports: [
        BrowserModule,
        HttpModule,
        FormsModule,
        Ng2BootstrapModule,
        RouterModule.forRoot([
            { path: 'order', component: OrderComponent },
            { path: 'products', component: ProductComponent },
            { path: '', redirectTo: 'order', pathMatch: 'full' },
            { path: '**', redirectTo: 'order', pathMatch: 'full' }
        ])
    ],
    declarations: [
        AppComponent,
        UserComponent,
        ProductComponent,
        OrderComponent
    ],
    providers: [
        UserService,
        ProductService,
        OrderService
    ],
    bootstrap: [
        AppComponent
    ]
})
export class AppModule { }

 

Keep in mind, app.module is the first module to load when the application starts.

This is the final version of this file and contains the configuration for all the elements we will need for the final application.

The part to note at this point is the following:

image

This sets up the routes that will load the appropriate component when the user clicks a link to navigate to order or products.

image

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

 

import { Component, ViewContainerRef } from '@angular/core';
@Component({
    selector: 'my-app',
    template: ` 
       <div>
        <nav class='navbar navbar-default'>
            <div class='container-fluid'>
                <a class='navbar-brand'>{{pageTitle}}</a>
                <ul class='nav navbar-nav'>
                    <li><a [routerLink]="['/order']">Home</a></li>
                    <li><a [routerLink]="['/products']">Product List</a></li>
                </ul>
            </div>
        </nav>
        <div style='width:500px;'>
            <router-outlet></router-outlet>
        </div>
     </div>
     `
})
export class AppComponent {
    // Show Page title
    pageTitle: string = 'Angular 2 Example';
    // Hack for root view container ref
    private viewContainerRef: ViewContainerRef;
    public constructor(viewContainerRef: ViewContainerRef) {
        // You need this small hack in order to catch application 
        // root view container ref
        this.viewContainerRef = viewContainerRef;
    }
}

 

Again, this is the final version of this file and contains the configuration for all the elements we will need for the final application.

The part to note at this point is the following:

image

This displays links that the user can click on to navigate to the different routes.

When a user clicks on a link and navigates to the component, the component is displayed in the div marked router-outlet.

Test The Application

image

Hit F5 to run the application…

image

Your web browser will open.

Click the Log In link to log in as the Administrator (using the username and password indicated on the main page).

(Note: If the Admin@Admin.com account does not already exist in the database, it will be programmatically created when you click on the Log In link)

image

The main page will indicate that the order.component is loaded.

image

In we click on the Product List tab, the original product.component will load.

Close the web browser to stop the application.

Product Security

Right now a non-Administrator can edit the Products.

To prevent this, change the security of the edit methods in the Controllers/OData4Controllers/ODataProductsController.cs file from:

image

To:

image

In addition, change the getCurrentUser() method in the app/product/product.component.ts file…

image

to:

 

    getCurrentUser() {
        // Call the 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]") {
                    if (this.user.UserName.toLowerCase() != "admin@admin.com") {
                        // Only Admin can see this page
                        this.errorMessage = "Only the Administrator can change Products";
                    } else {
                        // Get the Products
                        this.getProducts();
                    }
                }
            },
            error => this.errorMessage = <any>error);
    }

 

image

Now when we log in as a non-Administrator, and click on the Product List tab, we will see a message indicating we cannot edit them.

Note that we must have the code in the OData method, that we inserted in the preceding step, for actual security.

Any security in the .ts file (this is the JavaScript layer) can be bypassed by a hacker.

Complete The Application

image

We need to create two classes that will be used by the order.service class to communicate with the server side OData methods.

Add the following files, (using the following code), to the order directory:

 

order.ts:

 

import { IOrderDetail } from './orderdetail';
/* Defines the order entity */
export interface IOrder {
    Id: number;
    UserName: string;
    OrderDate: string;
    colOrderDetails: Array<IOrderDetail>;
}

 

orderdetail.ts:

 

import { IProduct } from '../product/product';
/* Defines the orderdetail entity */
export interface IOrderDetail {
    Id: number;
    Quantity: number;
    ProductId: number;
    Product: IProduct;
}

 

image

The order.service class is used to communicate with the OData methods. It is called by the order.component class.

Replace all the code for the order.service.ts file with the following code:

 

import { Injectable } from '@angular/core';
import {
    Http, Response, RequestOptions, Request,
    RequestMethod, Headers
} from '@angular/http';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/do';
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/map';
import { IOrder } from './order';
import { IOrderDetail } from './orderdetail';
@Injectable()
export class OrderService {
    // This is the URL to the OData class
    private _OrderUrl = 'odata/ODataOrders';
    // Pass the Http object to the class through the constructor
    constructor(private _http: Http) { }
    getOrder(paramOrder: IOrder): Observable<IOrder> {
        // Make a OData call 
        // 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._OrderUrl + '(' + paramOrder.Id + ')')
            .map((response: Response) => {
                <IOrder>response.json();
            })
            .catch(this.handleError);
    }
    getOrders(paramPage: number): Observable<IOrder[]> {
        // Handle OData paging and request the ORderDetails with the Orders
        var urlODataFilterString: string = this._OrderUrl
            + "?$expand=colOrderDetails&$top=5&$skip=" + ((paramPage * 5) - 5);
        return this._http.get(urlODataFilterString)
            .map((response: Response) => <IOrder[]>response.json().value)
            .catch(this.handleError);
    }
    getOrdersCount(): Observable<number> {
        // Call the OData method to get the total count of Orders
        // this is called by the component to properly set
        // the paging control
        var urlODataFilterString: string = this._OrderUrl
            + "?$count=true&$top=0";
        return this._http.get(urlODataFilterString)
            .map((response: Response) => <number>response.json()['@odata.count'])
            .catch(this.handleError);
    }
    createOrder(paramOrder: IOrder): Observable<IOrder> {
        // This is a Post so we have to pass Headers
        let headers = new Headers({ 'Content-Type': 'application/json' });
        let options = new RequestOptions({ headers: headers });
        // Create a new Order
        // We need to remove the Product because the OData method cannot
        // handle too many nested levels        
        let newOrder: IOrder = this.copyOrder(paramOrder);
        // Make the Angular 2 Post
        return this._http.post(this._OrderUrl, JSON.stringify(newOrder), options)
            .map((response: Response) => <IOrder>response.json())
            .catch(this.handleError);
    }
    updateOrder(paramOrder: IOrder): 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 });
        // Create a new Order
        // We need to remove the Product because the OData method cannot
        // handle too many nested levels        
        let newOrder: IOrder = this.copyOrder(paramOrder);
        // Make the Angular 2 Put
        return this._http.put(
            this._OrderUrl + "(" + newOrder.Id + ")",
            JSON.stringify(newOrder), options)
            .catch(this.handleError);
    }
    deleteOrder(id: number): Observable<void> {
        // A Delete does not return anything
        return this._http.delete(this._OrderUrl + "(" + id + ")")
            .catch(this.handleError);
    }
    copyOrder(paramOrder: IOrder) {
        // Create new OrderDetails
        let NewOrderDetails: IOrderDetail[] = [];
        // Loop through each Order Detail
        for (let OrderDetail of paramOrder.colOrderDetails) {
            // Create a new Order Detail
            let newOrderDetail: IOrderDetail = {
                Id: OrderDetail.Id,
                Product: undefined, // undefined on purpose
                ProductId: OrderDetail.Product.Id,
                Quantity: OrderDetail.Quantity
            }
            // Add Order Detail to collection
            NewOrderDetails.push(newOrderDetail);
        }
        // Create a new Order
        let newOrder: IOrder = {
            Id: paramOrder.Id,
            OrderDate: paramOrder.OrderDate,
            UserName: paramOrder.UserName,
            colOrderDetails: NewOrderDetails
        }
        // Return copied Order
        return newOrder;
    }
    private handleError(error: Response) {
        // in a real world app, we may send the server to 
        // some remote logging infrastructure
        // instead of just logging it to the console
        console.error(error);
        return Observable.throw(error.json().error || 'Server error');
    }
}

 

image

The order.component class is the class that manages all aspects of the Order tab.

Replace all the code for the order.component.ts file with 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 { IOrder } from './order';
import { IOrderDetail } from './orderdetail';
import { OrderService } from './order.service';
import { IUser } from '../user/user';
import { UserService } from '../user/user.service';
import { IProduct } from '../product/product';
import { ProductService } from '../product/product.service';
import { ModalDirective } from 'ng2-bootstrap/ng2-bootstrap';
import { DropdownModule } from 'ng2-bootstrap/ng2-bootstrap';
@Component({
    moduleId: module.id,
    selector: 'order-form',
    templateUrl: 'order.component.html'
})
export class OrderComponent implements OnInit {
    @ViewChild('OrderModal') public orderModal: ModalDirective;
    user: IUser;
    // Order
    OrderSelected: IOrder = this.createNewOrder();
    OrderDetailSelected: IOrderDetail = this.createNewOrderDetail();
    Orders: IOrder[];
    showOrderList: boolean = false;
    // Products
    productSelected: IProduct;
    products: IProduct[];
    // Popup
    PopupMode: string = 'Order';
    PopupTitle: string = 'Edit Order';
    showEditOrder: boolean = true;
    showEditOrderDetail: boolean = false;
    // Paging
    intCurrentPage: number = 1;
    intTotalItems: number = 0;
    // Error message
    errorMessage: string;
    constructor(private _OrderService: OrderService,
        private _userService: UserService,
        private _productService: ProductService) {
        // Set OrderList to be hidden
        // Only show is user is logged in
        this.showOrderList = false;
    }
    ngOnInit() {
        // Call the Current User method
        this.getCurrentUser();
    }
    getCurrentUser() {
        // Call the 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);
    }
    getProducts() {
        // Call the service
        this._productService.getProducts()
            .subscribe((products) => {
                // Set the products 
                this.products = products;
                // Get the Orders
                this.getOrders();
            },
            error => this.errorMessage = <any>error);
    }
    getOrders() {
        // Call the service
        this._OrderService.getOrders(this.intCurrentPage)
            .subscribe((Orders) => {
                // Set the Orders 
                this.Orders = Orders;
                // Set the total count of Order (for the Pager)
                this._OrderService.getOrdersCount()
                    .subscribe((count) => {
                        // Set total count for the Pager control
                        this.intTotalItems = count;
                    },
                    error => this.errorMessage = <any>error);
                // The Order Details for the Orders have a 
                // ProductId but not an actual Product
                // Add the Product now
                // Loop through each Order
                for (let order of this.Orders) {
                    // Loop through each order detail of the Order
                    for (let orderDetail of order.colOrderDetails) {
                        // Loop through the Products collection 
                        // of the OrderDetail
                        for (let product of this.products) {
                            // Did we find the Product.Id 
                            // we are looking for?
                            if (product.Id == orderDetail.ProductId) {
                                // Set the Product
                                orderDetail.Product = product;
                            }
                        }
                    }
                }
                // Show the Orders list
                this.showOrderList = true;
            },
            error => this.errorMessage = <any>error);
    }
    selectOrder(paramOrder: IOrder) {
        // Set the selected Order
        this.OrderSelected = paramOrder;
        // Show the Order screen
        this.setPopupMode("Order");
        // Open the Order Popup
        this.orderModal.config.backdrop = false // workaround for Angular bug
        this.orderModal.show();
    }
    deleteOrder(paramOrder: IOrder) {
        // Call the service to delete the Order
        this._OrderService.deleteOrder(paramOrder.Id)
            .subscribe(() => {
                // Refresh list - Get the Orders
                this.getOrders();
            },
            error => this.errorMessage = <any>error);
    }
    newOrder() {
        // Set OrderSelected to a new Order
        this.OrderSelected = this.createNewOrder();
        this.OrderSelected.UserName = this.user.UserName;
        // Show the Order screen
        this.setPopupMode("Order");
        // Open the Order Popup
        this.orderModal.config.backdrop = false // workaround for Angular bug
        this.orderModal.show();
    }
    createNewOrder() {
        // Create new OrderDetails
        let NewOrderDetails: IOrderDetail[] = [];
        // Create a new Order
        let newOrder: IOrder = {
            Id: 0,
            OrderDate: this.currentDate(),
            UserName: '',
            colOrderDetails: NewOrderDetails
        }
        return newOrder;
    }
    newOrderDetail() {
        // Create new OrderDetail
        let NewOrderDetail: IOrderDetail = this.createNewOrderDetail();
        // Set selected OrderDetail
        this.OrderDetailSelected = NewOrderDetail;
        // Add OrderDetailSelected to OrderDetail collection
        // of the selected Order
        this.OrderSelected.colOrderDetails.push(NewOrderDetail);
        // Show the OrderDetail screen
        this.setPopupMode("OrderDetail");
        // Change the title to sad Add
        this.PopupTitle = 'Add Order Detail';
    }
    createNewOrderDetail() {
        // Create default Product
        let newProduct: IProduct = {
            Id: 0,
            ProductName: '',
            ProductPrice: '',
        }
        // Create a new Order Detail
        let newOrderDetail: IOrderDetail = {
            Id: 0, Product: newProduct, ProductId: 0, Quantity: 1
        }
        // If there are Products set the first one
        if (this.products !== undefined) {
            if (this.products.length > 0) {
                // Update the default colOrderDetails with one that has a Product
                newOrderDetail.Product = this.products[0];
                newOrderDetail.ProductId = this.products[0].Id;
            }
        }
        return newOrderDetail;
    }
    selectOrderDetail(paramOrderDetail: IOrderDetail) {
        // Set the selected OrderDetail
        this.OrderDetailSelected = paramOrderDetail;
        // Show the OrderDetail screen
        this.setPopupMode("OrderDetail");
    }
    deleteOrderDetail(paramOrderDetail: IOrderDetail) {
        // All records are managed in memory
        // so we just remove the OrderDetail
        // Find the index value of the selectedOrderDetail 
        // in colOrderDetails. This is a search by object instance
        // not by a key. We could have several entries that have the 
        // exact same values but this will always find the 
        // correct one.
        var IndexOfOrderDetailToDelete =
            this.OrderSelected.colOrderDetails.indexOf(paramOrderDetail);
        // Use the index value to remove it from the colOrderDetails collection
        // Because of the binding in Angular the item will simply dissapear
        // from the screen.
        this.OrderSelected.colOrderDetails.splice(IndexOfOrderDetailToDelete, 1);
    }
    setPopupMode(paramMode: string) {
        if (paramMode == 'Order') {
            this.PopupTitle = 'Edit Order';
            this.PopupMode = "Order";
            this.showEditOrder = true;
            this.showEditOrderDetail = false;
        }
        if (paramMode == 'OrderDetail') {
            this.PopupTitle = 'Edit Order Detail';
            this.PopupMode = "OrderDetail";
            this.showEditOrder = false;
            this.showEditOrderDetail = true;
        }
    }
    onClosePopup() {
        // If showing OrderDetail then switch back to 
        // Order List
        if (this.PopupMode == "OrderDetail") {
            // switch back to Order List
            this.setPopupMode("Order");
        }
        else {
            // Close popup
            this.orderModal.hide()
        }
    }
    onOrderDetailSubmit() {
        // Show the Order screen
        this.setPopupMode("Order");
    }
    onOrderSubmit() {
        // Is this a new Order?
        if (this.OrderSelected.Id == 0) {
            // Call the service to Insert the Order
            this._OrderService.createOrder(this.OrderSelected)
                .subscribe((createdOrder) => {
                    // Add the Order to the collection
                    this.Orders.push(createdOrder);
                    // Close the Order Popup
                    this.orderModal.hide();
                    // Get the Orders
                    this.getOrders();
                },
                error => this.errorMessage = <any>error);
        }
        else {
            // Call the service to update the Order
            this._OrderService.updateOrder(this.OrderSelected)
                .subscribe(() => {
                    // Close the Order Popup
                    this.orderModal.hide();
                    // Get the Orders
                    this.getOrders();
                },
                error => this.errorMessage = <any>error);
        }
    }
    public pageChanged(event: any): void {
        this.intCurrentPage = event.page;
        this.getOrders();
    };
    currentDate() {
        var CurrentDate = new Date();
        return (CurrentDate.getMonth() + 1)
            + "/" + CurrentDate.getDate()
            + "/" + CurrentDate.getFullYear();
    }
}

 

image

Finally, the order.component.html file contains the html markup for the order.component class.

Replace the contents of the the order.component.html file with the following code:

 

<!-- Error (if any) -->
<div class='panel panel-danger' *ngIf="errorMessage">
    <alert type="info">{{ errorMessage }}</alert>
</div>
<!-- Add Order Button -->
<button class='btn btn-success'
        (click)="newOrder()"
        *ngIf='showOrderList'>
    Add Order
</button><br /><br />
<!-- Orders list -->
<div class='panel panel-primary' *ngIf='showOrderList && Orders'>
    <div class="table-responsive">
        <table class="table">
            <thead>
                <tr>
                    <th>&nbsp;</th>
                    <th>Id</th>
                    <th>User Name</th>
                    <th>Order Date</th>
                </tr>
            </thead>
            <tbody>
                <tr *ngFor="let Order of Orders">
                    <td>
                        <div>
                            <button class="btn btn-primary btn-xs glyphicon glyphicon-edit"
                                    (click)="selectOrder(Order)"></button>
                            &nbsp;
                            <button class="btn btn-danger btn-xs glyphicon glyphicon-trash"
                                    (click)="deleteOrder(Order)"></button>
                        </div>
                    </td>
                    <td>{{ Order.Id }}</td>
                    <td>{{ Order.UserName }}</td>
                    <td>{{ Order.OrderDate | date:"MM/dd/yyyy"}}</td>
                </tr>
            </tbody>
        </table>
        <!-- The Pager -->
        <!-- Note: (ngModel) uses one-way binding -->
        <pagination [totalItems]="intTotalItems"
                    (ngModel)="intCurrentPage"
                    [maxSize]="5"
                    class="pagination-sm"
                    [boundaryLinks]="true"
                    [rotate]="false"
                    (numPages)="numPages = $event"
                    [itemsPerPage]="5"
                    (pageChanged)="pageChanged($event)"></pagination>
    </div>
</div>
<!-- Edit Order -->
<div class="modal fade" bsModal #OrderModal="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)="onClosePopup()">
                    <span aria-hidden="true">&times;</span>
                </button>
                <h4 class="modal-title">{{ PopupTitle }}</h4>
            </div>
            <!-- Order -->
            <div class="modal-body" *ngIf='showEditOrder'>
                <form (ngSubmit)="onOrderSubmit()" #OrderForm="ngForm">
                    <div class="form-group">
                        <label for="OrderName">User Name</label>
                        <p>{{ OrderSelected.UserName }}</p>
                    </div>
                    <div class="form-group">
                        <label for="OrderPrice">Order Date</label>
                        <input type="text" class="form-control" id="OrderDate"
                               required
                               [(ngModel)]="OrderSelected.OrderDate"
                               name="OrderPrice"
                               #name="ngModel">
                    </div>
                    <!-- Add Order Detail Button -->
                    <button type="button"
                            class='btn btn-success'
                            (click)="newOrderDetail()">
                        Add Order Detail
                    </button><br /><br />
                    <!-- Order Details -->
                    <table class="table">
                        <thead>
                            <tr>
                                <th>&nbsp;</th>
                                <th>Product</th>
                                <th>Quantity</th>
                            </tr>
                        </thead>
                        <tbody>
                            <tr *ngFor="let OrderDetail of OrderSelected.colOrderDetails">
                                <td>
                                    <div>
                                        <button type="button" class="btn btn-primary 
                                                btn-xs glyphicon glyphicon-edit"
                                                (click)="selectOrderDetail(OrderDetail)"></button>
                                        &nbsp;
                                        <button type="button" class="btn btn-danger 
                                                btn-xs glyphicon glyphicon-trash"
                                                (click)="deleteOrderDetail(OrderDetail)"></button>
                                    </div>
                                </td>
                                <td>{{ OrderDetail.Product.ProductName }}</td>
                                <td align="center">{{ OrderDetail.Quantity }}</td>
                            </tr>
                        </tbody>
                    </table>
                    <button type="submit" class="btn btn-danger"
                            [disabled]="!OrderForm.form.valid">
                        Save
                    </button>
                </form>
            </div>
            <!-- Order Detail -->
            <div class="modal-body" *ngIf='showEditOrderDetail'>
                <form (ngSubmit)="onOrderDetailSubmit()" #OrderDetailForm="ngForm">
                    <div class="form-group">
                        <label for="OrderName">Product</label>
                        <select [(ngModel)]="OrderDetailSelected.Product"
                                class="form-control"
                                name="Product"
                                #name="ngModel">
                            <option *ngFor="let product of products" 
                                    [ngValue]="product">{{product.ProductName}}</option>
                        </select>
                    </div>
                    <div class="form-group">
                        <label for="OrderPrice">Quantity</label>
                        <input type="text" class="form-control" id="Quantity"
                               required
                               [(ngModel)]="OrderDetailSelected.Quantity"
                               name="Quantity"
                               #name="ngModel">
                    </div>
                    <button type="submit" class="btn btn-danger"
                            [disabled]="!OrderDetailForm.form.valid">
                        Save
                    </button>
                </form>
            </div>
        </div>
    </div>
</div>

 

Notes

  • The following polyfill was added to the application:
<script src="https://cdn.polyfill.io/v2/polyfill.min.js?features=Intl.~locale.en"></script>

To fix an error thrown in IE10. See: date pipe issue on IE10 - 'Intl' is undefined.

  • If multiple ng2-Bootstrap Modal popups are nested inside of each other, it can cause the web browser to lock up. that is why it was not done in this tutorial.

Angular 2 Tutorial Series

  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

Resources to Learn Angular 2

Angular 2: Getting Started (Pluralsight – Paid)

Introduction to Angular 2.0 (Microsoft Virtual Academy – Free)

Getting Started with npm in Visual Studio

Using EcmaScript 2015 Modules in TypeScript with SystemJS

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.

1 comment(s) so far...


Gravatar

Re: Tutorial: An End-To-End Angular 2 Application Using MVC 5 and OData 4

Thank you so much for your effort in helping us to switch from LIGHTSWITCH. One of my favorite characteristics of LIGHTSWITCH is how easy is to deploy to SharePoint. Is this possible to do with an Angular 2 App? What do you suggest better to develop CRUD apps hosted in SharePoint Online?

By Luis Alva on   1/14/2017 2:02 AM

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