Click here to Skip to main content
14,022,475 members
Click here to Skip to main content
Add your own
alternative version

Stats

15.4K views
14 bookmarked
Posted 29 Jan 2017
Licenced CPOL

Angular 2 & .NET Core developing web app From Scratch Part 2: Implementing Front-end part

, 4 Feb 2017
Rate this:
Please Sign up or sign in to vote.
Great article to learn how you can create web application from scratch using Angular2 & .NET CORE WEB API

Source Code

Contents

Introduction

After preparing the server part, we will start the build of client part (front-end part) using Angular2 framework.
 
Our application is a single page application composed by four modules : 
  • home
 is the main page, used to expose available products to clients.
  • Login
is the authentication page, allow users to log into application and have access to back-office side.
  • Subscription
is the subscription page, which provides a form to create new users account.
  • Product  Management :
this module provides form to insert new products into database.
 

Background

To better understand this demo, it is preferable that you have a good knowledge’s about :

  • Programming in C#, JavaScript and HTML
  • Angular2
  • MVC architecture
  • Data Binding
  • Entity Framework
  • Visual Studio Code (optional)

Prerequisites

Using the code

A) Create and Configure Angular2 Project

First, use CMD to create new folder (new project) named 'angular2fromscratch' in which download the Angular2 project from Angular2 official documentation,

After you should create the same configuration files (based on the official Angular2 documentation,):

  • package.json
  • tsconfig.json
  • typings.json

Next you should install typescripttypings, webpack, cookies service and angular2-material using npm :

  • Npm install –g typescript
  • Npm install -g typings
  • Npm install angular2-cookie
  • npm install @angular2-material/sidenav
  • npm install @angular2-material/input
  • npm install @angular2-material/button
  • npm install @angular2-material/core
  • npm install @angular2-material/card
  • npm install @angular2-material/icon
  • npm install @angular2-material/toolbar
  • npm install @angular2-material/progress-circle
  • npm install @angular2-material/sidenav 

 

The final treeview should look like below : 
 
 
Before you start implementation, you should make some change on  systemjs.config.js fileto define alias for new required Angular2 packages  :
 
(function (global) {
  System.config({
    paths: {
      // paths serve as alias
      'npm:': 'node_modules/'
    },
    // map tells the System loader where to look for things
    map: {
      // our app is within the app folder
      app: 'app',
      // angular bundles
      '@angular/core': 'npm:@angular/core/bundles/core.umd.js',
      '@angular/common': 'npm:@angular/common/bundles/common.umd.js',
      '@angular/compiler': 'npm:@angular/compiler/bundles/compiler.umd.js',
      '@angular/platform-browser': 'npm:@angular/platform-browser/bundles/platform-browser.umd.js',
      '@angular/platform-browser-dynamic': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js',
      '@angular/http': 'npm:@angular/http/bundles/http.umd.js',
      '@angular/router': 'npm:@angular/router/bundles/router.umd.js',
      '@angular/forms': 'npm:@angular/forms/bundles/forms.umd.js',
      '@angular2-material/input': 'npm:@angular2-material/input/input.umd.js',
      '@angular2-material/core': 'npm:@angular2-material/core/core.umd.js',
      '@angular2-material/card': 'npm:@angular2-material/card/card.umd.js',
      '@angular2-material/button': 'npm:@angular2-material/button/button.umd.js',
      '@angular2-material/icon': 'npm:@angular2-material/icon/icon.umd.js',
      '@angular2-material/toolbar': 'npm:@angular2-material/toolbar/toolbar.umd.js',
      '@angular2-material/progress-circle': 'npm:@angular2-material/progress-circle/progress-circle.umd.js',
      '@angular2-material/sidenav': 'npm:@angular2-material/sidenav/sidenav.umd.js',
       'angular2-cookie': 'npm:angular2-cookie',
      // other libraries
      'rxjs':                      'npm:rxjs',
      'angular-in-memory-web-api': 'npm:angular-in-memory-web-api',
    },
    // packages tells the System loader how to load when no filename and/or no extension
    packages: {
    
      app: {
        main: './main.js',
        defaultExtension: 'js'
      },
      rxjs: {
        defaultExtension: 'js'
      },
      'angular-in-memory-web-api': {
        main: './index.js',
        defaultExtension: 'js'
      },
       'angular2-cookie': {
        main: './core.js',
        defaultExtension: 'js'
      },
    }
  });
})(this);

