Click here to Skip to main content
13,860,571 members
Click here to Skip to main content
Add your own
alternative version

Stats

32.7K views
477 downloads
27 bookmarked
Posted 25 Apr 2017
Licenced CPOL

Master Chef (Part 4) - ASP.NET Core and Angular 2 CRUD SPA

, 10 May 2017
Rate this:
Please Sign up or sign in to vote.
Inthis article, I talk about how to use ASP.NET Core MVC, Entity Framework Coreand Angular 2 to implement a CRUD SPA (Single Page Application).

Introduction

In Master Chef (part 1) and Master Chef (part 2),I introduced how to use ASP.Net Core and Angular JS. From Master Chef (part 3), I begin to introduce how to create ASP.NET Core and Angular 2 applications. In this article, I continuely talk about how to use ASP.NET Core MVC, Entity Framework Core and Angular 2 to implement a CRUD SPA (Single Page Application).

Server Data Model

Create, read, update and delete (as an acronym CRUD) are four basic functions of persistent storage.

We need to implement CRUD on database level in our repository class first. Add a basic Entity class.

    public class Entity
    {
        public virtual Guid Id { get; set; }

        public virtual Guid? ParentId { get; set; }
}

Then let Recipe, RecipeStep and RecipeItem inherit Entity class, and use Id and ParentId these generic names to replace the corresponding keys and references.

    public partial class Recipe : Entity
    {
        public Recipe()
        {
            RecipeSteps = new HashSet<RecipeStep>();
        }

        public string Name { get; set; }
        public DateTime ModifyDate { get; set; }
        public string Comments { get; set; }

        public virtual ICollection<RecipeStep> RecipeSteps { get; set; }
}

    public partial class RecipeStep : Entity
    {
        public RecipeStep()
        {
            RecipeItems = new HashSet<RecipeItem>();
        }

        public int StepNo { get; set; }
        public string Instructions { get; set; }

        public virtual ICollection<RecipeItem> RecipeItems { get; set; }
        [JsonIgnore]
        public Recipe Recipe { get; set; }
}
    public partial class RecipeItem : Entity
    {
        public string Name { get; set; }
        public decimal Quantity { get; set; }
        public string MeasurementUnit { get; set; }
        [JsonIgnore]
        public RecipeStep RecipeStep { get; set; }
}

Now we need change the DbContext class to apply Id and ParentId.

modelBuilder.Entity<RecipeItem>(entity =>
{
    entity.HasKey(e => e.Id)
        .HasName("PK_RecipeItems");

    entity.Property(e => e.Id).ValueGeneratedNever().HasColumnName("ItemId");
    entity.Property(e => e.ParentId).HasColumnName("RecipeStepId");
    entity.Property(e => e.MeasurementUnit)
        .IsRequired()
        .HasColumnType("varchar(20)");

    entity.Property(e => e.Name)
        .IsRequired()
        .HasColumnType("varchar(255)");

    entity.Property(e => e.Quantity).HasColumnType("decimal");

    entity.HasOne(d => d.RecipeStep)
        .WithMany(p => p.RecipeItems)
        .HasForeignKey(d=>d.ParentId)
        .OnDelete(DeleteBehavior.Cascade)
        .HasConstraintName("FK_RecipeItems_RecipeSteps");
});

For RecipeItem entity, we use "HasColumnName" to tell model builder the mappings, "Id" mapping to "ItemId" and "ParentId" mapping to "RecipeStepId". Then in reference definition, change HasForeignKey(d=>d.RecipeStepId) to HasForeignKey(d=>d.ParentId).

The same solution for RecipeStep:

modelBuilder.Entity<RecipeStep>(entity =>
{
    entity.HasKey(e => e.Id)
        .HasName("PK_RecipeSteps");

    entity.Property(e => e.Id).ValueGeneratedNever().HasColumnName("RecipeStepId");
    entity.Property(e => e.ParentId).HasColumnName("RecipeId");
    entity.Property(e => e.Instructions).HasColumnType("text");

    entity.HasOne(d => d.Recipe)
        .WithMany(p => p.RecipeSteps)
        .HasForeignKey(d => d.ParentId)
        .OnDelete(DeleteBehavior.Cascade)
        .HasConstraintName("FK_RecipeSteps_Recipes");
});

What’s DeleteBehavior.Cascade? This is the option that deletes children when you delete the parent object. For our case, removing a recipe will delete all recipe steps and recipe items of this recipe, and removing a step will delete all items of this step.

Recipe class doesn’t have ParentId. So we need tell model builder to ignore the mapping.

modelBuilder.Entity<Recipe>(entity =>
{
    entity.HasKey(e => e.Id)
        .HasName("PK_Recipes");
    entity.Ignore(e => e.ParentId);
    entity.Property(e => e.Id).ValueGeneratedNever().HasColumnName("RecipeId");

    entity.Property(e => e.Comments).HasColumnType("text");

    entity.Property(e => e.ModifyDate).HasColumnType("date");

    entity.Property(e => e.Name)
        .IsRequired()
        .HasColumnType("varchar(255)");
});

After applying these changes, now we can use generics in a repository class to implement, create, read, update and delete functionality for Recipe, RecipeStep and RecipeItem.

public T GetEntity<T>(Guid id) where T : Entity
{
    try
    {
        return _dbContext.Find<T>(id);
    }
    catch (Exception ex)
    {
        throw ex;
    }
}

public T AddEntity<T>(T entity) where T : Entity
{
    _dbContext.Add<T>(entity);
    _dbContext.SaveChanges();
    var result = GetEntity<T>(entity.Id);
    return result;
}

public void UpdateEntity<T>(T entity) where T : Entity
{
    _dbContext.Update<T>(entity);
    _dbContext.SaveChanges();
}

public void DeleteEntity<T>(Guid id) where T : Entity
{
    var entity = GetEntity<T>(id);
    _dbContext.Remove<T>(entity);
    _dbContext.SaveChanges();
}

Web API Controller

In the RecipesController class, we set up functions to deal with basic CRUD requests. We're getting a GET request here requesting all recipes. We have another Get function here that takes an id, so a user can request a specific recipe to return. And we also have some more functions here POST – allowing a user to create a new recipe. And also PUT – where we can update an existing recipe. And finally, DELETE – where a specific recipe can be deleted.

[HttpGet("{id}")]
public IActionResult Get(Guid id)
{
    var recipe = _repository.GetEntity<Recipe>(id);
    if (recipe != null)
        return new ObjectResult(recipe);
    else
        return new NotFoundResult();
}

