Dec
30
Written by:
Michael Washington
12/30/2016 4:29 PM
This article covers an application that implements a full CRUD (Create Read Update Delete) Angular 2 Tree application.
This makes extensive use of the free open source PrimeNG Angular 2 components.
We start with the application created in the article: Tutorial: Creating An Angular 2 CRUD Application Using MVC 5 and OData 4.
The Database
When we open the project in Visual Studio and open the database in the App_Data folder…
We see the Nodes table that contains the data for the Tree.
When we open it, we see the schema for the table that was created using the following script:
CREATE TABLE [dbo].[Nodes] (
[Id] INT IDENTITY (1, 1) NOT NULL,
[NodeName] NVARCHAR (255) DEFAULT ('') NOT NULL,
[CreatedBy] NVARCHAR (255) NULL,
[Created] DATETIMEOFFSET (7) NULL,
[ModifiedBy] NVARCHAR (255) NULL,
[Modified] DATETIMEOFFSET (7) NULL,
[RowVersion] ROWVERSION NOT NULL,
[Node_Node] INT NULL,
PRIMARY KEY CLUSTERED ([Id] ASC),
CONSTRAINT [Node_Node] FOREIGN KEY ([Node_Node]) REFERENCES [dbo].[Nodes] ([Id])
);
When we open the Angular2QuickStartDAL.edmx file we see the data context created to allow us to programmatically interact with the database.
The Generic File Handler (To display Tree Data)
The data for a Tree can have several nested nodes. To display the nodes, we require code that uses recursive functions.
The generic file handler will be called by the Angular code.
The code is as follows:
using Angular2QuickStart.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Script.Serialization;
namespace LightSwitchApplication.Web
{
/// <summary>
/// Get the data and creates a JSON feed for the Tree control
/// </summary>
///
public class TreeData : IHttpHandler
{
public void ProcessRequest(HttpContext context)
{
// This connects to the database
using (Entities db = new Entities())
{
// Collection to hold final TreeNodes
List<DTONode> colTreeNodes = new List<DTONode>();
// This returns all Nodes in the database
var colNodes = (from objNode in db.Nodes
select new DataNode
{
Data = objNode.Id.ToString(),
NameName = objNode.NodeName,
NodeParentData = objNode.Node1.Id.ToString()
}).ToList();
// Loop through Parent 'root' nodes
// (meaning the NodeParentData is blank)
foreach (DataNode objNode in colNodes
.Where(x => x.NodeParentData == ""))
{
// Create a new Node
DTONode objNewNode = new DTONode();
objNewNode.data = objNode.Data;
objNewNode.label = objNode.NameName;
objNewNode.expandedIcon = "fa-folder-open";
objNewNode.collapsedIcon = "fa-folder";
objNewNode.parentId = 0;
objNewNode.children = new List<DTONode>();
colTreeNodes.Add(objNewNode);
// Add Child Nodes
AddChildren(colNodes, colTreeNodes, objNewNode);
}
// Create JavaScriptSerializer
JavaScriptSerializer jsonSerializer = new JavaScriptSerializer();
// Output as JSON
context.Response.Write(jsonSerializer.Serialize(colTreeNodes));
}
}
#region AddChildren
private void AddChildren(
List<DataNode> colNodeItemCollection,
List<DTONode> colTreeNodeCollection,
DTONode paramTreeNode)
{
// Get the children of the current item
// This method may be called from the top level
// or recursively by one of the child items
var ChildResults = from objNode in colNodeItemCollection
where objNode.NodeParentData == paramTreeNode.data
select objNode;
// Loop thru each Child of the current Node
foreach (var objChild in ChildResults)
{
// Create a new Node
var objNewNode = new DTONode();
objNewNode.data = objChild.Data;
objNewNode.label = objChild.NameName;
objNewNode.expandedIcon = "fa-folder-open";
objNewNode.collapsedIcon = "fa-folder";
objNewNode.parentId = Convert.ToInt32(paramTreeNode.data);
objNewNode.children = new List<DTONode>();
// Search for the Node in colTreeNodeCollection
// By looping through each 'root' Node
// (meaning the NodeParentData is blank)
foreach (DataNode objNode in colNodeItemCollection
.Where(x => x.NodeParentData == ""))
{
// See if Parent is in the colTreeNodeCollection
DTONode objParent =
colTreeNodeCollection.Where(x => x.data == objNode.Data).FirstOrDefault();
if (objParent != null) // Parent exists in the colTreeNodeCollection
{
// Get the Parent Node for the current Child Node
DTONode objParentTreeNode = objParent.Descendants()
.Where(x => x.data == paramTreeNode.data).FirstOrDefault();
if (objParentTreeNode != null)
{
// Add the Child node to the Parent
objParentTreeNode.children.Add(objNewNode);
}
}
}
//Recursively call the AddChildren method adding all children
AddChildren(colNodeItemCollection, colTreeNodeCollection, objNewNode);
}
}
#endregion
public bool IsReusable
{
get
{
return false;
}
}
}
}
If we have data in the database, when we run the application, and look at the output in an application like Fiddler, we can see that the code returns the nodes in a nested format.
Install PrimeNG
We built the application using the free open source suite of components called PrimeNG.
The setup directions are here:
http://www.primefaces.org/primeng/#/setup
In our application, we first added:
"primeng": "^1.0.0"
to the
package.json file.
Then we added:
'primeng': 'npm:primeng'
and:
primeng: {
defaultExtension: 'js'
}
to the
systemjs.config.js file.
Next, we downloaded and installed Font Awesome.
Then we added the following lines to _Layout.cshtml:
<link rel="stylesheet"
type="text/css"
href="node_modules/primeng/resources/themes/omega/theme.css" />
<link rel="stylesheet"
type="text/css"
href="node_modules/primeng/resources/primeng.min.css" />
<link href="~/Content/font-awesome.min.css" rel="stylesheet" />
Enable PrimeNG
To enable PrimeNG in our application, we open the app.module.ts file and add:
import {
InputTextModule,
DropdownModule,
ButtonModule,
FieldsetModule,
TreeModule,
TreeNode,
SelectItem
} from 'primeng/primeng';
and:
InputTextModule,
TreeModule,
DropdownModule,
ButtonModule,
FieldsetModule
Display The Tree
To display the Tree, we create a file, NodeService.ts, that calls the generic file handler we created earlier, using the following code:
// Angular Imports
import {
Injectable
} from '@angular/core';
import {
Http, Response, RequestOptions,
Request, RequestMethod, Headers
} from '@angular/http';
import 'rxjs/add/operator/toPromise';
import { TreeNode } from 'primeng/primeng';
@Injectable()
export class NodeService {
// This is the URL to the end point
private _treenodeUrl = 'web/TreeData.ashx';
// Pass the Http object to the class
// through the constructor
constructor(private _http: Http) {
}
getFiles() {
// Call the data end point
// and return the Tree nodes
return this._http.get(this._treenodeUrl)
.toPromise()
.then(res => <TreeNode[]>res.json())
.then(data => {
return data;
});
}
}
We then create a file, tree.component.html using the following code:
<!-- Error (if any) -->
<div class="ui-button-danger" *ngIf="errorMessage">
<p>{{ errorMessage }}</p>
</div>
<!-- Tree Node Control -->
<p-tree [value]="treeNodes"
selectionMode="single"
(onNodeSelect)="nodeSelect()"
[(selection)]="selectedNode"></p-tree>
<br />
We create a file, tree.component.ts using the following code:
import {
Component, OnInit, OnDestroy, Input, Output,
ViewContainerRef, EventEmitter, ViewChild, trigger
} from '@angular/core';
import {
Router, ActivatedRoute
} from '@angular/router';
import {
Subscription
} from 'rxjs/Subscription';
import {
NodeService
} from './NodeService';
import {
InputTextModule,
DropdownModule,
ButtonModule,
FieldsetModule,
TreeModule,
TreeNode,
SelectItem
} from 'primeng/primeng';
@Component({
moduleId: module.id,
selector: 'tree-form',
templateUrl: 'tree.component.html'
})
export class TreeComponent implements OnInit {
treeNodes: TreeNode[];
EditModeLabel: string = "Edit Node";
// Any error messages
errorMessage: string;
selectedNode: TreeNode;
editNodeName: string;
nodeParents: TreeNode[];
selectedNodeParent: SelectItem;
nodeParentsDropdown: SelectItem[] = [];
// Contructor is called when the class is created
constructor(
private _NodeService: NodeService) { }
// Handle data related tasks in ngOnInit
ngOnInit() {
this.populateTree();
}
populateTree() {
// Call the service to update the Tree
this._NodeService.getFiles().then(files => {
// Set the data source for the Tree
this.treeNodes = files;
});
}
}
Finally, we add the following lines to app.module.ts:
import { NodeService } from './tree/NodeService';
and:
providers: [
NodeService
],
If we have data in the database, when we run the application, we will see the Tree.
The Dropdown and the Form
There is a form, that contains a dropdown of the nodes, that allows you to create and edit the Tree nodes.
The HTML mark-up for the form and dropdown are as follows:
<!-- Edit Form -->
<p-fieldset legend="{{ EditModeLabel }}">
<label for="NodeName">Node Name</label>
<input type="text"
id="NodeName"
required
[(ngModel)]="editNodeName"
name="NodeName"
#name="ngModel">
<br />
<label for="NodeParent">Node Parent</label>
<p-dropdown id="NodeParent"
[options]="nodeParentsDropdown"
[(ngModel)]="selectedNodeParent"
[style]="{'width':'150px'}"></p-dropdown>
<br />
<button pButton type="button"
(click)="Save()"
label="Save"></button>
<button pButton type="button"
(click)="NewNode()"
label="New"
class="ui-button-success"></button>
<button pButton type="button"
(click)="DeleteNode()"
label="Delete"
class="ui-button-danger"></button>
</p-fieldset>
The code to fill the dropdown is as follows:
populateDropdown() {
// Call the service
this._treeService.getTreeNodes()
.subscribe((nodes) => {
// Clear the list
this.nodeParentsDropdown = [];
// Loop through the returned Tree Nodes
for (let node of nodes) {
// Create a new SelectedItem
let newSelectedItem: SelectItem = {
label: node.label,
value: node.data
}
// Add Selected Item to the DropDown
this.nodeParentsDropdown.push(newSelectedItem);
}
// Set the selected option to the first option
this.selectedNodeParent = this.nodeParentsDropdown[0];
},
error => this.errorMessage = <any>error);
}
This code calls the _treeService class, that communicates with the back-end OData service. That code is as follows:
// Angular Imports
import {
Injectable
} from '@angular/core';
import {
Http, Response, RequestOptions,
Request, RequestMethod, Headers
} from '@angular/http';
// OData IDTONode class
import {
IDTONode
} from './DTONode';
// OData IDTODataNode class
import {
IDTODataNode
} from './DTODataNode';
// This service uses rxjs Observable
import {
Observable
} from 'rxjs/Observable';
import 'rxjs/add/operator/do';
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/map';
// This is marked Injectable because it will be
// consumed in the Controller class
@Injectable()
export class TreeService {
// This is the URL to the OData end points
private _createTreeNodeUrl = 'odata/ODataNodes';
private _updateTreeNodeUrl = 'odata/DTODataNodes';
// Pass the Http object to the class
// through the constructor
constructor(private _http: Http) { }
// ** Get all Tree Nodes **
getTreeNodes(): Observable<IDTONode[]> {
// Make the Angular 2 Get
// Note that we must use .value
// because the OData response is wrapped in an object
// and the data we want to map is at .value
return this._http.get(this._createTreeNodeUrl)
.map((response: Response) => <IDTONode[]>response.json().value)
.catch(this.handleError);
}
// ** Called when there are any errors **
private handleError(error: Response) {
console.error(error);
return Observable.throw(error.json().error || 'Server error');
}
}
The code for the server-side OData service that communicates with the database (through the .edmx DataContext class) and returns the data for the dropdown is as follows:
using System;
using System.Data;
using System.Data.Entity;
using System.Linq;
using System.Net;
using System.Web.Http;
using System.Web.OData;
using Angular2QuickStart.Models;
using System.Collections.Generic;
namespace Angular2QuickStart.Controllers
{
public class ODataNodesController : ODataController
{
// This connects to the database
private Entities db = new Entities();
#region public IEnumerable<DTONode> GetODataNodes()
// GET: odata/GetODataNodes
public IEnumerable<DTONode> GetODataNodes()
{
// Collection to hold final TreeNodes
List<DTONode> colTreeNodes = new List<DTONode>();
// This returns all Nodes in the database
var colNodes = (from objNode in db.Nodes
select new DataNode
{
Data = objNode.Id.ToString(),
NameName = objNode.NodeName,
NodeParentData = objNode.Node1.Id.ToString()
}).OrderBy(x => x.NodeParentData).ThenBy(y => y.NameName).ToList();
// Create a '[None]' Node
DTONode objNoneNode = new DTONode();
objNoneNode.data = "0";
objNoneNode.label = "[None]";
objNoneNode.parentId = 0;
colTreeNodes.Add(objNoneNode);
// Loop through Parent 'root' nodes
// (meaning the NodeParentData is blank)
foreach (DataNode objNode in colNodes
.Where(x => x.NodeParentData == ""))
{
// Create a new Node
DTONode objNewNode = new DTONode();
objNewNode.data = objNode.Data;
objNewNode.label = objNode.NameName;
if (objNode.NodeParentData != "")
{
objNewNode.parentId = Convert.ToInt32(objNode.NodeParentData);
}
colTreeNodes.Add(objNewNode);
// Add Nodes
AddNodes(colNodes, colTreeNodes, objNewNode);
}
// This is not Queryable because we need the list
// to stay in the correct order (by NodeParentData)
return colTreeNodes.ToList();
}
#endregion
// Utility
#region AddNodes
private void AddNodes(
List<DataNode> colNodeItemCollection,
List<DTONode> colTreeNodeCollection,
DTONode paramTreeNode)
{
// Get the children of the current item
// This method may be called from the top level
// or recuresively by one of the child items
var ChildResults = from objNode in colNodeItemCollection
where objNode.NodeParentData == paramTreeNode.data
select objNode;
// Loop thru each Child of the current Node
foreach (var objChild in ChildResults)
{
// Create a new Node
var objNewNode = new DTONode();
objNewNode.data = objChild.Data;
// See if there is a Parent
if (objChild.NodeParentData != "")
{
// Set the Parent
objNewNode.parentId = Convert.ToInt32(objChild.NodeParentData);
// Get the Parent
DTONode objParent =
colTreeNodeCollection.Where(x => x.data == objChild.NodeParentData).FirstOrDefault();
// See how many dots the Parent has
int CountOfParentDots = objParent.label.Count(x => x == '.');
// Update the label to add dots in front of the name
objNewNode.label = $"{AddDots(CountOfParentDots + 1)}{objChild.NameName}";
}
else
{
// There was no parent so don't add any dots
objNewNode.label = objChild.NameName;
}
colTreeNodeCollection.Add(objNewNode);
//Recursively call the AddChildren method adding all children
AddNodes(colNodeItemCollection, colTreeNodeCollection, objNewNode);
}
}
#endregion
#region AddDots
private static string AddDots(int intDots)
{
String strDots = "";
for (int i = 0; i < intDots; i++)
{
strDots += ". ";
}
return strDots;
}
#endregion
#region protected override void Dispose(bool disposing)
protected override void Dispose(bool disposing)
{
if (disposing)
{
// Dispoase of the database object
db.Dispose();
}
base.Dispose(disposing);
}
#endregion
}
}
The Remaining CRUD Methods
The form allows users to create new nodes, select and update existing nodes, and to delete nodes.
The methods that provide that functionality are as follows:
nodeSelect() {
// We are editing
this.EditModeLabel = "Edit Node";
// Set the edit node label
this.editNodeName = this.selectedNode.label
// Set the Edit Node parent
var selectedTreeNode: SelectItem;
if (this.selectedNode.parent !== undefined) {
selectedTreeNode = this.nodeParentsDropdown.find(x => x.value == this.selectedNode.parent.data);
} else {
// Select the '[None]' node
// It is always the first option in the list
selectedTreeNode = this.nodeParentsDropdown[0];
}
// Set the selected option to update the DropDown
this.selectedNodeParent = selectedTreeNode.value;
}
NewNode() {
// We are creating a new node
this.EditModeLabel = "New Node";
// Set the Edit Node parent
var selectedTreeNode: SelectItem;
if (this.selectedNode !== undefined) {
selectedTreeNode = this.nodeParentsDropdown.find(x => x.value == this.selectedNode.data);
if (selectedTreeNode !== undefined) {
// Set the selected option to update the DropDown
this.selectedNodeParent = selectedTreeNode.value;
}
}
// Set selectedNode to a new TreeNode
this.selectedNode = this.createNewTreeNode();
// Set the edit node label
this.editNodeName = this.selectedNode.label
}
createNewTreeNode() {
// Create a new TreeNode
let newTreeNode: TreeNode = {
label: "",
data: -1 // So we know it is a new Node
}
return newTreeNode;
}
Save() {
this.errorMessage = "";
// Create an IDTODataNode
// This will be used to update the database
let objTreeNode: IDTODataNode = {
Id: this.selectedNode.data,
NodeName: this.editNodeName,
ParentId: 0
}
// Is this a new TreeNode?
if (objTreeNode.Id == -1) {
// Set the ParentId
objTreeNode.ParentId =
Number(this.selectedNodeParent.value
? this.selectedNodeParent.value
: this.selectedNodeParent);
// Call the service to Insert the TreeNode
this._treeService.createTreeNode(objTreeNode)
.subscribe(() => {
// Refresh
this.populateTree();
this.populateDropdown();
// Set NewNode Mode
this.NewNode();
},
error => this.errorMessage = <any>error);
} else {
// A Node cannot be set as a parent to itself
if (this.selectedNodeParent !== this.selectedNode.data) {
// Set the ParentId
objTreeNode.ParentId = Number(this.selectedNodeParent);
// Call the service to update the TreeNode
this._treeService.updateTreeNode(objTreeNode)
.subscribe(() => {
// Refresh
this.populateTree();
this.populateDropdown();
// Set NewNode Mode
this.NewNode();
},
error => this.errorMessage = <any>error);
}
}
}
DeleteNode() {
this.errorMessage = "";
// Get TreeNode
var NodeId: number = Number(this.selectedNode.data);
// Only a Node Id other than -1 can be deleted
if (NodeId > -1) {
// Call the service to delete the Node
this._treeService.deleteTreeNode(NodeId)
.subscribe(() => {
// Refresh
this.populateTree();
this.populateDropdown();
// Set NewNode Mode
this.NewNode();
},
error => this.errorMessage = <any>error);
}
}
expandAll() {
this.treeNodes.forEach(node => {
this.expandRecursive(node, true);
});
}
private expandRecursive(node: TreeNode, isExpand: boolean) {
node.expanded = isExpand;
if (node.children) {
node.children.forEach(childNode => {
this.expandRecursive(childNode, isExpand);
});
}
}
This code calls methods in the _treeService class. Those methods are as follows:
// ** Create a Tree Node **
createTreeNode(paramtreenode: IDTODataNode): Observable<IDTODataNode> {
// This is a Post so we have to pass Headers
let headers = new Headers({ 'Content-Type': 'application/json' });
let options = new RequestOptions({ headers: headers });
// Make the Angular 2 Post
return this._http.post(this._updateTreeNodeUrl,
JSON.stringify(paramtreenode), options)
.map((response: Response) => <IDTODataNode>response.json())
.catch(this.handleError);
}
// ** Update a Tree Node **
updateTreeNode(paramtreenode: IDTODataNode): Observable<void> {
// This is a Put so we have to pass Headers
let headers = new Headers({ 'Content-Type': 'application/json' });
let options = new RequestOptions({ headers: headers });
// Make the Angular 2 Put
return this._http.put(
this._updateTreeNodeUrl + "(" + paramtreenode.Id + ")",
JSON.stringify(paramtreenode), options)
.catch(this.handleError);
}
// ** Delete a Tree Node **
deleteTreeNode(id: number): Observable<void> {
// A Delete does not return anything
return this._http.delete(this._updateTreeNodeUrl + "(" + id + ")")
.catch(this.handleError);
}
These methods call the server-side OData methods. Those methods are as follows:
// This connects to the database
private Entities db = new Entities();
#region public IHttpActionResult Post(DTODataNode DTODataNode)
// POST: odata/ODataNodes
public IHttpActionResult Post(DTODataNode DTODataNode)
{
// Create a new Node
var NewNode = new Node();
NewNode.NodeName = DTODataNode.NodeName;
if (DTODataNode.ParentId > 0)
{
NewNode.Node_Node = DTODataNode.ParentId;
}
// Save the Node
db.Nodes.Add(NewNode);
db.SaveChanges();
// Populate the ID that was created and pass it back
DTODataNode.Id = NewNode.Id;
// Return the Node
return Created(DTODataNode);
}
#endregion
#region public IHttpActionResult Put([FromODataUri] int key, DTODataNode DTODataNode)
// PUT: odata/ODataNodes(1)
public IHttpActionResult Put([FromODataUri] int key, DTODataNode DTODataNode)
{
// Get the existing Node using the key that was passed
Node ExistingNode = db.Nodes.Find(key);
// Did we find a Node?
if (ExistingNode == null)
{
// If not return NotFound
return StatusCode(HttpStatusCode.NotFound);
}
// Update the Node
ExistingNode.NodeName = DTODataNode.NodeName;
if (DTODataNode.ParentId > 0)
{
ExistingNode.Node_Node = DTODataNode.ParentId;
}
else
{
ExistingNode.Node_Node = null;
}
// Save changes
db.Entry(ExistingNode).State = EntityState.Modified;
db.SaveChanges();
// Return the Updated Node
// Return that the Node was Updated
return Updated(ExistingNode);
}
#endregion
#region public IHttpActionResult Delete([FromODataUri] int key)
// DELETE: odata/ODataNodes(1)
public IHttpActionResult Delete([FromODataUri] int key)
{
// Get the existing Node using the key that was passed
Node ExistingNode = db.Nodes.Find(key);
// Did we find a Node?
if (ExistingNode == null)
{
// If not return NotFound
return StatusCode(HttpStatusCode.NotFound);
}
int? ParentNodeID = null;
// Possibly update Child Nodes
if (ExistingNode.Node_Node.HasValue)
{
// Get the Parent Node of the ExistingNode
ParentNodeID = ExistingNode.Node_Node.Value;
}
// Get the children of the current item
var ChildResults = from objNode in db.Nodes
where objNode.Node_Node.Value == ExistingNode.Id
where objNode.Node_Node.HasValue == true
select objNode;
// Loop thru each Child of the current Node
foreach (var objChild in ChildResults)
{
// Update the Parent Node
// for the Child Node
objChild.Node_Node = ParentNodeID;
}
// Delete the Node
db.Nodes.Remove(ExistingNode);
// Save changes
db.SaveChanges();
// Return a success code
return StatusCode(HttpStatusCode.NoContent);
}
#endregion
Links
PrimeNG
Font Awesome
Angular 2 Tutorial Series
- Hello World! in Angular 2 using Visual Studio 2015 and ASP.NET 4
- Implement ASP.NET 4 MVC Application Security in Angular 2 Using OData 4
- Tutorial: Creating An Angular 2 CRUD Application Using MVC 5 and OData 4
- Tutorial: An End-To-End Angular 2 Application Using MVC 5 and OData 4
Resources to Learn Angular 2
Angular 2: Getting Started (Pluralsight – Paid)
Introduction to Angular 2.0 (Microsoft Virtual Academy – Free)
Getting Started with npm in Visual Studio
Using EcmaScript 2015 Modules in TypeScript with SystemJS
Download
The project is available at http://lightswitchhelpwebsite.com/Downloads.aspx
You must have Visual Studio 2015 Update 3 (or higher) and TypeScript 2.0 (or higher) installed to run the code.
3 comment(s) so far...
Hi, Very Nice Article.. I am just a beginner in MVC so can you do me a favour to create the same in MVC 5..
By Chintan Gandhi on
7/13/2019 9:54 AM
|
@Chintan Gandhi - I am only doing Blazor tutorials now.
By Michael Washington on
7/13/2019 9:55 AM
|
@Michael - Thank you for your quick response.
But Is there anyone who can help me out for this requirement. I need this as soon as possible.
Please Help me.
By Chintan Gandhi on
7/13/2019 11:06 AM
|