B) Setup Angular2 Project

Into the app folder, create the following  files:
 
  • app.module.ts

this file is used to :

  • define routes using Router Module :  "RouterModule",
  • import needed Angular2 modules via the word key: "imports",
  • declare components via the word key: "declarations",
  • declare services via the word key: "providers",
  • specify the root components to include into index.html file via the word key : "bootstrap".

 

import { NgModule }             from '@angular/core';
import { BrowserModule }        from '@angular/platform-browser';
import { FormsModule }          from '@angular/forms';
import { RouterModule, Routes } from '@angular/router';
import { MdInputModule } from '@angular2-material/input';
import { MdCardModule } from '@angular2-material/card';
import { MdButtonModule } from '@angular2-material/button';
import { MdIconModule } from '@angular2-material/icon';
import { MdToolbarModule } from '@angular2-material/toolbar';
import { MdIconRegistry } from '@angular2-material/icon';
import { CookieService } from 'angular2-cookie/services/cookies.service';
import { AppComponent }   from './app.component';
import { loginComponenent }   from './app.loginComponenent';
import { homeComponenent }   from './app.homeComponenent';
import { subscriptionComponenent }   from './app.subscriptionComponenent';
import { productManageComponent }   from './app.productManageComponent';
import { PageNotFoundComponent }   from './app.pageNotFoundComponent';
import { MdProgressCircleModule } from '@angular2-material/progress-circle';
import { MdSidenavModule } from '@angular2-material/sidenav';

import { AuthentificationService } from './services/authentificationService';
import { ManageProductService } from './services/manageProductService'

const appRoutes: Routes = [
  { path: 'home', component: homeComponenent },
  { path: 'login', component: loginComponenent },
  { path: 'subscription', component: subscriptionComponenent },
  { path: 'productManagement', component: productManageComponent },
  { path: '', redirectTo: '/home',  pathMatch: 'full'},
  { path: '**', component: PageNotFoundComponent }
];

@NgModule({

   imports:  [ FormsModule , BrowserModule,MdSidenavModule, MdProgressCircleModule, MdInputModule, MdToolbarModule, MdCardModule, MdButtonModule, MdIconModule,  RouterModule.forRoot(appRoutes)],
    declarations: [ AppComponent, loginComponenent, homeComponenent, subscriptionComponenent, productManageComponent, PageNotFoundComponent],
    providers: [ CookieService, MdIconRegistry, AuthentificationService, ManageProductService ],
    bootstrap: [ AppComponent ]
})
export class AppModule { }
  • app.component.ts

this file is used as code behind for the app.html template, , in which you will have implementation of :

  • constructor : used to initialize our master page (app.html), and to declare router listener that controls user navigation depending on  current session value ( session is managed by cookies).
  • logOut event : this event is used to free current session by clearing cookies, and redirect user to home page.

 

import { Component } from '@angular/core';
import {MdToolbar} from '@angular2-material/toolbar';
import {CookieService} from 'angular2-cookie/core';
import {Router, NavigationStart, NavigationEnd} from '@angular/router'
@Component({
  selector: 'my-app',
  templateUrl: 'app/templates/app.html',
     
})
export class AppComponent {
  title = 'Angular 2 Application';
  showMenu : boolean = false;
  login : string = "";
  constructor(private _cookieService: CookieService, private router : Router){
      this.showMenu = false;
      router.events.subscribe((val) => {
        // see also 
        if(val instanceof NavigationStart) {
            this.login = this._cookieService.get("login");
            if(this.login != null && this.login != ""){
                this.showMenu = true;
                if(val.url.endsWith("login") || val.url.endsWith("subscription")){
                    router.navigateByUrl("/home");
                }
            } 
            else{
                this.showMenu = false;
                if(val.url.endsWith("productManagement")){
                      router.navigateByUrl("/home");
                  }
            }
        }
     });
  
  }
  public logOut(right){
    this.showMenu = false;
    right.close();
    this._cookieService.remove("login");
    location.reload();
    //this.router.navigateByUrl("/home");
  } 
}
  • app.homeComponenent.ts