[HttpPost]
public IActionResult Post([FromBody]Recipe recipe)
{
    if (recipe.Id == Guid.Empty)
    {
        recipe.Id = Guid.NewGuid();
        recipe.ModifyDate = DateTime.Now;
        return new ObjectResult(_repository.AddEntity<Recipe>(recipe));
    }
    else
    {
        var existingOne = _repository.GetEntity<Recipe>(recipe.Id);
        existingOne.Name = recipe.Name;
        existingOne.Comments = recipe.Comments;
        existingOne.ModifyDate = DateTime.Now;
        _repository.UpdateEntity<Recipe>(existingOne);
        return new ObjectResult(existingOne);
    }
}

[HttpPut("{id}")]
public IActionResult Put(Guid id, [FromBody]Recipe recipe)
{
    var existingOne = _repository.GetEntity<Recipe>(recipe.Id);
    existingOne.Name = recipe.Name;
    existingOne.Comments = recipe.Comments;
    _repository.UpdateEntity<Recipe>(existingOne);
    return new ObjectResult(existingOne);
}

[HttpDelete("{id}")]
public IActionResult Delete(Guid id)
{
    _repository.DeleteEntity<Recipe>(id);
    return new StatusCodeResult(200);
}

But how about RecipeStep and RecipeItem? Can we put different HttpGet, HttpPost and HttpDelete in one API controller?

Routing is how Web API matches a URI to an action. Web API 2 supports a new type of routing, called attribute routing. As the name implies, attribute routing uses attributes to define routes. Attribute routing gives you more control over the URIs in your web API. For example, you can easily create URIs that describe hierarchies of resources.

So now we use attribute routing to define multiple HTTPGet, HTTPPost and HTTPDelete in one API controller.

//GET api/recipes/step/:id
[HttpGet]
[Route("step/{id}")]
public IActionResult GetStep(Guid id)
{
    var recipeStep = _repository.GetEntity<RecipeStep>(id);
    if (recipeStep != null)
        return new ObjectResult(recipeStep);
    else
        return new NotFoundResult();

}

//POST api/recipes/step
[HttpPost]
[Route("step")]
public IActionResult UpdateStep([FromBody]RecipeStep recipeStep)
{
    if (recipeStep.Id == Guid.Empty)
    {
        recipeStep.Id = Guid.NewGuid();
        return new ObjectResult(_repository.AddEntity<RecipeStep>(recipeStep));
    }
    else
    {
        var existingOne = _repository.GetEntity<RecipeStep>(recipeStep.Id);
        existingOne.StepNo = recipeStep.StepNo;
        existingOne.Instructions = recipeStep.Instructions;
        _repository.UpdateEntity<RecipeStep>(existingOne);
        return new ObjectResult(existingOne);
    }
}

//DELETE api/recipes/step/:id
[HttpDelete]
[Route("step/{id}")]
public IActionResult DeleteStep(Guid id)
{
    _repository.DeleteEntity<RecipeStep>(id);
    return new StatusCodeResult(200);
}

// GET api/recipes/item/:id
[HttpGet]
[Route("item/{id}")]
public IActionResult GetItem(Guid id)
{
    var recipeItem = _repository.GetEntity<RecipeItem>(id);
    if (recipeItem != null)
        return new ObjectResult(recipeItem);
    else
        return new NotFoundResult();

}

//POST api/recipes/item
[HttpPost]
[Route("item")]
public IActionResult UpdateItem([FromBody]RecipeItem recipeItem)
{
    if (recipeItem.Id == Guid.Empty)
    {
        recipeItem.Id = Guid.NewGuid();
        if (recipeItem.MeasurementUnit == null)
            recipeItem.MeasurementUnit = "";
        return new ObjectResult(_repository.AddEntity<RecipeItem>(recipeItem));
    }
    else
    {
        var existingOne = _repository.GetEntity<RecipeItem>(recipeItem.Id);
        existingOne.Name = recipeItem.Name;
        existingOne.Quantity = recipeItem.Quantity;
        existingOne.MeasurementUnit = recipeItem.MeasurementUnit;
        _repository.UpdateEntity<RecipeItem>(existingOne);
        return new ObjectResult(existingOne);
    }
}

//DELETE api/recipes/item/:id
[HttpDelete]
[Route("item/{id}")]
public IActionResult DeleteItem(Guid id)
{
    _repository.DeleteEntity<RecipeItem>(id);
    return new StatusCodeResult(200);
}

Client View Models

In last article, we have created a recipe view model. Now we continually create recipestep and recipeitem.

Right click "viewmodels" to add new type script file. It’s named "recipeStep," which is a recipe step view model we use to display on the view.

export class RecipeStep {
    public parentId: string;
    public id: string;
    public stepNo: number;
    public instructions: string; 
    constructor() { }
}

Right click "viewmodels" to add another type script file. It’s named "recipeItem," which is a recipe item view model we use to display on the view.

Client Side Service

In our client side service "app.service.ts," we need add more methods to do the CRUD functionality.

Import client view model classes first.

import { Recipe } from "../viewmodels/recipe";
import { RecipeStep } from "../viewmodels/recipeStep";
import { RecipeItem } from "../viewmodels/recipeItem";
import { Observable } from "rxjs/Observable";

Please note as we implemented in web API controller, URL is different for recipe, step and item.

So in service class, we define three constant URL strings.

    //URL to web api
    private recipeUrl = 'api/recipes/';
    private stepUrl = 'api/recipes/step/';
private itemUrl = 'api/recipes/item/';

Get, update, and delete recipe methods:

    getRecipe(id: string) {
        if (id == null) throw new Error("id is required.");
        var url = this.recipeUrl + id;
        return this.http.get(url)
            .map(response => <Recipe>response.json())
            .catch(this.handleError);
    }

    saveRecipe(recipe: Recipe) {
        if (recipe == null) throw new Error("recipe is required.");
        var url = this.recipeUrl;
        return this.http.post(url, recipe)
            .map(response => <Recipe>response.json())
            .catch(this.handleError);
    }

    deleteRecipe(id:string) {
        if (id == null) throw new Error("id is required.");
        var url = this.recipeUrl + id;
        return this.http.delete(url)
            .catch(this.handleError);
}

