Tree Grid
The tree grid is achieved by setting `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 during the lazy request. `GetItem` is also used by `api.updateItem`. In the constructor of `GridModelBuilder`, only the collection of root items is passed.
Tree grid, basic features
Simple tree grid, get data for the current page from the server.
No lazy loading nodes.
No lazy loading nodes.
TreeGrid/Index.cshtml
@(Html.Awe().Grid("TreeGrid1")
.Url(Url.Action("SimpleTree", "TreeGrid"))
.Columns(
new Column { Bind = "Name", ClientFormat = ".(Name)" },
new Column { Bind = "Price", Width = 100 },
new Column { Bind = "Id", Width = 100 })
.Groupable(false)
.PageSize(3)
.Height(400))
Demos/Grid/TreeGridController.cs
public IActionResult SimpleTree(GridParams g)
{
var rootNodes = Db.TreeNodes.Where(o => o.Parent == null).AsQueryable();
var builder = new GridModelBuilder<TreeNode>(rootNodes, g)
{
KeyProp = o => o.Id,
GetChildrenAsync = async (node, nodeLevel) =>
{
return Db.TreeNodes.Where(n => n.Parent == node).ToArray();
},
Map = o => new {
o.Name,
o.Id,
Price = o.Price.ToString("N2")
}
};
return Json(builder.Build());
}
Lazy Loading Nodes, Tree Grid
Tree grid with lazy loading, nodes are loaded from the server only when expanded.
This improves performance for large datasets by loading child nodes on demand.
On the server side `GridModelBuilder.GetChildren` will return `Awe.Lazy` instead of data for a lazy node.
This improves performance for large datasets by loading child nodes on demand.
On the server side `GridModelBuilder.GetChildren` will return `Awe.Lazy` instead of data for a lazy node.
TreeGrid/Index.cshtml
@(Html.Awe().Grid("LazyTreeGrid")
.Url(Url.Action("LazyTree", "TreeGrid"))
.Columns(
new Column { Bind = "Name" },
new Column { Bind = "Price", Width = 100 },
new Column { Bind = "Id", Width = 100 })
.Persistence(Persistence.Session)
.PageSize(3)
.Height(400))
Demos/Grid/TreeGridController.cs
public IActionResult LazyTree(GridParams g)
{
var rootNodes = Db.TreeNodes.Where(o => o.Parent == null).AsQueryable();
var builder = new GridModelBuilder<TreeNode>(rootNodes, g)
{
KeyProp = o => o.Id,
GetChildrenAsync = async (node, nodeLevel) =>
{
var children = Db.TreeNodes.Where(o => o.Parent == node).ToArray();
// 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.First(o => o.Id == id);
},
Map = MapNode
};
return Json(builder.Build());
}
Tree Grid with CRUD Operations
Tree grid with full CRUD (Create, Read, Update, Delete) support.
You can add root or child nodes, edit node names, and delete nodes (including their subtrees).
The grid uses popups for editing and creating nodes, and supports searching by name.
All operations are handled server-side in the `CrudTree` action of `TreeGridController`, which ensures parent relationships and updates the tree structure accordingly.
You can add root or child nodes, edit node names, and delete nodes (including their subtrees).
The grid uses popups for editing and creating nodes, and supports searching by name.
All operations are handled server-side in the `CrudTree` action of `TreeGridController`, which ensures parent relationships and updates the tree structure accordingly.
TreeGrid/Index.cshtml
@{
var gridId = "CrudTreeGrid";
}
@(Html.InitCrudPopupsForGrid(gridId, "TreeGrid", 200))
<div class="bar">
<button type="button" class="awe-btn" onclick="awe.open('create@(gridId)')">Add root</button>
<div style="margin-left:auto;">
@Html.Awe().TextBox("txtTreeName").Placeholder("Search...").CssClass("searchtxt").ClearButton()
</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("txtTreeName", "name")
.Height(400))
Demos/Grid/TreeGridController.cs
public IActionResult CrudTree(GridParams g, string name = "")
{
var all = Db.TreeNodes.ToList();
var sresult = all.Where(o => o.Name.ToLower().Contains(name.ToLower())).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 Db.Find<TreeNode>(key);
};
gmb.GetChildren = (node, nodeLevel) =>
{
var children = result.Where(o => o.Parent == node);
return gmb.OrderBy(children.AsQueryable());
};
return Json(gmb.Build());
}
private object MapNode(TreeNode node)
{
return new { node.Name, node.Id, Price = node.Price.ToString("N2") };
}
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 IActionResult Create(TreeNodeInput input)
{
if (!ModelState.IsValid) return PartialView(input);
var parent = input.ParentId.HasValue ? Db.Find<TreeNode>(input.ParentId) : null;
var node = new TreeNode { Name = input.Name, Parent = parent };
Db.Add(node);
var result = new
{
node.Id // used to flash the new row if visibile
};
return Json(result);
}
public IActionResult Edit(int id)
{
var node = Db.Find<TreeNode>(id);
return PartialView("Create", new TreeNodeInput { Id = node.Id, Name = node.Name });
}
[HttpPost]
public IActionResult Edit(TreeNodeInput input)
{
if (!ModelState.IsValid) return PartialView("Create", input);
var node = Db.Find<TreeNode>(input.Id);
node.Name = input.Name;
return Json(new { node.Id });
}
public IActionResult Delete(int id)
{
var node = Db.Find<TreeNode>(id);
return PartialView(new DeleteConfirmInput
{
Id = id,
Type = "node",
Name = node.Name
});
}
[HttpPost]
public IActionResult Delete(DeleteConfirmInput input)
{
var node = Db.Find<TreeNode>(input.Id);
DeleteNodeTree(node);
var result = new
{
node.Id
};
return Json(result);
}
private void DeleteNodeTree(TreeNode node)
{
var children = Db.TreeNodes.Where(o => o.Parent == node).ToList();
foreach (var child in children)
{
DeleteNodeTree(child);
}
Db.Remove(Db.Find<TreeNode>(node.Id));
}
Use as TreeView
The menu on the left is a TreeGrid with the footer and header hidden.Shared/Menu.cshtml
<div id="sideCont">
<aside class="o-flexf o-flex-col">
<div id="menuPlaceh"></div>
<div id="demoMenu" class="o-flexf o-flex-col">
<div class="o-flex" style="align-items: center;border-bottom: 1px solid var(--brdcol);">
<div class="o-flex" style="padding: 0 0 0 .7em;">
@MyAweIcons.ZoomIco()
</div>
@(Html.Awe().TextBox(new TextBoxOpt
{
Name = "menuSearch",
Placeholder = "Search...",
CssClass = "o-src msearch",
FieldAttr = new { @class = "msearch" },
ClearBtn = true
}))
</div>
<nav role="navigation" class="scrlh o-flexf">
@(Html.Awe().SideMenu(new SideMenuOpt {
Name = "Menu",
InitFunc = string.Format("site.sideMenuInit('{0}')", Url.Action("GetMenuNodes", "Data"))
}).Persistence(Persistence.Session))
</nav>
</div>
</aside>
</div>
TreeGrid Client Data
Using the awesome TreeGrid with client data.
On the first load, the data will be fetched from the server and stored in the `loadedNodes` variable.
Filtering is done using a parent control. We're binding to the `input` event for real-time updates as you type.
On the first load, the data will be fetched from the server and stored in the `loadedNodes` variable.
Filtering is done using a parent control. We're binding to the `input` event for real-time updates as you type.
TreeGrid/Index.cshtml
<div class="bar">
@(Html.Awe().TextBox("txtFilterClientGrid").Placeholder("Search..."))
</div>
@{
var clientTreeGridId = "ClientDataTreeGrid";
}
@(Html.Awe().Grid(clientTreeGridId)
.DataFunc($"getTreeGridData('{Url.Action("GetMenuNodes", "Data")}')")
.Columns(
new Column { Bind = "Name" },
new Column { Bind = "Keywords" },
new Column { Bind = "Id", Width = 100 })
.Parent("txtFilterClientGrid", "search", false) // false since we bind to input event manually
.Height(400))
<script>
$(function(){
// Reloads the TreeGrid when the input field changes.
$('#txtFilterClientGrid').on('input', () => $('#@clientTreeGridId').data('api').load());
});
let loadedNodes = null;
function getTreeGridData(dataUrl) {
// Fetches data from the server.
const getData = () => $.get(dataUrl);
const buildMenuGridModel = (gp) => {
gp.paging = false;
const search = gp.search || '';
// Splits the search query into individual words for filtering.
const words = search.split(" ").filter(Boolean);
// Filters nodes based on whether they contain all search words.
const result = loadedNodes.filter(node => {
const nodeText = `${node.Keywords} ${node.Name}`.toLowerCase();
return words.every(word => nodeText.includes(word));
});
// Ensures filtered items include their parents for hierarchical integrity.
result.forEach(o => addParentsTo(result, o, loadedNodes));
// Extracts root nodes (those without parents).
const rootNodes = result.filter(o => !o.ParentId);
// Retrieves children of a given node.
const getChildren = (node) => result.filter(o => o.ParentId === node.Id);
// Configures grid row headers.
const makeHeader = (info) => ({
Item: info.NodeItem,
Collapsed: !search && info.NodeItem.Collapsed,
IgnorePersistence: Boolean(search)
});
return aweUtils.gridModelBuilder({
key: "Id",
gp,
items: rootNodes,
getChildren,
defaultKeySort: aweUtils.Sort.None,
makeHeader
});
};
return async (sgp) => {
const gp = aweUtils.getGridParams(sgp);
// If data is already loaded, use it.
if (loadedNodes) {
return buildMenuGridModel(gp);
}
// Fetch data from the server and process it.
loadedNodes = await getData();
return buildMenuGridModel(gp);
};
}
function addParentsTo(res, node, all) {
// Ensures a node's ancestors are included in the filtered results.
if (!node.ParentId) return;
// Check if the parent is already in the result set.
if (!res.some(o => o.Id === node.ParentId)) {
// Locate the parent node and add it.
const parent = all.find(o => o.Id === node.ParentId);
if (parent) {
res.push(parent);
addParentsTo(res, parent, all); // Recursively add ancestors.
}
}
}
</script>
Tree grid with custom rendering
This example demonstrates how to use the custom render mod to control the HTML output of the TreeGrid.
By overriding grid API methods, the grid is rendered using
You can customize node appearance, indentation, group headers, and row rendering logic to match your application's design requirements.
By overriding grid API methods, the grid is rendered using
div elements with custom padding and structure instead of the default table layout.You can customize node appearance, indentation, group headers, and row rendering logic to match your application's design requirements.
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;
// 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) {
return api.ceb() + g.c;
};
// render row
api.renderItem = function (opt) {
let { attr, ceb, content} = opt;
function val(col) {
return aweUtils.gcv(api, col, opt);
}
if (!content) {
content = '';
if (ceb) content += api.ceb();
content += val(aweUtils.columnByBind(o, 'Name'));
}
if (ceb) {
attr.class += ' tgitem awe-ceb';
} else {
attr.class += ' tgitem';
}
return `<div ${awef.objToAttrStr(attr)}>${content}</div>`;
};
}
</script>
See also: TreeGrid inline editing
Comments