Nov
23
Written by:
Michael Washington
11/23/2016 10:18 PM
Note: This tutorial is part of a series of articles that create a complete Angular 2 application using OData 4 and ASP.NET 4:
- 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
- Tutorial: Creating An Angular 2 CRUD Application Using MVC 5 and OData 4 (this article)
- 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.
We will create a Data layer, the Angular 2 code, and display everything on the MVC View.
Creating The Application
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
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.
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.
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
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)
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)
);
Click the Update Database button.
The table will be created.
In the Server Explorer, click the refresh button to see the Products table show.
Create The Entity Framework Data Model
Return to the Solution Explorer.
Right-click on the Models folder and select Add then New Item…
Add an ADO.NET Entity Data Model called Angular2QuickStartDAL.
Select EF Designer from database.
Select the DefaultConnection.
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)
The .edmx file will be created.
Close the file.
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.
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; }
}
}
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
}
}
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
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"
Save the file, then right-click on it and select Restore Packages.
The packages will install.
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">
Select the Show All Files button.
Include the Systemjs.config.js file (in the root directory).
Select the Show All Files button again to hide the other files.
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
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.
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.
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);
}
}
}
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> </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>
<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>
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">×</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>
This contains the template to handle the Bootstrap modal popup.
Update The Angular Module Code
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.
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 { }
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>
Hit F5 to run the application.
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.