Get, update and delete recipe step methods:

    getStep(id: string) {
        if (id == null) throw new Error("id is required.");
        var url = this.stepUrl + id;
        return this.http.get(url)
            .map(response => <RecipeStep>response.json())
            .catch(this.handleError);
    }

    saveStep(step: RecipeStep) {
        if (step == null) throw new Error("recipe step is required.");
        var url = this.stepUrl;
        return this.http.post(url, step)
            .map(response => <RecipeStep>response.json())
            .catch(this.handleError);
    }

    deleteStep(id: string) {
        if (id == null) throw new Error("id is required.");
        var url = this.stepUrl + id;
        return this.http.delete(url)
            .catch(this.handleError);
}

Get, update and delete recipe item methods:
    getItem(id: string) {
        if (id == null) throw new Error("id is required.");
        var url = this.itemUrl + id;
        return this.http.get(url)
            .map(response => <RecipeItem>response.json())
            .catch(this.handleError);
    }

    saveItem(item: RecipeItem) {
        if (item == null) throw new Error("recipe item is required.");
        var url = this.itemUrl;
        return this.http.post(url, item)
            .map(response => <RecipeItem>response.json())
            .catch(this.handleError);
    }

    deleteItem(id: string) {
        if (id == null) throw new Error("id is required.");
        var url = this.itemUrl + id;
        return this.http.delete(url)
            .catch(this.handleError);
}

Client Side Routing

Using MVC in ASP.NET, you used routing whenever you specified what controller you expected your code to hit when you specified a particular URL. We also had the option of specifying parameters that we wanted to have passed into our controller methods. That’s the server side routing.

In a SPA, client side routing does essentially the same thing. The only difference is that we never have to call the server. This makes all our "Pages" virtual. Instead of requiring that our visitors always start at our home page and navigate into the rest of our site; instead of creating a separate page on the server for each page in our site; we can load all the site up front and the user can navigate to exactly the page they want to be at. They can even link directly to that page and the client side will handle displaying the page appropriately.

Typically routes get enabled at the top of your application after all the common code has been implemented. So, in the location where you want the routing to take effect, add the following tag:

<router-outlet></router-outlet>

App Component

Now we change our App Component to enable client side routing to implement Single Page Application.

import { Component, OnInit } from "@angular/core";
import { Router } from "@angular/router";
import { Recipe } from "./viewmodels/recipe";
import { AppService } from "./services/app.service";

@Component({
    selector: 'masterchef2',
    template: `
        <h1>{{title}}</h1>
        <router-outlet></router-outlet>
    `
})

export class AppComponent {
   title = "Master Chef Recipes";
}

You can see now App Component is very simple. Only display a title. <router-outlet></router-outlet> will bring different templates according to the path.

Recipe List Component

In the last article, we put recipe list in app component. Because we need to implement more complicated functionalities, I take it out from app component.

Right click "scripts/app/components" folder, add a new item. Select ".Net Core/Client-Side" TypeScript File. Give the name "recipe-list.component.ts".

import { Recipe } from "../viewmodels/recipe";
import { AppService } from "../services/app.service";

@Component({
    selector: 'recipe-list',
    templateUrl: '../partials/recipes.html'
})

export class RecipeListComponent implements OnInit {

    items: Recipe[];
    errorMessage: string;

    constructor(private appService: AppService) {
        //called first time before the ngOnInit()
    }

    ngOnInit() {
        //called after the constructor and called  after the first ngOnChanges()
        var service = this.appService.getAllRecipes();
        service.subscribe(
            items => {
                this.items = items;
            },
            error => this.errorMessage = <any>error
        );
    }

    public Expand(recipe:Recipe) {
        recipe.show = !recipe.show;
    }

}

Please note the change in the Expand method. Now the "show" property is not in the component level anymore. It’s moved to Recipe view model. That is because I’d like to control each recipe, not all recipes.

Recipe Detail Component

Right click the "scripts/app/components" folder, and add a new item. Select ".Net Core/Client-Side" TypeScript File. Give the name "recipe-detail.component.ts".

import { Component, OnInit, OnDestroy } from "@angular/core";
import { Router, ActivatedRoute } from "@angular/router";  
import { Recipe } from "../viewmodels/recipe";
import { AppService } from "../services/app.service";

@Component({
    selector: 'recipe-detail',
    templateUrl: '../partials/edit.html'
})

export class RecipeDetailComponent implements OnInit {
    item: Recipe;
    sub: any;

    constructor(private AppService: AppService, private router: Router, private route: ActivatedRoute) { }

    ngOnInit() {
        this.sub = this.route.params.subscribe(params => {
            var id = params['id'];
            this.AppService.getRecipe(id).subscribe(item => this.item = item);
        });
    }

    ngOnDestroy() {
        this.sub.unsubscribe();
    }

    public editRecipe() {
        this.AppService.saveRecipe(this.item).subscribe(
            item => { this.item = item; this.router.navigate(['/recipes']); },
            error => console.log(error)
        )
    }
}

In this class, we call the getRecipe service function to get recipe information first, then call the saveRecipe service function to update recipe.

Recipe New Component

Right click the "scripts/app/components" folder, add a new item. Select ".Net Core/Client-Side" TypeScript File. Give the name "recipe-new.component.ts".

import { Component, OnInit, OnDestroy } from "@angular/core";
import { Router, ActivatedRoute } from "@angular/router";
import { Recipe } from "../viewmodels/recipe";
import { AppService } from "../services/app.service";

@Component({
    selector: 'recipe-new',
    templateUrl: '../partials/add.html'
})

export class RecipeNewComponent implements OnInit {
    item: Recipe;
    sub: any;

    constructor(private AppService: AppService, private router: Router, private route: ActivatedRoute) { }

    ngOnInit() {
        this.item = new Recipe();
    }

    ngOnDestroy() {
    }

    public addRecipe() {
        this.AppService.saveRecipe(this.item).subscribe(
            item => { this.item = item; this.router.navigate(['/recipes']); },
            error => console.log(error)
        )
    }

}

In this class, we create a new recipe first, then call the saveRecipe service function to add recipe.

Recipe Delete Component

Right click the "scripts/app/components" folder, and add a new item. Select ".Net Core/Client-Side" TypeScript File. Give the name "recipe-delete.component.ts".

import { Component, OnInit, OnDestroy } from "@angular/core";
import { Router, ActivatedRoute } from "@angular/router";  
import { Recipe } from "../viewmodels/recipe";
import { AppService } from "../services/app.service";

@Component({
    selector: 'recipe-delete',
    templateUrl: '../partials/delete.html'
})

export class RecipeDeleteComponent implements OnInit {
    item: Recipe;
    sub: any;

