Introduction
Tree view is one kind of structure or pattern that is used to arrange documents or records, and it becomes more useful when we deal with hierarchy pattern. let's make it more easy with an example.
Now lets Assume there is one football tournament is being organized and there are two user for this tournament,
First is Tournament Organizer and second is Team Owner, These both user has their role in the tournament.
Tournament Organizer's role:
(1) He can add New team and its Players
(2) He can delete Team.
(3) He can shift player(child Nodes) from one team to other team(By Drag and drop)
Team Owner's role:
(1)He can select who will play from his team
(2)He can not shift players(Node).
The above example is only for better understanding of TreeView. We can use this treeview approche in several scenario.
Demo of Treeview and its operations :
Features of article:
In this article we can perform these following actions.
(1)We can add new node whether it is parent node or child node.
(2)We can delete selected node.
(3)We can drag and drop every child node anywhere in hierarchy(after drag and drop one node to another it will save dynamically in database).
(4)We can get the selected node for saving record.
Table of contents:
(1)Database: In this section we will create table in our database with its content.
(2)MVC web project : In this section we will create step by step MVC web application.
(3)DB layer : In this section we will create a library project to integrate entity data model in Mvc Project.
(4)Using code: In this section we will discuss code logic and functionality.
(1) Database:
Step 1.1 : Create Table in your DataBase, for this you can execute query mentioned below
USE [Your DataBase Name]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[HierarchyDetails]
(
[Id] [int] NOT NULL IDENTITY(1,1),
[HierarchyName] [nvarchar](50) NULL,
[PerentId] [Int] NULL,
CONSTRAINT [PK_HierarchyDetails] PRIMARY KEY CLUSTERED ( [Id] ASC )
WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
INSERT [dbo].[HierarchyDetails] ([HierarchyName], [PerentId]) VALUES (N'Team1',null)
INSERT [dbo].[HierarchyDetails] ([HierarchyName], [PerentId]) VALUES (N'Player1',1)
INSERT [dbo].[HierarchyDetails] ([HierarchyName], [PerentId]) VALUES (N'Player2',1)
After Execute the query your table will look like:
Select * from HirarchyDetails
(2) Create MVC web application project :
Step 2.1: Open visual studio and choose---> File--> New--> project
Step 2.2: Select ASP.NET Web Application option and set project Name and Location
Step 2.3: Select MVC Templete and then click OK
Step 2.4: Update Bootstrap version(3.7.7):- Tools --> NugGet Package Manager--> Package Manager Console. and then run this command:
PM> Update-Package bootstrap -Version 3.7.7
Step 2.5: In this step we will intregate Gijgo JQuery plug-in for Treeview in our project,
jQuery Tree by Gijgo.com is a plug-in for the jQuery Javascript library.
It is a very fast and extandable tool, and will add advanced interaction controls to any tree structure.
This plugin allows you to create tree structure using bootstrap or material design styles.
Free open source tool distributed under MIT License.
For more information about Gijgo treeview, look at the below links.
Link: http://gijgo.com/Tree/configuration
Now in our project Add a new folder in script section and Add javascript file gijgo.js into this folder, Copy the code from http://code.gijgo.com/1.3.0/js/gijgo.js and then paste it into you local gijgo.js file,
Here we are keeping gijgo.js file in local because we can customize it.
Step 2.6: Edit updateChildrenState method in gijgo.js file for handling single child parent checkbox state, See the image below(Open gijgo.js file and find UpdateChildrenState then update length from 1 to 0)
(3) Create DB layer(Class Library) for Entity Framwork :
Step 3.1 : Add new library project in the solution. see the the image below
Step 3.2 : Select class library
Step 3.3 : Delete autogenerated class form library project
Step 3.4 : Add new folder in Library project for adding Entity model. and then add New Item.
Step 3.5 : Select ADO.NET Entity Data Model and click Add button
Step 3.6 : Select model content as shown in image.
Step 3.7 : Choose you data connection --> Click on New Connection Button
Step 3.8 : Fill connection properties and select related table from database as shown in image below.
Step 3.9 : Copy auto generated connection string from app.config and paste it into Web.config file in our project.
Step 3.10 : Add reference of Db layer into our mvc web project.
(4)Using code : Create [ Model-->View-->Controller ] and descuse the functionality
(A) Models :
In models folder i have Added two class
first-->AddNode.cs : this model class is used to bind our partial view for adding new node
public class AddNode
{
[Required(ErrorMessage = "Node type is required.")]
public string NodeTypeRbtn { get; set; }
[Required(ErrorMessage = "Node Name is required.")]
public string NodeName { get; set; }
[requiredif("NodeTypeRbtn","Cn",ErrorMessage ="Parent Node is required")]
public int? ParentName { get; set; }
}
Second-->HierarchyViewModel.cs : this model class is used to store List of hierarchy details from data base.
public class HierarchyViewModel
{
public int Id { get; set; }
public string text { get; set; }
public int? perentId { get; set; }
public virtual List<HierarchyViewModel> children { get; set; }
}
Third--> Data Entity Model(Auto generated from database in Db Layer):
public partial class HierarchyDetail
{
public int Id { get; set; }
public string HierarchyName { get; set; }
public Nullable<int> PerentId { get; set; }
}
Create validationhelper Folder and requiredif.cs class for conditional validation
requiredif is conditional attributes which is used to validate Input field based on some condition , In our case while adding new node from partial view we are using conditional attributes. Please see the code below.
1 Copy and paste this code in requiredif.cs class:
public class requiredif : ValidationAttribute, IClientValidatable
{
private RequiredAttribute _innerAttribute = new RequiredAttribute();
private const string DefaultErrorMessageFormatString = "The {0} field is required.";
private readonly string _dependentPropertyName;
public object TargetValue { get; set; }
public requiredif(string dependentPropertyName, object targetValue)
{
_dependentPropertyName = dependentPropertyName;
this.TargetValue = targetValue;
ErrorMessage = DefaultErrorMessageFormatString;
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var containerType = validationContext.ObjectInstance.GetType();
var field = containerType.GetProperty(this._dependentPropertyName);
if (field != null)
{
var dependentvalue = field.GetValue(validationContext.ObjectInstance, null);
if (
(dependentvalue == null && this.TargetValue == null) ||
(dependentvalue != null && dependentvalue.Equals(this.TargetValue))
)
{
if (!_innerAttribute.IsValid(value))
return new ValidationResult(this.ErrorMessage, new[] { validationContext.MemberName });
}
}
return ValidationResult.Success;
}
public IEnumerable <ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
var rule = new ModelClientValidationRule
{
ErrorMessage = ErrorMessage,
ValidationType = "requiredif"
};
string depProp = BuildDependentPropertyId(metadata, context as ViewContext);
string targetValue = (this.TargetValue ?? "").ToString();
if (this.TargetValue.GetType() == typeof(bool))
targetValue = targetValue.ToLower();
rule.ValidationParameters.Add("dependentproperty", depProp);
rule.ValidationParameters.Add("targetvalue", targetValue);
yield return rule;
}
private string BuildDependentPropertyId(ModelMetadata metadata, ViewContext viewContext)
{
string depProp = viewContext.ViewData.TemplateInfo.GetFullHtmlFieldId(this._dependentPropertyName);
var thisField = metadata.PropertyName + "_";
if (depProp.StartsWith(thisField))
depProp = depProp.Substring(thisField.Length);
return depProp;
}
}
2 : Create Js file in script folder and paste this javascript code, by this we will be able to add our conditional attributes to the JQuery validation and enable client side validation.
$.validator.unobtrusive.adapters.add(
'requiredif',
['dependentproperty', 'targetvalue'],
function (options) {
options.rules['requiredif'] = {
dependentproperty: options.params['dependentproperty'],
targetvalue: options.params['targetvalue']
};
options.messages['requiredif'] = options.message;
});
$.validator.addMethod('requiredif', function (value, element, parameters) {
var desiredvalue = parameters.targetvalue;
desiredvalue = (desiredvalue == null ? '' : desiredvalue).toString();
var controlType = $("input[id$='" + parameters.dependentproperty + "']").attr("type");
var actualvalue = {}
if (controlType == "checkbox" || controlType == "radio") {
var control = $("input[id$='" + parameters.dependentproperty + "']:checked");
actualvalue = control.val();
} else {
actualvalue = $("#" + parameters.dependentproperty).val();
}
if ($.trim(desiredvalue).toLowerCase() === $.trim(actualvalue).toLocaleLowerCase()) {
var isValid = $.validator.methods.required.call(this, value, element, parameters);
return isValid;
}
return true;
});
(B) Controller :
(1): Change the controller name from Home to Hierarchy in RouteConfig file.
(2): There are two methods in HierarchyContoller.cs class, first is GetHierarchy() and second is GetChild() in the controller. These methods are used to get datasource for the tree view. Here GetChild() is recursive method (When a mathod calls itself, it'll be named recursive method and recursive method calls itself so many times until being satisfied).
Now lets assume one example for understanding the functionality of these two methods.
Suppose we have table with some content and its final view result as shown below:-
Table:
Result View:
When GetHierarchy() method is executed it gets all details from datasource(whose parentid is Null) and fills all property of our model HierarchyViewModel and then execute recursive method GetChild() who takes two parametre, 1st is the list of HierarchyDetail and 2nd is Id for filling the list of HierarchyViewModel. and this recursive method calls itself so many times until being satisfied(as long as the id is exist in ParentId column).
Here in image[P0] Id is 1 which is Parent Id for two Node(P1 and P2) in table , So there will be two child of ParentNode [P0], hance the children count is 2. Please see image [P1] and [P2] shown below.
Image[P0]:
Here in image[P1] id is 2, which is Parent Id for one Node(P1) in table, So there will be one child of ParentNode [P1],
Image[P1]:
Here in image[P1.1] id is 4, which is not a parent Id of any row in table, So there will be no child of ParentNode [P1.1]
Image[P1.1]:
Here in image[P2] id is 3, which is not a parent Id of any row in table, So there will be no child of ParentNode [P2]
Image[P2]:
Finally GetHierarchy() returns a json result which is passed as dataSource for gijgo Tree view object.
[Note]: If you want to add nested node in treeview. You can remove the where clause from Hierarchy Details table by doing this you will enable dropdown to render all options
Below are the complete codes of HierarchyController.cs class
public class HierarchyController : Controller
{
public ActionResult Index()
{
using (TreeViewEntities context = new TreeViewEntities())
{
var plist = context.HierarchyDetails.Where(p=>p.PerentId==null).Select(a => new
{
a.Id,
a.HierarchyName
}).ToList();
ViewBag.plist = plist;
}
GetHierarchy();
return View();
}
public JsonResult GetHierarchy()
{
List<HierarchyDetail> hdList;
List<HierarchyViewModel> records;
using (TreeViewEntities context = new TreeViewEntities())
{
hdList = context.HierarchyDetails.ToList();
records = hdList.Where(l => l.PerentId == null)
.Select(l => new HierarchyViewModel
{
Id = l.Id,
text = l.HierarchyName,
perentId = l.PerentId,
children = GetChildren(hdList, l.Id)
}).ToList();
}
return this.Json(records, JsonRequestBehavior.AllowGet);
}
private List<HierarchyViewModel> GetChildren(List<HierarchyDetail> hdList, int parentId)
{
return hdList.Where(l => l.PerentId == parentId)
.Select(l => new HierarchyViewModel
{
Id = l.Id,
text = l.HierarchyName,
perentId = l.PerentId,
children = GetChildren(hdList, l.Id)
}).ToList();
}
[HttpPost]
public JsonResult ChangeNodePosition(int id, int parentId)
{
using (TreeViewEntities context = new TreeViewEntities())
{
var Hd = context.HierarchyDetails.First(l => l.Id == id);
Hd.PerentId = parentId;
context.SaveChanges();
}
return this.Json(true,JsonRequestBehavior.AllowGet);
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult AddNewNode(AddNode model)
{
try
{
if (ModelState.IsValid)
{
using (TreeViewEntities db = new TreeViewEntities())
{
HierarchyDetail hierarchyDetail = new HierarchyDetail()
{
HierarchyName = model.NodeName,
PerentId = model.ParentName,
};
db.HierarchyDetails.Add(hierarchyDetail);
db.SaveChanges();
}
return Json(new { success = true }, JsonRequestBehavior.AllowGet);
}
}
catch (Exception ex)
{
throw ex;
}
return Json(new { success = false }, JsonRequestBehavior.AllowGet);
}
[HttpPost]
public JsonResult DeleteNode(string values)
{
try
{
using (TreeViewEntities context = new TreeViewEntities())
{
var id = values.Split(',');
foreach (var item in id)
{
int ID = int.Parse(item);
context.HierarchyDetails.RemoveRange(context.HierarchyDetails.Where(x => x.Id == ID).ToList());
context.SaveChanges();
}
}
return Json(new { success = true }, JsonRequestBehavior.AllowGet);
}
catch (Exception ex)
{
throw ex;
}
}
}
(C) View UI and JavaScript:
(1): Here we add a view for Index actionMethod and then write script to handel Gijgo treeview
. this script is used to bind data from database and post (save) node which has been changed their parents :
(A)Dragable and Dropable Script: In the script section nodeDrop is the event that handels drag and drop functionality,Here when we start to drag a node it gets its Id and when we drop this node on other node it gets its id as parent id, later on we use these ids in callback function to POST data into tha data base, once we get the success response from server, then we sent a Ajax call for updating UI. Please see the scrip below.
tree.on('nodeDrop', function (e, Id, PerentId) {
currentNode = Id ? tree.getDataById(Id) : {};
console.log("current Node = " + currentNode);
parentNode = PerentId ? tree.getDataById(PerentId) : {};
console.log("parent Node = " + parentNode);
if (currentNode.perentId === null && parentNode.perentId === null) {
alert("Parent node is not droppable..!!");
return false;
}
var params = { id: Id, parentId: PerentId };
$.ajax({
type: "POST",
url: "/Hierarchy/ChangeNodePosition",
data: params,
dataType: "json",
success: function (data) {
$.ajax({
type: "Get",
url: "/Hierarchy/GetHierarchy",
dataType: "json",
success: function (records) {
Usertree.destroy();
Usertree = $('#Usertree').tree({
primaryKey: 'Id',
dataSource: records,
dragAndDrop: false,
checkboxes: true,
iconsLibrary: 'glyphicons',
});
}
});
}
});
});
Complete code of Index
@model DynamicTreeView.Models.AddNode
<div class="col-md-12" style="margin:100px auto;">
<div class="modal fade in" id="modalAddNode" role="dialog" aria-hidden="true">
@Html.Partial("_AddNode")
</div>
<div class="col-md-5">
<div class="panel panel-primary">
<div class="panel-heading">Tournament organizer -: [ Add team and its players ]</div>
<div class="panel-body">
<div id="tree"></div>
<div class="clearfix">
</div>
<br />
<div>
<button id="btnDeleteNode" data-toggle="modal" class='btn btn-danger'> Delete Node <span class="glyphicon glyphicon-trash"></span> </button>
<button id="btnpopoverAddNode" data-toggle="modal" class='btn btn-warning'> Add Node <span class="glyphicon glyphicon-plus"></span> </button>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="panel panel-primary">
<div class="panel-heading">Team owner -: [ Select team and players who will play ]</div>
<div class="panel-body">
<div id="Usertree"></div>
<div class="clearfix">
</div>
<br />
<div>
<button id="btnGetValue" class="btn btn-warning">Get Checked Value</button>
</div>
</div>
</div>
</div>
</div>
@section Scripts {
@Scripts.Render("~/bundles/jqueryval")
<script src="@Url.Content("~/Scripts/conditional-validation.js")" type="text/javascript"></script>
<script src="~/Scripts/Gijgo/gijgo.js"></script>
<link href="http://code.gijgo.com/1.3.0/css/gijgo.css" rel="stylesheet" type="text/css" />
<script type="text/javascript">
$(document).ready(function () {
var Usertree = "";
var tree = "";
$.ajax({
type: 'get',
dataType: 'json',
cache: false,
url: '/Hierarchy/GetHierarchy',
success: function (records, textStatus, jqXHR) {
tree = $('#tree').tree({
primaryKey: 'Id',
dataSource: records,
dragAndDrop: true,
checkboxes: true,
iconsLibrary: 'glyphicons',
});
Usertree = $('#Usertree').tree({
primaryKey: 'Id',
dataSource: records,
dragAndDrop: false,
checkboxes: true,
iconsLibrary: 'glyphicons',
});
tree.on('nodeDrop', function (e, Id, PerentId) {
currentNode = Id ? tree.getDataById(Id) : {};
console.log("current Node = " + currentNode);
parentNode = PerentId ? tree.getDataById(PerentId) : {};
console.log("parent Node = " + parentNode);
if (currentNode.perentId === null && parentNode.perentId === null) {
alert("Parent node is not droppable..!!");
return false;
}
var params = { id: Id, parentId: PerentId };
$.ajax({
type: "POST",
url: "/Hierarchy/ChangeNodePosition",
data: params,
dataType: "json",
success: function (data) {
$.ajax({
type: "Get",
url: "/Hierarchy/GetHierarchy",
dataType: "json",
success: function (records) {
Usertree.destroy();
Usertree = $('#Usertree').tree({
primaryKey: 'Id',
dataSource: records,
dragAndDrop: false,
checkboxes: true,
iconsLibrary: 'glyphicons',
});
}
});
}
});
});
$('#btnGetValue').click(function (e) {
var result = Usertree.getCheckedNodes();
if (result == "") { alert("Please Select Node..!!") }
else {
alert("Selected Node id is= " + result.join());
}
});
$('#btnDeleteNode').click(function (e) {
e.preventDefault();
var result = tree.getCheckedNodes();
if (result != "") {
$.ajax({
type: "POST",
url: "/Hierarchy/DeleteNode",
data: { values: result.toString() },
dataType: "json",
success: function (data) {
alert("Deleted successfully ");
window.location.reload();
},
error: function (jqXHR, textStatus, errorThrown) {
alert('Error - ' + errorThrown);
},
});
}
else {
alert("Please select Node to delete..!!");
}
});
},
error: function (jqXHR, textStatus, errorThrown) {
alert('Error - ' + errorThrown);
}
});
$('#btnpopoverAddNode').click(function (e) {
e.preventDefault();
$("#modalAddNode").modal("show");
});
$(document).on("click", "#savenode", function (event) {
event.preventDefault();
$.validator.unobtrusive.parse($('#formaddNode'));
$('#formaddNode').validate();
if ($('#formaddNode').valid()) {
var formdata = $('#formaddNode').serialize();
$.ajax({
type: "POST",
url: "/Hierarchy/AddNewNode",
dataType: "json",
data: formdata,
success: function (response) {
window.location.reload();
},
error: function (response) {
alert('Exception found');
window.location.reload();
},
complete: function () {
}
});
}
});
$(document).on("click", "#closePopup", function (e) {
e.preventDefault();
$("#modalAddNode").modal("hide");
});
$('.rbtnnodetype').click(function (e) {
if ($(this).val() == "Pn") {
$('.petenddiv').attr("class", "petenddiv hidden");
$("#ParentName").val("");
}
else {
$('.petenddiv').attr("class", "petenddiv");
}
});
});
</script>
}
2: Add _Addnode.cshtml partila view in the view section.
@model DynamicTreeView.Models.AddNode
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
<h4 class="modal-title">Add Node</h4>
</div>
<div class="modal-body">
@using (Html.BeginForm("AddNewNode", "Hierarchy", FormMethod.Post, new { @id = "formaddNode", @class = "form-horizontal", role = "form", enctype = "multipart/form-data" }))
{
@Html.AntiForgeryToken()
@Html.ValidationSummary(true)
<div class="col-md-12">
<div class="col-md-6 row">
<div class="input-group">
<input type="text" class="form-control" value="Perent Node" readonly="readonly">
<span class="input-group-addon">
@Html.RadioButtonFor(model => model.NodeTypeRbtn, "Pn", new { @class = "btn btn-primary rbtnnodetype" })
</span>
</div>
</div>
<div class="col-md-6">
<div class="input-group ">
<input type="text" class="form-control" value="Child Node" readonly="readonly">
<span class="input-group-addon">
@Html.RadioButtonFor(model => model.NodeTypeRbtn, "Cn", new { @class = "rbtnnodetype" })
</span>
</div>
</div>
<br />
@Html.ValidationMessageFor(m => m.NodeTypeRbtn, "", new { @class = "alert-error" })
</div>
<div class="clearfix">
</div>
<div class="col-md-12">
<div class="petenddiv hidden">
@Html.Label("Select Perent")
@Html.DropDownList("ParentName", new SelectList(ViewBag.plist, "Id", "HierarchyName"), "--select--", new { @class = "form-control" })
@Html.ValidationMessageFor(m => m.ParentName, "", new { @class = "alert-error" })
</div>
</div>
<div class="clearfix">
</div>
<div class="col-md-12">
<div>
@Html.Label("Node Name")
@Html.TextBoxFor(model => model.NodeName, new { @class = "form-control" })
@Html.ValidationMessageFor(model => model.NodeName, "", new { @class = "alert-error" })
</div>
</div>
<div class="clearfix">
</div>
<br />
<br />
<div class="col-md-12">
<div>
<div class="pull-left">
<input type="submit" id="savenode" value="S A V E" class="btn btn-primary" />
</div>
<div class="pull-right">
<input type="button" id="closePopOver" value="C L O S E" class="btn btn-primary" />
</div>
</div>
</div>
<div class="clearfix">
</div>
}
</div>
</div>
</div>
History
- May 09, 2017: First version