Tree Grid Demo

Tree grid is achieved by setting the GridModelBuilder.GetChildren, this function should return an IQueryable<T> when the item is a node, or an Awe.Lazy when its a lazy node. For lazy loading GetItem also needs to be set, it will be executed in the lazy request, GetItem is also used by the api.updateItem. In the constructor of the GridModelBuilder only root items are set.

Tree grid, basic features

simple tree grid, without lazy loading
TreeGrid/Index.cshtml
@(Html.Awe().Grid("TreeGrid1")
.Url(Url.Action("SimpleTree", "TreeGrid"))
.Columns(
new Column { Bind = "Name" },
new Column { Bind = "Id", Width = 100 })
.Persistence(Persistence.View)
.Groupable(false)
.PageSize(3)
.Height(400)
)
Demos/Grid/TreeGridController.cs
public ActionResult SimpleTree(GridParams g)
{
var rootNodes = Db.TreeNodes.Where(o => o.Parent == null);

var builder = new GridModelBuilder<TreeNode>(rootNodes.AsQueryable(), g)
{
Key = "Id", //required for the TreeGrid
GetChildren = (node, nodeLevel) => Db.TreeNodes.Where(o => o.Parent == node).AsQueryable(),
Map = o => new { o.Name, o.Id }
};

return Json(builder.Build());
}

Lazy loading Nodes, Tree Grid

TreeGrid/Index.cshtml
@(Html.Awe().Grid("LazyTreeGrid")
.Url(Url.Action("LazyTree", "TreeGrid"))
.Columns(
new Column { Bind = "Name" },
new Column { Bind = "Id", Width = 100 })
.Persistence(Persistence.View)
.ColumnsPersistence(Persistence.Session)
.PageSize(3)
.Height(400))
Demos/Grid/TreeGridController.cs
public ActionResult LazyTree(GridParams g)
{
var rootNodes = Db.TreeNodes.Where(o => o.Parent == null);

var builder = new GridModelBuilder<TreeNode>(rootNodes.AsQueryable(), g)
{
Key = "Id", // required for the TreeGrid
GetChildren = (node, nodeLevel) =>
{
var children = Db.TreeNodes.Where(o => o.Parent == node).AsQueryable();

// set this node as lazy when it's above level 1 (relative), it has children, and this is not a lazy request already
if (nodeLevel > 1 && children.Any() && g.Key == null) return Awe.LazyNode;

return children;
},
GetItem = () => // used for lazy loading
{
var id = Convert.ToInt32(g.Key);
return Db.TreeNodes.FirstOrDefault(o => o.Id == id);
},
Map = o => new { o.Name, o.Id }
};

return Json(builder.Build());
}

Tree Grid with CRUD operations

TreeGrid/Index.cshtml
@(Html.Awe().InitPopupForm()
.Name("createNode")
.Height(200)
.Group("tree")
.Url(Url.Action("Create", "TreeGrid"))
.Success("nodeCreated"))

@(Html.Awe().InitPopupForm()
.Name("editNode")
.Height(200)
.Group("tree")
.Url(Url.Action("Edit", "TreeGrid"))
.Success("nodeUpdated"))

@(Html.Awe().InitPopupForm()
.Name("deleteNode")
.Height(200)
.Modal(true)
.Group("tree")
.Url(Url.Action("Delete", "TreeGrid"))
.Success("nodeDeleted")
.OnLoad("utils.delConfirmLoad('CrudTreeGrid')")
.Parameter("gridId", "CrudTreeGrid"))

<script>
// needed to handle crud when search criteria is present
// (e.g. if you add a child and update the node it might not show up because of the search criteria)
var nodesAdded = [];
$(function () {
$('#CrudTreeGrid').on('aweload', function () {
nodesAdded = [];
});
});

function nodeCreated(res) {
var $grid = $('#CrudTreeGrid');
nodesAdded.push(res.Node.Id);

//update parent if present of reload the grid with empty sorting and grouping rules
if (res.ParentId) {
var xhr = $grid.data('api').update(res.ParentId, { nodesAdded: nodesAdded });
$.when(xhr).done(function () {
$("#CrudTreeGrid [data-k=" + res.Id + "]").addClass("awe-changing").removeClass("awe-changing", 1000);
});
} else {
var $row = $grid.data('api').renderRow(res.Node, 1);// render row as lvl 1 (root)
$grid.find('.awe-tbody').prepend($row);
}
}

function nodeUpdated(res) {
var api = $('#CrudTreeGrid').data('api');
var xhr = api.update(res.Id);
$.when(xhr).done(function () {
var $row = api.select(res.Id)[0];
var altcl = $row.hasClass("awe-alt") ? "awe-alt" : "";
$row.switchClass(altcl, "awe-changing", 1).switchClass("awe-changing", altcl, 1000);
});
}

//remove the deleted node and its children
function nodeDeleted(res) {
var rows = $('#CrudTreeGrid').data('api').select(res.Id);
var duration = rows.length > 3 ? 1000 : 500;
var promises = $.map(rows, function (row, i) {
return $.Deferred(function (dfd) {
row.fadeOut(duration - Math.min(700, i * 50), function () {
row.remove();
dfd.resolve();
});
});
});

$.when.apply($, promises).done(function () {
//reload grid when page empty
if (!$('#CrudTreeGrid .awe-row').length)
$('#CrudTreeGrid').data('api').load();

//update parent node, if there is one
if (res.ParentId) {
$('#CrudTreeGrid').data('api').update(res.ParentId, { nodesAdded: nodesAdded });
}
});
}
</script>