    constructor(private AppService: AppService, private router: Router, private route: ActivatedRoute) { }

    ngOnInit() {
        this.sub = this.route.params.subscribe(params => {
            var id = params['id'];
            this.AppService.getRecipe(id).subscribe(item => this.item = item);
        });
    }

    ngOnDestroy() {
        this.sub.unsubscribe();
    }

    public deleteRecipe() {
        this.AppService.deleteRecipe(this.item.id).subscribe(
            () => this.router.navigate(['/recipes']),
            error => console.log(error)
        )
    }

}

In this class, we call the getRecipe service function to get recipe information first, then call the deleteRecipe service function to delete the recipe.

Step Detail Component

Right click the "scripts/app/components" folder, add a new item. Select ".Net Core/Client-Side" TypeScript File. Give the name "step-detail.component.ts".

import { Component, OnInit, OnDestroy } from "@angular/core";
import { Router, ActivatedRoute } from "@angular/router";  
import { RecipeStep } from "../viewmodels/recipestep";
import { AppService } from "../services/app.service";

@Component({
    selector: 'step-detail',
    templateUrl: '../partials/editStep.html'
})

export class StepDetailComponent implements OnInit {
    item: RecipeStep;
    sub: any;

    constructor(private AppService: AppService, private router: Router, private route: ActivatedRoute) { }

    ngOnInit() {
        this.sub = this.route.params.subscribe(params => {
            var id = params['id'];
            this.AppService.getStep(id).subscribe(item => this.item = item);
        });
    }

    ngOnDestroy() {
        this.sub.unsubscribe();
    }

    public editRecipeStep() {
        this.AppService.saveStep(this.item).subscribe(
            item => { this.item = item; this.router.navigate(['/recipes']); },
            error => console.log(error)
        )
    }

}

In this class, we call the getStep service function to get recipe step information first, then call saveStep service function to update recipe step.

Step New Component

Right click the "scripts/app/components" folder, and add a new item. Select ".Net Core/Client-Side" TypeScript File. Give the name "step-new.component.ts".

import { Component, OnInit, OnDestroy } from "@angular/core";
import { Router, ActivatedRoute } from "@angular/router";  
import { RecipeStep } from "../viewmodels/recipeStep";
import { AppService } from "../services/app.service";

@Component({
    selector: 'step-new',
    templateUrl: '../partials/addStep.html'
})

export class StepNewComponent implements OnInit {
    item: RecipeStep;
    sub: any;

    constructor(private AppService: AppService, private router: Router, private route: ActivatedRoute) { }

    ngOnInit() {
        this.sub = this.route.params.subscribe(params => {
            var parentId = params['id'];
            this.item = new RecipeStep();
            this.item.parentId = parentId;
        });
    }

    ngOnDestroy() {
        this.sub.unsubscribe();
    }

    public addRecipeStep() {
        this.AppService.saveStep(this.item).subscribe(
            item => { this.item = item; this.router.navigate(['/recipes']);},
            error => console.log(error)
        )
    }

}

In this class, we create a new step first, then call the saveStep service function to add a recipe step.

Step Delete Component

Right click the "scripts/app/components" folder, and add a new item. Select ".Net Core/Client-Side" TypeScript File. Give the name "step-delete.component.ts".

import { Component, OnInit, OnDestroy } from "@angular/core";
import { Router, ActivatedRoute } from "@angular/router";  
import { RecipeStep } from "../viewmodels/recipeStep";
import { AppService } from "../services/app.service";

@Component({
    selector: 'step-delete',
    templateUrl: '../partials/deleteStep.html'
})

export class StepDeleteComponent implements OnInit {
    item: RecipeStep;
    sub: any;

    constructor(private AppService: AppService, private router: Router, private route: ActivatedRoute) { }

    ngOnInit() {
        this.sub = this.route.params.subscribe(params => {
            var id = params['id'];
            this.AppService.getStep(id).subscribe(item => this.item = item);
        });
    }

    ngOnDestroy() {
        this.sub.unsubscribe();
    }

    public deleteStep() {
        this.AppService.deleteStep(this.item.id).subscribe(
            () => this.router.navigate(['/recipes']),
            error => console.log(error)
        )
    }

}

In this class, we call the getStep service function to get recipe step information first, then call the deleteStep service function to delete recipe step.

Item Detail Component

Right click the "scripts/app/components" folder, and add a new item. Select ".Net Core/Client-Side" TypeScript File. Give the name "item-detail.component.ts".

import { Component, OnInit, OnDestroy } from "@angular/core";
import { Router, ActivatedRoute } from "@angular/router";  
import { RecipeItem } from "../viewmodels/recipeitem";
import { AppService } from "../services/app.service";

@Component({
    selector: 'item-detail',
    templateUrl: '../partials/editItem.html'
})

export class ItemDetailComponent implements OnInit {
    item: RecipeItem;
    sub: any;

    constructor(private AppService: AppService, private router: Router, private route: ActivatedRoute) { }

    ngOnInit() {
        this.sub = this.route.params.subscribe(params => {
            var id = params['id'];
            this.AppService.getItem(id).subscribe(item => this.item = item);
        });
    }

    ngOnDestroy() {
        this.sub.unsubscribe();
    }

    public editRecipeItem() {
        this.AppService.saveItem(this.item).subscribe(
            item => { this.item = item; this.router.navigate(['/recipes']); },
            error => console.log(error)
        )
    }

}

In this class, we call the getItem service function to get recipe item information first, then call saveItem service function to update recipe item.

Item New Component

Right click the "scripts/app/components" folder, and add a new item. Select ".Net Core/Client-Side" TypeScript File. Give the name "item-new.component.ts".

import { Component, OnInit, OnDestroy } from "@angular/core";
import { Router, ActivatedRoute } from "@angular/router";  
import { RecipeItem } from "../viewmodels/recipeItem";
import { AppService } from "../services/app.service";

@Component({
    selector: 'item-new',
    templateUrl: '../partials/addItem.html'
})

export class ItemNewComponent implements OnInit {
    item: RecipeItem;
    sub: any;

    constructor(private AppService: AppService, private router: Router, private route: ActivatedRoute) { }

    ngOnInit() {
        this.sub = this.route.params.subscribe(params => {
            var parentId = params['id'];
            this.item = new RecipeItem();
            this.item.parentId = parentId;
        });
    }

    ngOnDestroy() {
        this.sub.unsubscribe();
    }

