Tree Grid

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 it is a lazy node.

For lazy loading GridModelBuilder.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 collection of only the root items are passed.

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 })
.Groupable(false)
.PageSize(3)
.Height(400))
Demos/Grid/TreeGridController.cs
public async Task<IActionResult> SimpleTree(GridParams g)
{
var rootNodes = mcx.TreeNodes.Where(o => o.Parent == null).AsQueryable();

var builder = new GridModelBuilder<TreeNode>(rootNodes, g)
{
KeyProp = o => o.Id,
GetChildrenAsync = async (node, nodeLevel) =>
await mcx.TreeNodes.Where(n => n.Parent == node).ToArrayAsync(),
Map = o => new { o.Name, o.Id }
};

return Json(await builder.EFBuildAsync());
}

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.Session)
.PageSize(3)
.Height(400))
Demos/Grid/TreeGridController.cs
public async Task<IActionResult> LazyTree(GridParams g)
{
var rootNodes = mcx.TreeNodes.Where(o => o.Parent == null).AsQueryable();

var builder = new GridModelBuilder<TreeNode>(rootNodes, g)
{
KeyProp = o => o.Id,
GetChildrenAsync = async (node, nodeLevel) =>
{
var children = await mcx.TreeNodes.Where(o => o.Parent == node).ToArrayAsync();

// 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;
},
GetItemAsync = () => // used for lazy loading
{
var id = Convert.ToInt32(g.Key);
return mcx.TreeNodes.FirstAsync(o => o.Id == id);
},
Map = MapNode
};

return Json(await builder.EFBuildAsync());
}

Tree Grid with CRUD operations

TreeGrid/Index.cshtml
@{
var gridId = "CrudTreeGrid";
}

@(Html.InitCrudPopupsForGrid(gridId, "TreeGrid", 200,
cfg: (opt, act) =>
{
if (act == "create")
{
opt.OnSuccess = "nodeCreated";
}
}))
<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 () {
$('#@(gridId)').on('aweload', function () {
nodesAdded = [];
});
});

function nodeCreated(res) {
var grid = $('#@(gridId)');
var api = grid.data('api');

nodesAdded.push(res.Node.Id);

// update parent if present of reload the grid with empty sorting and grouping rules
if (res.ParentId) {
var xhr = api.update(res.ParentId, { nodesAdded: nodesAdded });
$.when(xhr).done(function () {
if (res.Node) {
awe.flash(api.select(res.Node.Id)[0]);
}
});
} else {
var row = api.renderRow(res.Node, 1);// render row as lvl 1 (root)
grid.find('.awe-content .awe-itc').prepend(row);
}
}
</script>

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

@(Html.Awe().Grid(gridId)
.Url(Url.Action("CrudTree", "TreeGrid"))
.Columns(
new Column { Bind = "Name" },
new Column { Bind = "Id", Width = 100 },
new Column { ClientFormat = GridUtils.AddChildFormat(gridId), Width = 120 },
new Column { ClientFormat = GridUtils.EditFormatForGrid(gridId), Width = GridUtils.EditBtnWidth },
new Column { ClientFormat = GridUtils.DeleteFormatForGrid(gridId), Width = GridUtils.DeleteBtnWidth })
.Resizable()
.PageSize(3)
.Parent("txtname", "name")
.Height(400))
Demos/Grid/TreeGridController.cs
// clear nodesAdded on aweload
public async Task<IActionResult> CrudTree(GridParams g, int[] nodesAdded, string name = "")
{
nodesAdded = nodesAdded ?? new int[] { };

var all = await mcx.TreeNodes
.Include(o => o.Parent)
.ToListAsync();

var sresult = all.Where(o => o.Name.Contains(name) || nodesAdded.Contains(o.Id)).ToList();

var result = sresult.ToList();

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

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

var gmb = new GridModelBuilder<TreeNode>(roots, g)
{
KeyProp = o => o.Id,
Map = MapNode
};

gmb.GetItemAsync = async () =>
{
var key = Convert.ToInt32(gmb.GridParams.Key);
return await mcx.FindAsync<TreeNode>(key);
};

gmb.GetChildren = (node, nodeLevel) =>
{
var children = result.Where(o => o.Parent == node);
return gmb.OrderBy(children.AsQueryable());
};

return Json(await gmb.BuildAsync());
}

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 IActionResult Create(int? parentId)
{
return PartialView(new TreeNodeInput { ParentId = parentId });
}