Constitute the code behind for our home template, in which you will have implementation of:

  • ngOnInit event : refresh product list on each page initialization,
  • Remove event : deletes an existing product (identified by its "unique key") by calling "Delete method" of "_service variable",
  • loadProducts event : load all available products form remote server via the call of loadProducts method of _service variable. 

 

import { Component , OnInit} from '@angular/core';
import { Product } from './model/product'
import { ManageProductService } from './services/manageProductService'
import {CookieService} from 'angular2-cookie/core';

@Component({
  templateUrl: 'app/templates/home.html'
})
export class homeComponenent extends OnInit {
  title = 'Products';
  showProgress : boolean = false; 
  showError : boolean = false;
  showEmpty : boolean = false;
  isLogged  : boolean = false;
  listProduct : Product[];

  constructor(private _service : ManageProductService, private _cookieService: CookieService){
         super();
         console.log("constructor");
  }

  ngOnInit() {
     this.loadProducts();

     this.isLogged = (this._cookieService.get("login")!= null)?true: false;
  }

  public Remove(id: number) {
    var result = confirm("Do you want to continue the suppression process ?");
    let _self = this;
    _self.showError = false;
    _self.showEmpty = false;
    if (result == true) {
       
           this._service.deleteProduct(id).then(function(){
                _self.listProduct = _self.listProduct.filter(value=> {
                    return (value.id != id);
                });
                if(_self.listProduct.length==0){
                    _self.showEmpty = true;
                }
           }).catch(function(){
               _self.showError = true;
           });
    } else {
        
    }

  }
  public loadProducts(){
      
      let _self = this;
      _self.showError = false;
      _self.showEmpty = false;
      _self.showProgress = true;
      _self._service.loadProducts().then(function(response){
        
         _self.showProgress = false;
         if(response.length >0){
         
           _self.listProduct = response;
      
         }else{
            _self.showEmpty = true;
         }
         
      }).catch(function(error: any){
         _self.showProgress = false;
          _self.showError = true;
      });

  }
}
  • app.loginComponenent.ts

Constitute the code behind for login template, in which you will have the implementation of  :

  • tryToConnect event :  open a new session with server using user credential (email, password).

 

 import { Component } from '@angular/core';
import {Router} from '@angular/router'
import { User } from './model/user';
import { AuthentificationService } from './services/authentificationService';
import {CookieService} from 'angular2-cookie/core';

@Component({
  templateUrl: 'app/templates/login.html'
})

export class loginComponenent {
  public title = 'Login';
  public model : User = new User();
  constructor(private _service : AuthentificationService, private _cookieService:CookieService, private router: Router){

  }

  tryToConnect(){
     let _self = this;
      _self._service.Login(_self.model).then(function (response){
          
          _self._cookieService.put("login", _self.model.login);
          _self.router.navigateByUrl('/home');
       })
       .catch(function(error : any){
          alert("Fail to login");
       });
  }
}

 

  • app.productManageComponent.ts

Constitute the code behind for our productManagement template, in which you will have the implementation of  :

  • addNewProduct event : called when user click on submit button for submitting the form data to register a new product. It will call  addProduct method of _service variable.

 

 import { Component } from '@angular/core';

import { Product } from './model/Product';
import { ManageProductService } from './services/manageProductService';
@Component({
  templateUrl: 'app/templates/productManagement.html'
})
export class productManageComponent {
  title = 'Manage Product';
  model : Product;
  constructor(private _service : ManageProductService){
      this.model = new Product();
  }
  onChange(event) {
        var file = event.srcElement.files[0];
        this.model.pictureFile = file;
    
    }
  public addNewProduct(){

    let _self = this;
    let formData = new FormData();
    formData.append("title",_self.model.title);
    formData.append("fullDescription",_self.model.fullDescription);
    formData.append("price",_self.model.price);
    formData.append("file",_self.model.pictureFile, _self.model.pictureFile.name);
    _self._service.addProduct(formData).then(function(response){
      alert("new product was successfully created");
    }).catch(function(error: any){
      alert("Server error");
    });
  }
}
  • app.subscriptionComponenent.ts