    public addRecipeItem() {
        this.AppService.saveItem(this.item).subscribe(
            item => { this.item = item; this.router.navigate(['/recipes']);},
            error => console.log(error)
        )
    }

}

In this class, we create a new item first, then call the saveItem service function to add a recipe item.

Item Delete Component

Right click "scripts/app/components" folder, add a new item. Select ".Net Core/Client-Side" TypeScript File. Give the name "item-delete.component.ts".

import { Component, OnInit, OnDestroy } from "@angular/core";
import { Router, ActivatedRoute } from "@angular/router";  
import { RecipeItem } from "../viewmodels/recipeItem";
import { AppService } from "../services/app.service";

@Component({
    selector: 'item-delete',
    templateUrl: '../partials/deleteItem.html'
})

export class ItemDeleteComponent implements OnInit {
    item: RecipeItem;
    sub: any;

    constructor(private AppService: AppService, private router: Router, private route: ActivatedRoute) { }

    ngOnInit() {
        this.sub = this.route.params.subscribe(params => {
            var id = params['id'];
            this.AppService.getItem(id).subscribe(item => this.item = item);
        });
    }

    ngOnDestroy() {
        this.sub.unsubscribe();
    }

    public deleteItem() {
        this.AppService.deleteItem(this.item.id).subscribe(
            () => this.router.navigate(['/recipes']),
            error => console.log(error)
        )
    }

}

In this class, we call the getItem service function to get the recipe step information first, then call deleteItem service function to delete the recipe item.

Change App Module

An Angular module class describes how the application parts fit together. Every application has at least one Angular module, the root module that you bootstrap to launch the application. You can call it anything you want. So we load all components created.

///<reference path="../../typings/index.d.ts"/>
import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { HttpModule } from "@angular/http";
import { RouterModule } from "@angular/router";
import { FormsModule } from "@angular/forms";
import "rxjs/Rx";

import { AppComponent } from "./app.component";
import { RecipeListComponent } from "./components/recipe-list.component";
import { RecipeDetailComponent } from "./components/recipe-detail.component";
import { RecipeNewComponent } from "./components/recipe-new.component";
import { RecipeDeleteComponent } from "./components/recipe-delete.component";
import { StepDetailComponent } from "./components/step-detail.component";
import { StepNewComponent } from "./components/step-new.component";
import { StepDeleteComponent } from "./components/step-delete.component";
import { ItemDetailComponent } from "./components/item-detail.component";
import { ItemNewComponent } from "./components/item-new.component";
import { ItemDeleteComponent } from "./components/item-delete.component";

import { AppRouting } from "./app.routing";
import { AppService } from "./services/app.service";

@NgModule({
    // directives, components, and pipes
    declarations: [
        AppComponent,
        RecipeListComponent,
        RecipeDetailComponent,
        RecipeNewComponent,
        RecipeDeleteComponent,
        StepDetailComponent,
        StepNewComponent,
        StepDeleteComponent,
        ItemDetailComponent,
        ItemNewComponent,
        ItemDeleteComponent,
    ],
    // modules
    imports: [
        BrowserModule,
        HttpModule,
        FormsModule,
        RouterModule,
        AppRouting

    ],
    // providers
    providers: [
        AppService
    ],
    bootstrap: [
        AppComponent
    ]
})
export class AppModule { }

Also, we import route module here. Then we can do a route configuration.

Client Route Configuration

A routed Angular application has one singleton instance of the Router service. When the browser's URL changes, that router looks for a corresponding Route from which it can determine the component to display.

A router has no routes until you configure it. We configure our client routing in app.routing.ts.

Right click the "scripts/app/components" folder, and add a new item. Select ".Net Core/Client-Side" TypeScript File. Give the name "app.routing.ts".

import { ModuleWithProviders } from "@angular/core";
import { Routes, RouterModule } from "@angular/router";
import { RecipeListComponent } from "./components/recipe-list.component";
import { RecipeDetailComponent } from "./components/recipe-detail.component";
import { RecipeNewComponent } from "./components/recipe-new.component";
import { RecipeDeleteComponent } from "./components/recipe-delete.component";
import { StepDetailComponent } from "./components/step-detail.component";
import { StepNewComponent } from "./components/step-new.component";
import { StepDeleteComponent } from "./components/step-delete.component";
import { ItemDetailComponent } from "./components/item-detail.component";
import { ItemNewComponent } from "./components/item-new.component";
import { ItemDeleteComponent } from "./components/item-delete.component";

const routes: Routes = [
    {
        path: '',
        redirectTo: '/recipes',
        pathMatch: 'full'
    },
    {
        path: 'recipes',
        component: RecipeListComponent
    },
    {
        path: 'recipes/edit/:id',
        component: RecipeDetailComponent
    },
    {
        path: 'recipes/add',
        component: RecipeNewComponent
    },
    {
        path: 'recipes/delete/:id',
        component: RecipeDeleteComponent
    },
    {
        path: 'recipes/editStep/:id',
        component: StepDetailComponent
    },
    {
        path: 'recipes/addStep/:id',
        component: StepNewComponent
    },
    {
        path: 'recipes/deleteStep/:id',
        component: StepDeleteComponent
    },
    {
        path: 'recipes/editItem/:id',
        component: ItemDetailComponent
    },
    {
        path: 'recipes/addItem/:id',
        component: ItemNewComponent
    },
    {
        path: 'recipes/deleteItem/:id',
        component: ItemDeleteComponent
    },
];

export const AppRoutingProviders: any[] = [
];

export const AppRouting: ModuleWithProviders = RouterModule.forRoot(routes);

Here we configure all the paths and components in an array, then app module imports this array.

Recipe List Template (Repcipes.html)