[HttpPost]
public async Task<IActionResult> Create(TreeNodeInput input)
{
if (!ModelState.IsValid) return View(input);

var parent = input.ParentId.HasValue ? await mcx.FindAsync<TreeNode>(input.ParentId) : null;
var node = new TreeNode { Name = input.Name, Parent = parent };

mcx.Add(node);

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

await mcx.SaveChangesAsync();

return Json(result);
}

public async Task<IActionResult> Edit(int id)
{
var node = await mcx.FindAsync<TreeNode>(id);
return PartialView("Create", new TreeNodeInput { Id = node.Id, Name = node.Name });
}

[HttpPost]
public async Task<IActionResult> Edit(TreeNodeInput input)
{
if (!ModelState.IsValid) return View("Create", input);

var node = await mcx.FindAsync<TreeNode>(input.Id);
node.Name = input.Name;
await mcx.SaveChangesAsync();
return Json(new { node.Id });
}

public async Task<IActionResult> Delete(int id)
{
var node = await mcx.FindAsync<TreeNode>(id);

return PartialView(new DeleteConfirmInput
{
Id = id,
Type = "node",
Name = node.Name
});
}

[HttpPost]
public async Task<IActionResult> Delete(DeleteConfirmInput input)
{
var node = await mcx.FindAsync<TreeNode>(input.Id);
await DeleteNodeTree(node);

await mcx.SaveChangesAsync();

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 async Task DeleteNodeTree(TreeNode node)
{
var children = await mcx.TreeNodes.Include(o => o.Parent).Where(o => o.Parent == node).ToListAsync();

foreach (var child in children)
{
await DeleteNodeTree(child);
}

mcx.Remove(await mcx.FindAsync<TreeNode>(node.Id));
}

Use as TreeView

The menu on the left is a TreeGrid with hidden footer and header.
Shared/Menu.cshtml
<div id="sideCont">
<aside>
<div id="menuPlaceh"></div>
<div id="demoMenu">
<input id="msearch" type="text" class="msearch awe-display awe-txt o-src"
placeholder="search..." data-brf="true"
autocomplete="off"
/>
<nav role="navigation">
@(Html.Awe().Grid("Menu")
.Columns(new Column { Id = "col1" })
.DataFunc("site.menuDataFunc('" + @Url.Action("GetMenuNodes", "Data", new { area = string.Empty }) + "')")
.Mod(o => o.CustomRender("sideMenu.menutree").Loading(false))
.CssClass("scrlh")
.RowClassClientFormat(".(Selected)")
.Parent("msearch", "search", false)
.ShowFooter(false)
.ShowHeader(false)
.ShowGroupBar(false)
.Load(false)
.ColumnWidth(100))
</nav>
</div>
</aside>
</div>

Tree grid with custom rendering



Using custom render mod to override api methods and render the grid using divs with padding instead of the default table.
Shared/Demos/TreeGridCustomRender.cshtml
@(Html.Awe().Grid("TreeGridCr")
.Url(Url.Action("SimpleTree", "TreeGrid"))
.Mod(o => o.CustomRender("crtree"))
.Columns(
new Column { Bind = "Name" },
new Column { Bind = "Id", Width = 100 }
)
.Groupable(false)
.PageSize(3)
.Height(400))
<script>
function crtree(o) {
var api = o.api;
var fcol = utils.colf(o.columns).fcol;

// node content wrap
api.ncon = function (p) {
// don't wrap at level 0 and nodetype = items
if (!p.lvl || p.gv.nt == 2) return p.ren();
return '<div style="padding-left:' + p.lvl + 'em;" >' + p.ren() + '</div>';
};

// node
api.nodef = function (p) {
var attr = p.h ? 'style="display:none;"' : '';
if (p.lvl == 1) {
var res = '<div class="tgroot" ' + attr + '>' + p.ren() + '</div>';
return res;
}

return p.ren();
};

// group header content
api.ghead = function (g, lvl) {
return api.ceb() + g.c;
};

// render row
api.itmf = function (opt) {
function val(col) {
return utils.gcv(api, col, opt);
}

var content = '';
if (opt.con) {
content = opt.con;
} else {
if (opt.ceb) content += api.ceb();
content += val(fcol('Name'));
}

if (opt.ceb) {
opt.clss += ' tgitem awe-ceb';
} else {
opt.clss += ' tgitem';
}

var attr = opt.attr;
attr += ' class="' + opt.clss + '"';
opt.style && (attr += ' style="' + opt.style + '"');

return '<div ' + attr + '>' + content + '</div>';
};
}
</script>

See also: TreeGrid inline editing


Comments