Constitute the code behind for our singup template, in which you will have the implementation of  :

Subscription event :   raised when user try to submit new user details. It will call  addProduct method of _service variable.

import { Component } from '@angular/core';
import { User } from './model/user';
import { AuthentificationService } from './services/authentificationService';
import {  NgModule }  from '@angular/core';
@Component({
  templateUrl: 'app/templates/singup.html'
})
export class subscriptionComponenent {
    title = 'Subscription';
  public model : User;

  constructor(private _service : AuthentificationService){
    this.model = new User();
  }
     
  public Subscription(){
    let _self = this;
    this._service.Signup(this.model).then(function(response){
 
       alert("Congratulation, the user with login : "+ _self.model.login  + " has been created");
    })
     .catch(function(error : any){
          alert("Fail to create new user");
      });
  }
}
 
After you should create the different Folder and files : 
 
1) model folder : 
  • product.ts

Used to deserialize json data into a product object.

 

export class Product{
     id : number;
     title : string = '';
     fullDescription : string = '';
     price : number = 0;
     picture : string;
     pictureFile : any;
}
  • user.ts

Used to deserialize json data into a user object.

export class User{
     login : string = '';
     pwd : string = '';
     pwdConfirmation : string = '';
}
2) services folder :
 
  • authentificationService.ts

implements needed methods to ensure user authentication and user registration : 

  • Login : invokes an external web service "api/Services/Login" to create session with server.
  • Signup : call a remote web service "api/Services/Subscription"  to register user.

 

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 AuthentificationService {

    constructor( private _http : Http){
    }

     Login(model : User): Promise<any> {
            
            let _url = "http://localhost:5000/api/Services/Login";

            let bodyString = JSON.stringify(model);
            let headers    = new Headers({ 'Content-Type': 'application/json' }); // ... Set content type to JSON
            let options    = new RequestOptions({ headers: headers });
    
            return this._http.post(_url, bodyString, options)
            .toPromise();
 
    }
    
    Signup(model : any): Promise<any> {
            
            let _url = "http://localhost:5000/api/Services/Subscription";

            let bodyString = JSON.stringify(model);
            let headers    = new Headers({ 'Content-Type': 'application/json' }); // ... Set content type to JSON
            let options    = new RequestOptions({ headers: headers });
    
            return this._http.post(_url, bodyString, options)
            .toPromise();
       
    }
 
    protected handleErrorPromise(error: any): Promise<void> {

        try {
            error = JSON.parse(error._body);
        } catch (e) {
        }
        
        return Promise.reject(error);
    }
 
}
  • manageProductService.ts

this class offers services to  manage product such as :

  • loadProducts : loads available products list by calling an external web service via "api/Services/GetProducts" address ,
  • addProduct : it call an external web service "api/Services/AddProduct" to create a new product,
  • deleteProduct : deletes a specific product from database  by calling an existing web service  "api/Services/DeleteProduct/" .

 

import { Injectable } from '@angular/core';
import { Http, Response, RequestOptions, Headers } from '@angular/http';
import { Product} from '../model/Product';
import 'rxjs/add/operator/toPromise';


@Injectable()
export class ManageProductService {
    constructor(private _http: Http) { }

   loadProducts(): Promise<Product[]> {
                
       let _url = "http://localhost:5000/api/Services/GetProducts";
        return this._http.get(_url)
            .toPromise()
            .then(response => this.extractArray(response));

    }      

    addProduct(formData: any): Promise<any>{
          let _url = "http://localhost:5000/api/Services/AddProduct";
        return new Promise(function (resolve, reject) {

            let xhr = new XMLHttpRequest();
            xhr.open('POST', _url, true);

            xhr.onload = function (e) {
                resolve(JSON.parse(xhr.response));
            };
            xhr.onerror = function (e) {
                reject(e);
            }

            xhr.send(formData);
        });
    }
  
    deleteProduct(id: number) {
        let _url = "http://localhost:5000/api/Services/DeleteProduct/";
        return this._http.delete(_url+'?id=' + id).toPromise();
    }    

    protected extractArray(res: Response, showprogress: boolean = true) {
        let data = res.json();
        return data || [];
    }

}