<div>
    <a routerLink="/recipes/add" class="btn breadcrumb m-2">create a new recipe</a>
    <div *ngFor="let recipe of items">
        <div class="btn-group tab-pane mb-2">
            <button class="btn-info pull-left" (click)="Expand(recipe)"><h5>{{recipe.name}} - {{recipe.comments}}</h5></button>
        </div>
        <div class="btn-group">
            <a routerLink="/recipes/edit/{{recipe.id}}" class="breadcrumb-item">edit</a>
            <a routerLink="/recipes/delete/{{recipe.id}}" class="breadcrumb-item">delete</a>
        </div>
        <div *ngIf="recipe.show">
            <a routerLink="/recipes/addStep/{{recipe.id}}" class="btn breadcrumb m-2">create a new step</a>
            <div *ngFor="let step of recipe.recipeSteps">
                <div class="row ml-2">
                    <div class="breadcrumb ml-2">
                        <span>step {{step.stepNo}} : {{step.instructions}}</span>
                    </div>
                    <div class="btn-group m-2">
                        <a routerLink="/recipes/editStep/{{step.id}}" class="breadcrumb-item">edit</a>
                        <a routerLink="/recipes/deleteStep/{{step.id}}" class="breadcrumb-item">delete</a>
                    </div>
                </div>
                <a routerLink="/recipes/addItem/{{step.id}}" class="btn breadcrumb ml-4">create a new item</a>
                <div *ngFor="let item of step.recipeItems">
                    <div class="row ml-4">
                        <div class="card-text ml-4">
                            <p> {{item.name}}  {{item.quantity}} {{item.measurementUnit}}</p>
                        </div>
                        <div class="btn-group ml-2">
                            <a routerLink="/recipes/editItem/{{item.id}}" class="breadcrumb-item">edit</a>
                            <a routerLink="/recipes/deleteItem/{{item.id}}" class="breadcrumb-item">delete</a>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>

The RouterLink directives on the anchor tags give the router control over those elements. The navigation paths are fixed, so you can assign a string to the routerLink (a "one-time" binding).

Had the navigation path been more dynamic, you could have bound to a template expression that returned an array of route link parameters (the link parameters array). The router resolves that array into a complete URL.

In the Recipe List template we have both fixed link and dynamic link. I use ngIf="recipe.show" to expand or collapse the corresponding recipe. One thing I have to mention, for all edit and delete functions we pass the object id, but create a new step and new item, we need to pass the parent object id. That means creating a new step, we need pass recipe id; create a new item, then we need pass step id. Obviously we don’t need pass anything to create a new recipe.

Recipe Detail Template (edit.html)

Right click the "wwwroot/partials" folder, and add a new item. Select ".Net Core/Client-Side" HTML Page. Give the name "edit.html".

<div class="badge badge-info">
    <h4>Edit Recipe</h4>
</div>
<div *ngIf="item" class="card-text">
    <form  (ngSubmit)="editRecipe()">
        <div class="row">
            <div class="col-xl-6 form-group">
                <label for="name">Name</label>
                <input [(ngModel)]="item.name" name="name" type="text" class="form-control" />
            </div>
        </div>
        <div class="row">
            <div class="col-xl-6 form-group">
                <label for="comments">Comments</label>
                <input [(ngModel)]="item.comments" name="comments" type="text" class="form-control" />
            </div>
        </div>
        <div class="row m-2">
            <button type="submit" class="btn btn-primary">Save</button>
            <a routerLink="/recipes" class="btn btn-default">Cancel</a>
        </div>
    </form>
</div>

Recipe Details Template actually is a submit form. However, ngSubmit ensures that the form doesn’t submit when the handler code throws (which is the default behaviour of submit) and causes an actual http post request.

In order to register form controls, we use the ngModel directive. In combination with a name attribute, ngModel creates a form control abstraction for us behind the scenes. Every form control that is registered with ngModel will automatically show up in form.value and can then easily be used for further post processing.

In this template, ngSubmit binding with eidtRecipe() method in Recipe Detail Component. "Cancel" button just comes back to the list.

Recipe New Template (add.html)

Right click "wwwroot/partials" folder, add a new item. Select ".Net Core/Client-Side" HTML Page. Give the name "add.html".

<div class="badge badge-info">
    <h4>Add Recipe</h4>
</div>
<div *ngIf="item" class="card-text">
    <form (ngSubmit)="addRecipe()">
        <div class="row">
            <div class="col-xl-6 form-group">
                <label for="name">Name</label>
                <input [(ngModel)]="item.name" name="name" type="text" class="form-control" />
            </div>
        </div>
        <div class="row">
            <div class="col-xl-6 form-group">
                <label for="comments">Comments</label>
                <input [(ngModel)]="item.comments" name="comments" type="text" class="form-control" />
            </div>
        </div>
        <div class="row m-2">
            <button type="submit" class="btn btn-primary">Save</button>
            <a routerLink="/recipes" class="btn btn-default">Cancel</a>
        </div>
    </form>
</div>

In this template, ngSubmit binding with addRecipe() method in Recipe New Component. The "Cancel" button just comes back to the list.

Recipe Delete Template (delete.html)

Right click the "wwwroot/partials" folder, and add a new item. Select ".Net Core/Client-Side" HTML Page. Give the name "delete.html".

<div *ngIf="item">

    <div class="row">
        <div class="alert alert-warning">
            <p>Do you really want to delete this recipe?</p>
            <p> {{item.name}} - {{item.comments}}</p>
        </div>
    </div>
    <button (click)="deleteRecipe()" class="btn btn-danger">Yes</button>
    <a routerLink="/recipes" class="btn btn-default">No</a>

</div>

Recipe Delete template is not a submit form. The "Yes" button calls deleteRecipe() of Recipe Delete Component directly. The "No" button navigates back to recipe list.

Step Detail Template (editStep.html)

Right click the "wwwroot/partials" folder, and add a new item. Select ".Net Core/Client-Side" HTML Page. Give the name "editStep.html".

<div class="badge badge-info">
    <h4>Edit Recipe Step</h4>
</div>
<div *ngIf="item" class="card-text">
    <form (ngSubmit)="editRecipeStep()">
        <div class="row">
            <div class="col-xl-6 form-group">
                <label for="stepNo">Step No.</label>
                <input [(ngModel)]="item.stepNo" name="stepNo" type="text" class="form-control" />
            </div>
        </div>

        <div class="row">
            <div class="col-xl-6 form-group">
                <label for="instructions">Instructions</label>
                <input [(ngModel)]="item.instructions" name="instructions" type="text" class="form-control" />
            </div>
        </div>
        <div class="row m-2">
            <button type="submit" class="btn btn-primary">Save</button>
            <a routerLink="/recipes" class="btn btn-default">Cancel</a>
        </div>
    </form>
</div>

In this template, ngSubmit is binding with editRecipeStep() method in Step Detail Component. The "Cancel" button just comes back to the list.

Step New Template (addStep.html)

Right click the "wwwroot/partials" folder, and add a new item. Select ".Net Core/Client-Side" HTML Page. Give the name "addStep.html".

<div class="badge badge-info">
    <h4>Add a new recipe Step</h4>