<div class="bar">
<button type="button" class="awe-btn" style="float: left;" onclick="awe.open('createNode')">add root</button>
<div style="text-align: right;">
@Html.Awe().TextBox("txtname").Placeholder("search...").CssClass("searchtxt")
</div>
</div>

@(Html.Awe().Grid("CrudTreeGrid")
.Url(Url.Action("CrudTree", "TreeGrid"))
.Columns(
new Column { Bind = "Name" },
new Column { Bind = "Id", Width = 100 },
new Column { ClientFormat = GridUtils.AddChildFormat(), Width = 100 },
new Column { ClientFormat = GridUtils.EditFormat("editNode"), Width = 50 },
new Column { ClientFormat = GridUtils.DeleteFormat("deleteNode"), Width = 50 })
.Persistence(Persistence.View)
.ColumnsPersistence(Persistence.Session)
.Reorderable(true)
.Resizable(true)
.PageSize(3)
.Parent("txtname", "name")
.Height(400))
Demos/Grid/TreeGridController.cs
//clear nodesAdded on aweload
public ActionResult CrudTree(GridParams g, string name, int[] nodesAdded)
{
name = name ?? "";
nodesAdded = nodesAdded ?? new int[] { };

var result = Db.TreeNodes.Where(o => o.Name.ToLower().Contains(name.ToLower()) || nodesAdded.Contains(o.Id)).ToList();
var searchResult = result.ToList();

foreach (var treeNode in searchResult)
{
AddParentsTo(result, treeNode);
}

var roots = result.Where(o => o.Parent == null);

var model = new GridModelBuilder<TreeNode>(roots.AsQueryable(), g)
{
Key = "Id",
GetChildren = (node, nodeLevel) =>
{
// non lazy version
//var children = result.Where(o => o.Parent == node).ToArray();
//return children.AsQueryable();

var children = result.Where(o => o.Parent == node).AsQueryable();

// set this node as lazy when it's above level 1 (relative), it has children, and this is not a lazy request already
if (nodeLevel > 1 && children.Any() && g.Key == null) return Awe.LazyNode;

return children;
},
GetItem = () => // used for grid api updateItem
{
var id = Convert.ToInt32(g.Key);
return Db.TreeNodes.FirstOrDefault(o => o.Id == id);
},
Map = MapNode
};

return Json(model.Build());
}

private object MapNode(TreeNode node)
{
return new { node.Name, node.Id };
}

private void AddParentsTo(ICollection<TreeNode> result, TreeNode node)
{
if (node.Parent != null)
{
if (!result.Contains(node.Parent))
{
result.Add(node.Parent);
AddParentsTo(result, node.Parent);
}
}
}

public ActionResult Create(int? parentId)
{
return PartialView(new TreeNodeInput { ParentId = parentId ?? 0 });
}

[HttpPost]
public ActionResult Create(TreeNodeInput input)
{
if (!ModelState.IsValid) return View(input);

var parent = input.ParentId != 0 ? Db.Get<TreeNode>(input.ParentId) : null;
var node = new TreeNode { Name = input.Name, Parent = parent };

Db.Insert(node);

var result = new
{
Node = MapNode(node),
ParentId = node.Parent != null ? node.Parent.Id : 0 // we'll refresh the parent when adding child
};

return Json(result);
}

public ActionResult Edit(int id)
{
var node = Db.Get<TreeNode>(id);
return PartialView("Create", new TreeNodeInput { Id = node.Id, Name = node.Name });
}

[HttpPost]
public ActionResult Edit(TreeNodeInput input)
{
if (!ModelState.IsValid) return View("Create", input);

var node = Db.Get<TreeNode>(input.Id);
node.Name = input.Name;
Db.Update(node);
return Json(new { node.Id });
}

public ActionResult Delete(int id, string gridId)
{
var node = Db.Get<TreeNode>(id);

return PartialView(new DeleteConfirmInput
{
Id = id,
Message = string.Format("Are you sure you want to delete node {0} ", node.Name)
});
}

[HttpPost]
public ActionResult Delete(DeleteConfirmInput input)
{
var node = Db.Get<TreeNode>(input.Id);
DeleteNode(node);

var result = new
{
node.Id,
ParentId = node.Parent != null ? node.Parent.Id : 0 // we'll refresh the parent to remove collapse button when zero children
};

return Json(result);
}

private void DeleteNode(TreeNode node)
{
var children = Db.TreeNodes.Where(o => o.Parent == node).ToList();
Db.Delete<TreeNode>(node.Id);

foreach (var child in children)
{
DeleteNode(child);
}
}

Use as TreeView

The menu on the left is a TreeGrid with hidden footer and header.
Shared/_Layout.cshtml
<div id="demoMenu">
@{
var action = ViewContext.Controller.ValueProvider.GetValue("action").RawValue.ToString();
var controller = ViewContext.Controller.ValueProvider.GetValue("controller").RawValue.ToString();
}
@Html.Awe().TextBox("msearch").Placeholder("search...").CssClass("msearch")
@(Html.Awe().Grid("Menu")
.DataFunc("getMenuGridFunc()")
.Tag(new { ItemsUrl = Url.Action("GetMenuNodes", "Data") })
.ShowGroupBar(false)
.ShowHeader(false)
.ShowFooter(false)
.Persistence(Persistence.Local)
.Parameter("controller", controller)
.Parameter("action", action)
.Parent("msearch", "search", false)
.RowClassClientFormat(".Selected")
.ColumnWidth(100)
.Columns(new Column { ClientFormatFunc = "renderMenuItem" }))
</div>
<script>
$(function () {
$('#msearch').keyup(function () {
$('#Menu').data('api').load();
});
});
</script>