Introduction
The main idea through this demo is to initiate the development of an Angular2 application in .NET core web application.
In this post, I try to explain how to:
- Create a web API in .NET Core
- Reverse engineer using Entity Framework Database approach
- Create services in Angular2 project
- Create component in Angular2 project
- Create separate HTML template in Angular2 project
- Validate form in Angular2 project using
ReactiveFormsModule
module
After running this project, you will get the following render:
In this article, I relied on the following links to build my application:
Background
To better understand this demo, it is preferable that you have a good knowledge of:
- Programming in C#, JavaScript and HTML
- MVC architecture
- SQL language
- Data Binding
- Entity Framework
- Visual Studio
Prerequisites
To run this example, you need to install:
Before you start reading the rest of this article, I recommend you read my last article: CRUD in ASP.NETCore MVC with Angular 2 and Web API, it contains more details about configuration, installation and start-up of Angular2 & .NET CORE web application.
Using the Code
In this section, I will proceed step by step to explain how to build such a web application:
A) Setup DataBase
1) Create Database
I used SQL Server 2014 to locally host my database.
Our database will contain the following entities:
User Entity: will contain a list of users, composed by:
userId
: unique identifier for user is an auto increment, type: int
userName
: is the first name, type: varchar
userBirthDate
: is the birth date, type: datetime
. userBirthLocation
: is a reference key to the country
entity, type: varchar
userPicture
: it will store the absolute URL of image in the server, type: varchar
Country Entity: will store different countries name. It is used by userBirthLocation
attribute.
The following steps will help you to prepare your database:
- Create a database named ‘
cp_UsersDataBase
’ - Execute the following SQL Script:
create table Country(
id varchar(250) primary key
);
create table cp_users
(
userId int identity(1,1),
userName varchar(250),
userBirthDate DateTime,
userBirthLocation varchar(250),
userPicture varchar(max),
Primary Key(userId),
Foreign Key(userBirthLocation) references Country(id),
);
insert into Country values ('france'),('italy'),('russia'),('germany'),('spain')
2) Using Entity Framework Database Approach
In this section, you will do a reverse engineering to import model of your existing database. To do that, you should follow the steps given below:
- Navigate to the root folder of application
- Import required dependencies and tools: to do that, you should configure the project.json by:
*Adding the following dependencies:
"Microsoft.EntityFrameworkCore":"1.0.0",
"Microsoft.EntityFrameworkCore.SqlServer": "1.0.0",
"Microsoft.EntityFrameworkCore.SqlServer.Design": "1.0.0",
"Microsoft.EntityFrameworkCore.Tools": "1.0.0-preview2-final",
*Adding tools:
"Microsoft.EntityFrameworkCore.Tools": "1.1.0-preview4-final",
"Microsoft.EntityFrameworkCore.Tools.DotNet": "1.0.0-preview3-final",
*Using cmd.exe, navigate to project root and:
dotnet -restore
*Start the reverse engineering, to load the model for your existing database:
Scaffold-DbContext "Data Source=LFRUL-013;Initial Catalog=cp_UsersDataBase;
Integrated Security=True" Microsoft.EntityFrameworkCore.SqlServer -OutputDir Models/DataBase
B) Setup Restful WebApi
Create a web API controller inside controller folder named ‘ServicesController
’ that will contain the following actions:
api/Services/GetCountries
: returns list of available countries from database api/Services/GetUsers
: returnd list of user from database, this service uses a pagination system, it will return only a set of elements from remaining data based on two pieces of information: ‘Index of last loaded element’ and 'Page Size' api/Services/AddUser
: creates a new user into database api/Services/DeleteUser
: removes an existing user from database using unique identifier
[Route("api/Services")]
public class ServicesController : Controller
{
private readonly IHostingEnvironment _hostingEnvironment;
public ServicesController(IHostingEnvironment hostingEnvironment)
{
_hostingEnvironment = hostingEnvironment;
}
[Route("GetCountries")]
[HttpGet]
public IEnumerable<string> GetCountries()
{
cp_UsersDataBaseContext databaseContext = new cp_UsersDataBaseContext();
return databaseContext.Country.Select(p => p.Id.ToString()).ToList();
}
[Route("GetUsers")]
[HttpGet]
public IEnumerable<UserMessage> GetUsers(int currentLength, int pageSize)
{
cp_UsersDataBaseContext databaseContext = new cp_UsersDataBaseContext();
List<UserMessage> users = databaseContext.CpUsers.Select(p => new UserMessage() {
userId = p.UserId,
userBirthDate = p.UserBirthDate,
userPictureSrc = Tools.convertImageToBase64(p.UserPicture),
userBirthLocation = p.UserBirthLocation,
userName = p.UserName
}).OrderBy(p => p.userId).Skip(currentLength).Take(pageSize).ToList();
return users;
}
[Route("AddUser")]
[HttpPost]
public HttpResponseMessage AddUser()
{
try
{
string pictureServerPath = "";
if (Request.Form.Files.Count > 0)
{
string contentRootPath = _hostingEnvironment.ContentRootPath;
var formFile = Request.Form.Files[0];
var pictureRelativePath = Path.Combine("Content", "pictures",
Path.GetRandomFileName() + Path.GetExtension(formFile.FileName));
pictureServerPath = Path.Combine(contentRootPath, pictureRelativePath);
FileStream fileStream = new FileStream(pictureServerPath, FileMode.Create);
formFile.CopyTo(fileStream);
fileStream.Flush();
GC.Collect();
}
cp_UsersDataBaseContext databaseContext = new cp_UsersDataBaseContext();
CpUsers cpUser = new CpUsers()
{
UserName = Request.Form["userName"],
UserBirthLocation = Request.Form["userBirthLocation"],
UserBirthDate = DateTime.ParseExact(Request.Form["userBirthDate"],
"dd/MM/yyyy", CultureInfo.InvariantCulture),
UserPicture = pictureServerPath,
};
databaseContext.CpUsers.Add(cpUser);
databaseContext.SaveChanges();
}catch(Exception ex)
{
return new HttpResponseMessage(System.Net.HttpStatusCode.InternalServerError);
}
return new HttpResponseMessage(System.Net.HttpStatusCode.OK);
}
[Route("DeleteUser")]
[HttpDelete]
public HttpResponseMessage DeleteUser(int id)
{
try{
cp_UsersDataBaseContext databaseContext = new cp_UsersDataBaseContext();
CpUsers cpUser = new CpUsers() { UserId = id};
databaseContext.CpUsers.Attach(cpUser);
databaseContext.Entry(cpUser).State = Microsoft.EntityFrameworkCore.EntityState.Deleted;
databaseContext.SaveChanges();
}catch(Exception ex)
{
return new HttpResponseMessage(System.Net.HttpStatusCode.InternalServerError);
}
return new HttpResponseMessage(System.Net.HttpStatusCode.OK);
}
}
After running the web API application, you should get the below result when you navigate to the following URL: 'api/Services/GetCountries
':
C) Implement Frontend Part
At the end, what it looks like is the treeview
of the front-end project:
In the wwwroot/app folder, create the following files:
1) DatePipe.ts
Used to create a custom filter that transforms a date to specific string
format (dd/mm/yyyy
).
import { Pipe, PipeTransform } from '@angular/core'
import * as moment from 'moment';
@Pipe({
name: 'formatDate'
})
export class DatePipe implements PipeTransform {
transform(date: any, args?: any): any {
let d = new Date(date)
return moment(d).format('DD/MM/YYYY')
}
}
2) User.ts
Used to deserialize json data into a user object.
import { IUser } from '../model/IUser';
export class User implements IUser {
public userId: number;
public userName: string;
public userBirthDate: Date;
public userBirthLocation: string;
public userPictureFile: any;
public userPictureSrc: string;
constructor( )
{
}
}
3) index.service.ts
This is our service class that implements needed methods such as:
loadCountries
method: Loads list of countries from an external database, by calling an existing web service [/api/Services/GetCountries]
loadUsers
method: Loads list of user from an external database, by calling an existing web service [/api/Services/GetUsers]
addUser
method: Invokes an external web service [/api/Services/AddUser]
that adds a new user into database deleteUser
method: Deletes a specific product from database by calling an existing web service [/api/Services/DeleteUser
]
import { Injectable } from '@angular/core';
import { Http, Response, RequestOptions, Headers } from '@angular/http';
import 'rxjs/add/operator/toPromise';
import { User } from '../model/User';
@Injectable()
export class IndexService {
constructor(private _http: Http) { }
loadCountries(): Promise<string[]> {
return this._http.get('/api/Services/GetCountries')
.toPromise()
.then(response => this.extractArray(response))
.catch(this.handleErrorPromise);
}
loadUsers(skipSize: number, pageSize: number): Promise<User[]> {
return this._http.get
('/api/Services/GetUsers?currentLength=' + skipSize +'&pageSize=' + pageSize)
.toPromise()
.then(response => this.extractArray(response))
.catch(this.handleErrorPromise);
}
addUser(formData: any): Promise<any>{
return new Promise(function (resolve, reject) {
let xhr = new XMLHttpRequest();
xhr.open('POST', '/api/Services/AddUser', true);
xhr.onload = function (e) {
resolve(JSON.parse(xhr.response));
};
xhr.onerror = function (e) {
reject(e);
}
xhr.send(formData);
});
}
deleteUser(id: number) {
return this._http.delete
('/api/Services/DeleteUser?id=' + id).toPromise().catch(this.handleErrorPromise);
}
protected extractArray(res: Response, showprogress: boolean = true) {
let data = res.json();
return data || [];
}
protected handleErrorPromise(error: any): Promise<void> {
try {
error = JSON.parse(error._body);
} catch (e) {
}
return Promise.reject(error);
}
}
4) app.component.ts
This is the core of 'myhw'
component. It represents the code behind of 'apptemplate.component.html' template.
The HwComponent
class is the view model for the 'apptemplate.component.html', is composed by the following methods:
loadMore
method: loads more users and adds them to current list by invoking an external service via the call of " loadUsers
method" of "IndexService
variable". It implements a progressive load logic by retrieving 6 entries from remaining data in database after each call. ngOnInit
method: This method is called when the component is refreshed, it will load all existing countries data into select input of ‘newUserForm
’ form, and load the 6 first entries from ‘CpUsers
’ by calling loadMore
method. addUser
method: Adds a new user by passing the input data of ‘newUserForm
’ form to "Delete
method" of "IndexService
variable". deleteUser
method: Deletes an existing user identified by its "unique key" by calling "deleteUser
method" of "IndexService
variable".
import { Component, OnInit, Pipe, PipeTransform } from '@angular/core';
import { FormBuilder, FormGroup, Validators} from '@angular/forms';
import { User } from './model/User';
import { DatePipe } from './model/DatePipe';
import { IndexService } from './services/index.service';
@Component({
selector: 'myhw',
templateUrl: './app/template/apptemplate.component.html'
})
export class HwComponent extends OnInit {
public registerForm: FormGroup;
private model: User;
private listCountries : string[];
private listUsers: User[];
private pageSize: number = 6;
private isInserting: boolean;
constructor(private indexService: IndexService, private formBuilder: FormBuilder) {
super();
this.model = new User();
const dateFormatRegex = '^([0-9]{2})/([0-9]{2})/([0-9]{4})$';
this.registerForm = this.formBuilder.group({
userName: ['', Validators.required],
userBirthDate: ['', Validators.compose
([Validators.required, Validators.pattern(dateFormatRegex)])],
userBirthLocation: ['', Validators.required],
picture: ['', Validators.required],
});
}
loadMore() {
let _self = this;
this.indexService.loadUsers((_self.listUsers != null) ? _self.listUsers.length : 0,
_self.pageSize).then(response => {
if (_self.listUsers != null)
_self.listUsers = _self.listUsers.concat(response);
else {
_self.listUsers = response;
}
});
}
ngOnInit() {
this.indexService.loadCountries().then(response => {
this.listCountries = response;
});
this.loadMore();
}
onChange(event) {
var file = event.srcElement.files[0];
this.model.userPictureFile = file;
if (file != null && file.name.length > 0) {
(this.registerForm.controls["picture"]).setValue("azfazf");
}
}
addUser() {
let _self = this;
this.isInserting = false;
var datePipe = new DatePipe();
let formData = new FormData();
formData.append('userName', _self.model.userName);
formData.append('userBirthDate', datePipe.transform(_self.model.userBirthDate, 'dd/MM/yyyy'));
formData.append('userBirthLocation', _self.model.userBirthLocation);
formData.append('userPictureFile', _self.model.userPictureFile,
_self.model.userPictureFile.name);
this.indexService.addUser(formData).then(response => {
console.log(response);
if (response.statusCode == 200) {
alert("Inserting Operation successfully done");
} else {
alert("Fail to insert, reason :" + response.reasonPhrase);
}
_self.isInserting = false;
}).catch(response => {
_self.isInserting = false;
console.log(response);
});
}
deleteUser(id: number) {
let _self = this;
_self.isInserting = false;
_self.indexService.deleteUser(id).then(response => {
if (response.status == 200) {
_self.listUsers.map(function (obj, index) {
if (obj.userId == id) {
_self.listUsers.splice(index,1);
}
});
_self.isInserting = false;
alert("Deleting Operation successfully done");
} else {
alert("Fail to delete");
}
}).catch(response => {
console.log(response);
});
}
}
5) apptemplate.component.html
This is the HTML template, it contains:
newUserForm (form)
: used to create a new user characterized by: name (text), birth date (text), birth location (select box), picture (file). grid (div)
: is such a scroll view used to display users details. loadMoreUsers (Button)
: is used to load more entries into grid.
Note: registerForm
is FormGroup
used to assign a validation system to our form, to be sure that the add button will be enabled when all input respect 'validation rules' (see the initialization of registerForm
into constructor method of HwComponent
class).
<style>
.scrollView {
height: 600px;
overflow-y: auto;
overflow-x:hidden;
}
.fixedSize{
float: left; width:100px;
}
.errorInput{
color : red;
}
</style>
<div>
<div class="col col-lg-4">
<form id="newUserForm" class="well"
[formGroup]="registerForm" novalidate>
<h3>Add New User </h3>
<div class="form-group">
<label for="name">Name *</label>
<input type="text" class="form-control"
[(ngModel)]="model.userName" name="userName"
formControlName="userName" required>
<p *ngIf="registerForm.controls.userName.errors"
class="errorInput">This field is required!</p>
</div>
<div class="form-group">
<label for="description">BirthDate *</label>
<input type="text" placeholder="dd/mm/yyyy"
class="form-control" [(ngModel)]="model.userBirthDate"
name="userBirthDate" formControlName="userBirthDate" required>
<p *ngIf="registerForm.controls.userBirthDate.errors"
class="errorInput">
Required (Must be in format dd/mm/yyyy).
</p>
</div>
<div class="form-group">
<label for="description">From *</label>
<select class="form-control"
[(ngModel)]="model.userBirthLocation" name="userBirthLocation"
formControlName="userBirthLocation" required>
<option *ngFor="let item of listCountries" [value]="item">
{{item}}
</option>
</select>
<p *ngIf="registerForm.controls.userBirthLocation.errors"
class="errorInput">This field is required!</p>
</div>
<div class="form-group">
<label for="description">Picture *</label>
<input type="file" class="form-control"
(change)="onChange($event)" name="picture" required>
<p *ngIf="registerForm.controls.picture.errors"
class="errorInput">This field is required!</p>
</div>
<button type="button" [disabled]="registerForm.valid == false"
(click)="addUser()" class="btn btn-primary">Ajouter</button>
</form>
<button id="loadMoreUsers" [disabled]="isInserting == true"
(click)="loadMore()" class="btn btn-primary">
<span class="glyphicon glyphicon-refresh"></span> Load More Users</button>
</div>
<div id="grid" class="col col-lg-8 scrollView">
<div *ngFor="let item of listUsers" >
<div class="col-md-4" >
<div class="thumbnail">
<img alt="NULL" [src]="item.userPictureSrc"
class="img-responsive" style="height:150px; width:200px" />
<div class="caption">
<label><span class="fixedSize">Name:
</span>{{item.userName}}</label>
<div> <span style="fixedSize">Birth Date:
</span> {{item.userBirthDate | date: 'dd/MM/yyyy'}}</div>
<div> <span style="fixedSize">Birth Location:
</span> {{item.userBirthLocation}}</div>
<div style="text-align: center;">
<a type="button" style="width: 40px;
" (click)="deleteUser(item.userId)"
href="#"><span class="glyphicon
glyphicon-remove"></span></a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
D) Running the Web Application
To run the demo, you should write the following command-line using CMD
, but first be sure that you are in the root directory of your application:
webpack
: to transpile TS files to JavaScript files dotnet run
: to compile the project and run the server
References
Points of Interest
I hope that you appreciated this post. Try to download the source code and do not hesitate to leave your questions and comments.
History
- v1- 01/01/2017: Initial version