</div>
<div *ngIf="item" class="card-text">
    <form (ngSubmit)="addRecipeStep()">
        <div class="row">
            <div class="col-xl-6 form-group">
                <label for="stepNo">Step No.</label>
                <input [(ngModel)]="item.stepNo" name="stepNo" type="text" class="form-control" />
            </div>
        </div>

        <div class="row">
            <div class="col-xl-6 form-group">
                <label for="instructions">Instructions</label>
                <input [(ngModel)]="item.instructions" name="instructions" type="text" class="form-control" />
            </div>
        </div>
        <div class="row m-2">
            <button type="submit" class="btn btn-primary">Save</button>
            <a routerLink="/recipes" class="btn btn-default">Cancel</a>
        </div>
    </form>
</div>

In this template, ngSubmit is binding with addRecipeStep() method in Step New Component. "Cancel" button just comes back to the list.

Step Delete Template (deleteStep.html)

Right click the "wwwroot/partials" folder, and add a new item. Select ".Net Core/Client-Side" HTML Page. Give the name "deleteStep.html".

<div *ngIf="item">

    <div class="row">
        <div class="alert alert-warning">
            <p>Do you really want to delete this recipe step?</p>
            <p>Step {{item.stepNo}} - {{item.instructions}}</p>
        </div>
    </div>
    <button (click)="deleteStep()" class="btn btn-danger">Yes</button>
    <a routerLink="/recipes" class="btn btn-default">No</a>

</div>

The Step Delete template is not a submit form. The "Yes" button calls deleteStep() of Step Delete Component directly. The "No" button navigates back to recipe list.

Item Detail Template (editItem.html)

Right click the "wwwroot/partials" folder, and add a new item. Select ".Net Core/Client-Side" HTML Page. Give the name "editItem.html".

<div class="badge badge-info">
    <h4>Edit Recipe Item</h4>
</div>
<div *ngIf="item" class="card-text">
    <form (ngSubmit)="editRecipeItem()">
        <div class="row">
            <div class="col-xl-6 form-group">
                <label for="name">Name</label>
                <input [(ngModel)]="item.name" name="name" type="text" class="form-control" />
            </div>
        </div>
        <div class="row">
            <div class="col-xl-6 form-group">
                <label for="quantity">Quantity</label>
                <input [(ngModel)]="item.quantity" name="quantity" type="text" class="form-control" />
            </div>
        </div>
        <div class="row">
            <div class="col-xl-6 form-group">
                <label for="measurementUnit">Measurement Unit</label>
                <input [(ngModel)]="item.measurementUnit" name="measurementUnit" type="text" class="form-control" />
            </div>
        </div>
        <div class="row m-2">
            <button type="submit" class="btn btn-primary">Save</button>
            <a routerLink="/recipes" class="btn btn-default">Cancel</a>
        </div>
    </form>
</div>

In this template, ngSubmit is binding with the editRecipeItem() method in Item Detail Component. The "Cancel" button just comes back to the list.

Item New Template (addItem.html)

Right click the "wwwroot/partials" folder, and add a new item. Select ".Net Core/Client-Side" HTML Page. Give the name "addItem.html".

<div class="badge badge-info">
    <h4>Add a new recipe Item</h4>
</div>
<div *ngIf="item" class="container-fluid">
    <form (ngSubmit)="addRecipeItem()">
        <div class="row">
            <div class="col-xl-6 form-group">
                <label for="name">Name</label>
                <input [(ngModel)]="item.name" name="name" type="text" class="form-control" />
            </div>
        </div>
        <div class="row">
            <div class="col-xl-6 form-group">
                <label for="quantity">Quantity</label>
                <input [(ngModel)]="item.quantity" name="quantity" type="text" class="form-control" />
            </div>
        </div>
        <div class="row">
            <div class="col-xl-6 form-group">
                <label for="measurementUnit">Measurement Unit</label>
                <input [(ngModel)]="item.measurementUnit" name="measurementUnit" type="text" class="form-control" />
            </div>
        </div>
        <div class="row m-2">
            <button type="submit" class="btn btn-primary">Save</button>
            <a href="/" class="btn btn-default">Cancel</a>
        </div>
    </form>
</div>

In this template, ngSubmit is binding with the addRecipeItem() method in Item New Component. The "Cancel" button just comes back to the list.

Item Delete Template (deleteItem.html)

Right click the "wwwroot/partials" folder, and add a new item. Select ".Net Core/Client-Side" HTML Page. Give the name "deleteItem.html".

<div *ngIf="item">
    <div class="row">
        <div class="alert alert-warning">
            <p>Do you really want to delete this recipe item?</p>
            <p> {{item.name}}  {{item.quantity}} {{item.measurementUnit}}</p>
        </div>
    </div>
    <button (click)="deleteItem()" class="btn btn-danger">Yes</button>
    <a routerLink="/recipes" class="btn btn-default">No</a>
</div>

Item Delete template is not a submit form. "Yes" button calls deleteItem() of Item Delete Component directly. The "No" button navigates back to the recipe list.

Adding the base tag

We need to set the base tag as it will tell the routing engine how to compose all of the upcoming navigation URLs our app will eventually have.

We add the base tag in our index.html, which is under the wwwroot folder.

<html>
<head>
    <base href="/">
    <title>Master Chef2</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <!-- Step 1. Load libraries -->
    <!-- Polyfill(s) for older browsers -->
    <script src="js/shim.min.js"></script>
    <script src="js/zone.js"></script>
    <script src="js/Reflect.js"></script>
    <script src="js/system.src.js"></script>

    <!-- Angular2 Native Directives -->
    <script src="/js/moment.js"></script>

    <!-- Step 2. Configure SystemJS -->
    <script src="systemjs.config.js"></script>
    <script>
        System.import('app').catch(function (err) { console.error(err); });
    </script>
    <link href="lib/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet" media="screen">
</head>
<!-- Step 3. Display the application -->
<body>
    <div class="container-fluid">
        <!-- Application PlaceHolder -->
        <masterchef2>Please wait...</masterchef2>
    </div>
</body>
</html>

Angular 2 Typescript Cannot Find Names

When I build the solution, I get a lot of compile errors. For example, error TS2304: Build:Cannot find name 'Promise'.