3) templates folder : 

  • app.html

This is HTML template, is used as master page, it contains : 

  • md-toolbar :  it considered as a naviation menu, from which you can navigate to home, login and subscription pages.
  • md-sidenav :  is right side navigation menu, Which displays information about:
  • email of current user,  
  • navigation button : used to redirect to  productManagement page,
  • logout button : to delete current session and redirect to home page
  • router-outlet  : is placeholder used to display specific view based on router state (read more about router-outlet).  
<md-toolbar >
  <button md-icon-button  class="md-raised md-primary" routerLink="/home">
      <md-icon >home</md-icon>
  </button> 

  <span>{{title}}</span>
     <span flex></span>
    
        <a md-button routerLink="/login"   *ngIf="showMenu == false"  routerLinkActive="active">
            <i md-icon></i>Login
        </a>
        <a md-button routerLink="/subscription"  *ngIf="showMenu == false"  routerLinkActive="active">
                <i md-icon></i>Sign up
        </a>
 
      <button md-icon-button *ngIf="showMenu == true"  class="md-raised md-primary" (click)="right.toggle()">
          <md-icon  class="md-24">list</md-icon>  
    </button>
</md-toolbar>
 <md-sidenav-layout>
 
    <md-sidenav  #right align="end" layout-padding>
      <h5 style="text-align:center"><md-icon>account_circle</md-icon></h5>
      <h6 style="text-align:center"> {{login}}</h6>
      <hr>
          <div style="text-align:center">
            <button md-button  routerLink="/productManagement"  class="md-icon-button" style="width: 100%" (click)="right.close()" routerLinkActive="active">
                      <h5> Manage</h5>
              </button>
        </div>
       <div style="text-align:center">
            <button  md-button    class="md-icon-button"  style="width: 100%" (click)="logOut(right)" routerLinkActive="active">
                   <h5>Logout</h5>
            </button>
         </div>
    </md-sidenav>
    <div  layout-padding>
             <router-outlet></router-outlet>    
    </div>
  </md-sidenav-layout>
  • home.html

Through this page, user can see list of available products with details.
If user is connected, it will have possibility to remove some product form current list.

 

<div >
    <h1>{{title}}</h1>
    <div class="row">
    
        <md-progress-circle color="primary" mode="indeterminate"  *ngIf="showProgress" [style.width]="'40px'"  [style.margin]="'0 auto'" ></md-progress-circle>
        <div *ngIf="showEmpty" class="alert alert-info">
            <strong>Info :</strong> Empty Result
        </div>

        <div *ngIf="showError" class="alert alert-danger">
            <strong>Error :</strong> Problem happened in server side.
        </div>
    <div>
 
    <div class="row"  >
        <div  *ngFor="let obj of listProduct" class="col col-lg-4" style="margin-top:1px;  " > 
             <md-card >
                <md-card-title-group>
                    <img md-card-md-image  [src]="obj.picture">
                    <md-card-title>{{obj.title}}</md-card-title>
                    <md-card-subtitle>Price : {{obj.price}} $</md-card-subtitle>
                  
                </md-card-title-group>
                
                <md-card-actions *ngIf="isLogged == true" style="text-align: center">
                      <button md-icon-button  class="md-primary" (click)="Remove(obj.id)" >
                            <md-icon    class="md-24" >delete</md-icon>
                      </button>
                </md-card-actions>
            </md-card> 
            <div class="clearfix" ></div>
        </div>
    </div>
</div>
  • login.html

This page offers a connection form, that allow user to enter his credential (email and password)  and to open new session.

and to open new session. <div>
    <h1>{{title}}</h1>
    <div >
        <form>
            <div> 
                <label class="vSpace">  Login :</label>  <md-input type="email" name="login" [(ngModel)]="model.login" placeholder="Email" >  </md-input>
            </div>
            <div> 
                 <label class="vSpace"> Password : </label>  <md-input type="password" name="pwd" [(ngModel)]="model.pwd" placeholder="Password" >  </md-input>
            </div>
            <div> 
                <button type="button" md-raised-button   [disabled]="(model.login == '' || model.pwd =='')"  (click)="tryToConnect()"  class="md-primary" value="submit"  > 
                        submit
                </button>
            </div>
        </form>
    </div>