There are two ways to fix it.

  1. Switch the transpiler’s target from ES5 to ES6. To do that, change your tsconfig.json file to match the following values:
    {
      "compileOnSave": false,
      "compilerOptions": {
        "emitDecoratorMetadata": true,
        "experimentalDecorators": true,
        "module": "system",
        "moduleResolution": "node",
        "noImplicitAny": false,
        "noEmitOnError": false,
        "removeComments": false,
        "sourceMap": true,
        "target": "es6"
      },
      "exclude": [
        "node_modules",
        "wwwroot"
      ]
    }

    However, doing that could bring in some issues: you could be unable to use some of your tools/packages/libraries who don’t support ES6 yet, such as UglifyJS.

  2. Install Typings and core-js Type Definition Files. The tanspiller’s target is still ES5.
    {
      "compileOnSave": false,
      "compilerOptions": {
        "emitDecoratorMetadata": true,
        "experimentalDecorators": true,
        "module": "system",
        "moduleResolution": "node",
        "noImplicitAny": false,
        "noEmitOnError": false,
        "removeComments": false,
        "sourceMap": true,
        "target": "es5"
      },
      "exclude": [
        "node_modules",
        "wwwroot"
      ]
    }

    Open the package.json file (the one enumerating the NPM packages) and check if the typings package is already present within the dependencies or devDependencies node, together with the script required to run it during the post-install phase within the script block. If they’re not here, add them so that your file should look like the following:

    {
      "version": "1.0.0",
      "name": "asp.net",
      "dependencies": {
        "@angular/common": "2.0.0",
        "@angular/compiler": "2.0.0",
        "@angular/core": "2.0.0",
        "@angular/forms": "2.0.0",
        "@angular/http": "2.0.0",
        "@angular/platform-browser": "2.0.0",
        "@angular/platform-browser-dynamic": "2.0.0",
        "@angular/router": "3.0.0",
        "@angular/upgrade": "2.0.0",
        "core-js": "^2.4.1",
        "reflect-metadata": "^0.1.8",
        "rxjs": "5.0.0-rc.4",
        "systemjs": "^0.19.41",
        "typings": "^1.3.2",
        "zone.js": "^0.7.2",
        "moment": "^2.17.0"
      },
      "devDependencies": {
        "gulp": "^3.9.1",
        "gulp-clean": "^0.3.2",
        "gulp-concat": "^2.6.1",
        "gulp-less": "^3.3.0",
        "gulp-sourcemaps": "^1.9.1",
        "gulp-typescript": "^3.1.3",
        "gulp-uglify": "^2.0.0",
        "typescript": "^2.0.10"
      },
      "scripts": {
        "postinstall": "typings install dt~core-js@^0.9.7 --global"
      }
    }

    Please note we have to specify the version "0.9.7", otherwise it will install the latest version which is still cause problems. Now the ES6 TypeScript packages should compile without issues.

Run Application

First, rebuild the solution. Then go to the Tasks Runner Explorer window to run default task.

After all tasks finished, Click "IIS Express".

Add a new recipe, Mapo Tofu.

After you save, you can add steps and items for each step.

Debug Angular Code in Google Chrome

Although Angular 2 is TypeScript, all TypeScript files are converted to JavaScript minify files by gulp task. Look the below screenshot, the corresponding JavaScript files are created under the wwwroot/app folder.

So you cannot debug TypeScript directly. Fortunately, we can debug JavaScript files instead.

Click "IIS Express" dropdown button, select Google Chrome for the web browser. Then start the application by clicking "IIS Express". After the application starts, click "Developer Tools" from "More tools" in Google Chrome. Then click "Source." Now you can see all JavaScript files with a tree view. Pick up any JavaScript file you want to debug. Here we take deleting recipe as an example. So I pickup "recipe-delete.component.js".

As I said, all JavaScript files are created with minify style, which is very hard to read. But don’t worry, Chrome can restore the minify file to normal file for you. Just click "{}" which is at the left bottom of the middle window, the minify file will be changed to the "pretty print" file. I put the break pint at the deleteRecipe() function.

Click "delete" beside the recipe. Application shows Recipe Delete template.

Then click "Yes" to trigger the break point, and you can watch the variable you’re interested.

Put the break point at deleteRecipe function of the app.service.js. Then click "Resume script" button or press F8, the break point at app.service.js is triggered as well.

From App Service, it calls the server side web API. If you put the break point at the server side Http Delete method, server side break point will be triggered when you resume the script.

Conclusion

In these articles, I have showed you how to build an Angular 2 CRUD SPA within the context of ASP.NET Core. We have also learned how to use Angular 2 Route to navigate to different components and templates with client side routing configuration. Regarding Angular 4 has been released in March, Master Chef will be moved to Visual  Studio 2017 and Angular 4 in the next article.

I've created a public repository in github, Master Chef Repository.  Please feel free to participate the development.

License

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

Share

About the Author

Fred Song (Melbourne)
Software Developer (Senior)
Australia Australia
Fred Song is a senior software developer who lives in Melbourne, Australia. In 1993, he started Programming using Visual C++, Visual Basic, and Oracle Developer Tools. From 2003, He started with .Net using C#.

He is often working with software projects in different business domains based on different Microsoft Technologies like SQL-Server, C#, VC++, ASP.NET, WCF,WPF and Silverlight, although he also did some development works on IBM AS400.

You may also be interested in...

Comments and Discussions

 
QuestionGetting error (core_1.Component is not a function) Pin
Pragnesh K. Solanki1-Jun-17 4:42
memberPragnesh K. Solanki1-Jun-17 4:42 
AnswerRe: Getting error (core_1.Component is not a function) Pin
Fred Song (Melbourne)1-Jun-17 16:26
memberFred Song (Melbourne)1-Jun-17 16:26 
GeneralRe: Getting error (core_1.Component is not a function) Pin
Pragnesh K. Solanki6-Jun-17 23:31
memberPragnesh K. Solanki6-Jun-17 23:31 
GeneralMy vote of 5 Pin
RosalieGuen10-May-17 22:52
memberRosalieGuen10-May-17 22:52 
QuestionWhere is the links for other part Pin
Tridip Bhattacharjee10-May-17 1:07
professionalTridip Bhattacharjee10-May-17 1:07 
AnswerRe: Where is the links for other part Pin
Fred Song (Melbourne)10-May-17 16:22
memberFred Song (Melbourne)10-May-17 16:22 
GeneralMy vote of 5 Pin
UlysseEstienne29-Apr-17 1:27
memberUlysseEstienne29-Apr-17 1:27 

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
Web05 | 2.8.190214.1 | Last Updated 10 May 2017
Article Copyright 2017 by Fred Song (Melbourne)
Everything else Copyright © CodeProject, 1999-2019
Layout: fixed | fluid