</div>
  • productManagement.html

This page offers user possibility to add new product through using a form.

He must provides information about : product Name (title), description, price, thumbnail (Picture).

 

<div>
    <h1>{{title}}</h1>
    <div >
        <form>
            <div> 
                <label class="vSpace">  Title :</label>  <md-input type="text" name="title" [(ngModel)]="model.title" placeholder="title">  </md-input>
            </div>
             <div> 
                <label class="vSpace">  Full description :</label>  <md-input type="text"name="fullDescription" [(ngModel)]="model.fullDescription" placeholder="Description">  </md-input>
            </div>
            <div> 
                 <label class="vSpace"> Price ($) : </label>  <md-input type="number" name="price" [(ngModel)]="model.price"  placeholder="Price" ></md-input>
            </div>
            <div> 
                <label class="vSpace"> Piture : </label>
                <label class="btn btn-default btn-file">
                    Load File <input type="file" id="file" name="pictureFile" (change)="onChange($event)" class="md-primary" class="hidden">
                </label>
            </div>
            <div> 
   
                <button type="button" md-raised-button  [disabled]="(model.title == '' || model.fullDescription =='')" (click)="addNewProduct()"   class="md-primary" value="submit"  > 
                        Add
                </button>
            </div>
        </form>
    </div>
</div>
  • singup.html

Through this page, any anonymous user can create their own account, to have the privileges to access back-office of application.

To create a new account, user should complete subscription form by filling following information : login, password.

 

<div>
    <h1>{{title}}</h1>
    <div>
          <form id="newProductForm" >
            <div> 
            <label class="vSpace">Login :</label> 
                 <md-input type="email"  name="login" [(ngModel)]="model.login" placeholder="Email" >  </md-input>
            </div>
            <div> 
            <label class="vSpace">Password :</label>
                 <md-input type="password" name="pwd"  [(ngModel)]="model.pwd" placeholder="Password" >  </md-input>
            </div>
            <div> 
            <label class="vSpace">Confirm password :</label> 
               <md-input type="password"  name="pwdConfirmation"  [(ngModel)]="model.pwdConfirmation" placeholder="Confirm password" >  </md-input>
            </div>
            <div> 
 
            <button md-raised-button type="button"  [disabled]="(model.login == '' || model.pwd =='' || model.pwdConfirmation != model.pwd)" (click)="Subscription()"  class="md-primary" value="submit"  > 
                            submit
            </button>
            </div>
        </form>
    </div>
</div>
 
 

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:

  • npm start : to transpile TS files to JavaScript files and start application.

when you try to open application via the given url (http://localhost:3000), you will get the below result : 

To get this above result, you should  start your server application ( see Part 1 of this article : Server side implementation). Make sure your web service is accessible from this address :  http://localhost:5000 

 

References

Points of Interest

I hope that you appreciated this series of articles. Try to download the source code, and i'm waiting for your questions and comments.

History

  • v1 29/01/2017 : Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

O.Nasri
Engineer
France France
Microsoft certified professional in C# ,HTML 5 & CSS3 and JavaScript, Asp.net, and Microsoft Azure.

You may also be interested in...

Pro
Pro

Comments and Discussions

 
QuestionTry creating project using Angular template in VS 2017 Pin
Larry @Datasmith6-Oct-17 8:52
memberLarry @Datasmith6-Oct-17 8:52 
AnswerRe: Try creating project using Angular template in VS 2017 Pin
O.Nasri6-Nov-17 8:51
professionalO.Nasri6-Nov-17 8:51 
QuestionCan i have one Info, please ? Pin
daniweb333-Feb-17 1:05
memberdaniweb333-Feb-17 1:05 
AnswerRe: Can i have one Info, please ? Pin
O.Nasri4-Feb-17 2:47
professionalO.Nasri4-Feb-17 2:47 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Permalink | Advertise | Privacy | Cookies | Terms of Use | Mobile
Web01 | 2.8.190417.4 | Last Updated 4 Feb 2017
Article Copyright 2017 by O.Nasri
Everything else Copyright © CodeProject, 1999-2019
Layout: fixed | fluid