Introduction
There are many cases when you need to edit tree structure: article or catalog categories, file and directory structure, etc. DotNetNuke 5.2 offers a great TreeView control – RadTreeView that supports all you need to edit or show hierarchies. Drag and drop, inline rename lazy load, etc.
In this article I show you step-by-step how to create a complete article categories management solution using Telerik RadTreeView control. The solution will support:
- Store category tree in portal database
- Drag-and-drop categories
- Create / rename / delete categories
- Lazy load of child categories
As an integration platform I use an XsltDb DotNetNuke module. It can instantiate ASP.NET controls and provides a flexible javascript and XML client API. It will help us to query database from javascript code.
Live demo for this article can be found here: http://xsltdb.com/Tree/RadTreeView.aspx
Creating Database
First
we need a database tables and stored procedures:
create table {databaseOwner}XsltDb_Category(
CategoryID int identity primary key,
ParentID int,
PortalID int,
Position float,
Name nvarchar(128)
)
GO
create procedure {databaseOwner}XsltDb_Category_NormalizeOrders
@PortalID int
as
begin
declare @renum table(CategoryID int, position int identity);
insert @renum(CategoryID)
select CategoryID from {databaseOwner}XsltDb_Category
where PortalID = @PortalID
order by Position;
update {databaseOwner}XsltDb_Category
set Position = t.Position
from {databaseOwner}XsltDb_Category c
join @renum t on t.CategoryID = c.CategoryID;
end;
GO
create procedure {databaseOwner}[{objectQualifier}mdo_xslt_xsltdb_category_create]
@PortalID int,
@ParentID int,
@Name nvarchar(128)
as
begin
if @ParentID < 1 set @ParentID = null;
insert {databaseOwner}XsltDb_Category(PortalID, Name, ParentID, Position)
values(@PortalID, @Name, @ParentID,
(select MAX(Position)+1 from {databaseOwner}XsltDb_Category where PortalID = @PortalID));
select SCOPE_IDENTITY() as CategoryID;
exec {databaseOwner}XsltDb_Category_NormalizeOrders @PortalID;
end;
GO
create procedure {databaseOwner}[{objectQualifier}mdo_xslt_xsltdb_category_update]
@PortalID int,
@CategoryID int,
@Name nvarchar(max)
as
begin
update {databaseOwner}XsltDb_Category set Name = @Name
where CategoryID = @CategoryID and PortalID = @PortalID
end;
GO
create procedure {databaseOwner}[{objectQualifier}mdo_xslt_xsltdb_category_delete]
@PortalID int,
@CategoryID int
as
begin
delete {databaseOwner}XsltDb_Category
where CategoryID = @CategoryID and PortalID = @PortalID
end;
GO
create procedure {databaseOwner}[{objectQualifier}mdo_xslt_xsltdb_category_move]
@PortalID int,
@CategoryID int,
@RefCategoryID int,
@relation nvarchar(max)
as
begin
declare @NewPosition float;
declare @NewParentID int;
if @RefCategoryID < 1 set @RefCategoryID = null;
if @relation = 'inside' or @relation = 'over' begin
select @NewParentID = @RefCategoryID, @NewPosition =
(select MAX(Position) from {databaseOwner}XsltDb_Category where PortalID = @PortalID);
end else begin
select
@NewParentID = ParentID,
@NewPosition = Position + case when @relation = 'before' or @relation = 'above' then -0.5 else +0.5 end
from {databaseOwner}XsltDb_Category where CategoryID = @RefCategoryID and PortalID = @PortalID;
end;
update {databaseOwner}XsltDb_Category set
ParentID = @NewParentID,
Position = coalesce(@NewPosition, 0)
where CategoryID = @CategoryID
and PortalID = @PortalID;
exec {databaseOwner}XsltDb_Category_NormalizeOrders @PortalID;
end;
GO
create procedure {databaseOwner}[{objectQualifier}mdo_xslt_xsltdb_categories]
@PortalID int,
@RootID int
as
begin
if @RootID < 1 set @RootID = null;
if @RootID is null
select
*,
case when exists(select * from {databaseOwner}XsltDb_Category cc where cc.ParentID = c.CategoryID) then 'closed' else null end Closed
from {databaseOwner}XsltDb_Category c
where c.PortalID = @PortalID
and c.ParentID is null
order by c.Position;
else
select
*,
case when exists(select * from {databaseOwner}XsltDb_Category cc where cc.ParentID = c.CategoryID) then 'closed' else null end Closed
from {databaseOwner}XsltDb_Category c
where c.PortalID = @PortalID
and c.ParentID = @RootID
order by c.Position;
end;
This database is capable of storing oriented category tree and perform create / update / move / delete operations. To execute the script you have to login in DotNetNuke as a host user, navigate to the Host/SQL menu, paste the code, check "Run As Script" option and click Execute.
Creating RadTreeView And Imlementing Simple Operations
Now it is time to login in DotNetNuke. First, you have to install/upgrade XsltDb to 01.01.21 or higher. Create a new page and put an XsltDb module on it. Open XsltDb configuration by clicking "Edit XSLT" link. XsltDb allows us to create ASP.NET controls in the way similar to ascx file. So we have to create <telerik:RadTreeView /> as follows
<xsl:text disable-output-escaping="yes">
<![CDATA[
</xsl:text>
<mdo:asp xmlns:telerik="telerik" >
<telerik:RadTreeView
runat="server"
ID="tv1"
EnableDragAndDrop="true"
AllowNodeEditing="true"
EnableDragAndDropBetweenNodes="true"
>
<Nodes>
<telerik:RadTreeNode
Text="XsltDb Categories"
ExpandMode="ServerSideCallBack"
AllowEdit="false" />
</Nodes>
<ContextMenus>
<telerik:RadTreeViewContextMenu ID="MainContextMenu">
<Items>
<telerik:RadMenuItem Text="Create" Value="create" ImageUrl="~/images/add.gif" />
<telerik:RadMenuItem Text="Rename" Value="rename" ImageUrl="~/images/edit.gif" />
<telerik:RadMenuItem Text="Delete" Value="delete" ImageUrl="~/images/delete.gif" />
</Items>
</telerik:RadTreeViewContextMenu>
</ContextMenus>
</telerik:RadTreeView>
</mdo:asp>
As you can see the RadTreeView control is created in the same way as you probably do in Visual Studio. What we have actually done here:
- Declare tag prefix telerik and reference Telerik.Web.UI assmebly.
- Create as ASP.NET controls zone with XsltDb <mdo:asp/> tag. In the tag we must declare all namespaces (tag prefixes). Otherwise XSL transformation will inject namespaces in ASP.NET controls and it can lead to page load failure
- Assign values to the RadTreeView class properties. This is done by adding attributes to the <telerik:RadTreeView/> tag.
- Setup initial node structure. To do that we create <Nodes/> inside <telerik:RadTreeView/> tag. We also specify some properties of the node:
- Text – display name of node
- ExpandMode – use ServerSideCallBack to force ajax children load.
- AllowEdit is set to false as we don't want to rename fake root node.
- Setup a node menu using tag <ContextMenus/> section. RadTreeView allows us to create multiple menus for different types of nodes. We here create one menu with 3 items and assign the following properties:
- Text – the display title of menu item
- Value – string key that identifies menu item in menu handler
- ImageUrl – url (relative or absolute) of menu item image
If we save XsltDb configuration now we would see a root node "XsltDb Categories" that has a context menu with create, rename and delete items.
How this works: XsltDb allows us to instantiate any ASP.NET Control that is not abstract. This is done by using Control.ParseControl function. This function is safe and fast. This is because it never causes compilation. This allows us to be sure that XsltDb can't execute any malicious .NET code. Actually XsltDb module executes XSL transformation and sends the result of it to ParseControls. Althow this is safe method in terms of .NET code tis is not safe in terms of SQL. You can just create SqlDataSource and get unrestricted access to the database. So you must check XsltDb Super Module option to make this work properly.
Now it is time to create menu handler. To do that we can use a pageLoad() javascript function that is called by the framework when page is loaded.
function pageLoad()
{
var treeView = $find("{{mdo:client-id('tv1')}}");
treeView.add_contextMenuItemClicked(function(sender, args){
var menuItem = args.get_menuItem();
var node = args.get_node();
menuItem.get_menu().hide();
switch(menuItem.get_value())
{
case "create": createNode(node); break;
case "rename": if (node.category) node.startEdit(); else alert("Can't edit virtual root!"); break;
case "delete": deleteNode(node); break;
}
});
}
In pageLoad we obtain a client id of RadTreeView ASP.NET control using XsltDb mdo:client-id() extension. We incorporate this id in the javascript code. Then we use telerik $find function to retrieve treeview control itself. Using reference, returned by $find we can attach handler to the menu item click event. As you see we map each menu item to a separate javascript function or (in case of "rename") just starting required action.
Adding and deleting nodes is very simple:
function createNode(node)
{
ensureLoaded(node, function(){
node.expand();
var newNode = addNode("New Category", false, node);
newNode.startEdit();
});
}
function deleteNode(node)
{
node.get_parent().get_nodes().remove(node);
}
After we create these functions we can add and remove nodes. Changes are not saved to the database and there’s no validation. But we show the simplicity of using RadTreeView in DotNetNuke.
Updating Database
To save created node to the database we use XsltDb javascript API. This is very simple. Just add the following code to the configuration
<callable js="createCategory(categoryName, parentID)">
<xsl:value-of select="mdo:xml('xsltdb-category-create', 'cat',
mdo:request('parentID'),
mdo:request('categoryName')
)//CategoryID" />
</callable>
What is this code actually do? When page is loading this code is not executed but javascript function "createCategory" is created. If you call this function on client it would send parameters values to the server and execute XSL transformation. XSLT we see here is just a stored procedure call. As we use identity columns for the row IDs the procedure return ID of newly created category and createCategory sends it to the caller.
createCategory can be called both synchronous:
var newId = createCategory("New Root Category", -1);
alert(newId);
and asyncronous
create Category("New Root Category", -1, function(newId){
alert(newId);
});
Synchronous calls are simple but they block browser and it doesn’t execute other scripts, animate GIFs, etc. while category is being created. So in production environment it is recommended to use asynchronous calls.
Now we have to call createCategory to save new categories to database. But we have to do it after the user input correct name. So we need to attach a handler to "nodeEdited" event:
treeView.add_nodeEdited(function(sender, args)
{
var node = args.get_node();
if ( node.category )
updateCategory(node.category, node.get_text());
else
node.category = createCategory(node.get_text(), node.get_parent().category||-1);
});
How this works? In javascript we can add a property to any object in runtime. So we use a "category" property to associate a CategoryID with runtime javascript node. And if we see that category id already assigned we make an update. Else we create a new node and assign an ID after it would be created.
Nodes Lazy (Deferred) Loading.
Now create a deferred nodes load mechanism. RadTreeView provides us with the following methods of nodes loading
- Page method
- Web Service
- Client - side node creation in “populating” event handler
Page method leads to complete page creation and ASP.NET Page Life cycle execution. So if we don’t want fall in comlete page recalculation we must use IsPostBack and IsInAsyncPostBack flags to determine what to do in Page_Load and Page_Init. And all other modules of page must use this flags too. So this is not good approach for modular environment.
Web service is a good quick method but it requires creating and deploying to DotNetNuke an asmx file. And each time you change algorithm you have to redeploy the service. For the moment I don’t know a module that can act as a universal web servise (i.g. module that allows setup of SQL query and metadata and acts as a service according to settings made). May be XsltDb will be such a service in future.
So we switch to the client-side node generation. We can use a number of methods to query server get data and create javascript objects. As we are in XML/XSL environment – the simplest way is to build xml on server and analyze it on client. We also can return a JSON object or free javascript code but we won't. After we get XML on client we also have an option about how to query the XML. We can use browser XML API or incorporate the XML into current document DOM and access it via jQuery. Any method is acceptable. We choose here incorporating in browser’s DOM and using jQuery.
So we create the following javascript interface:
<callable js="getCategories(ParentID)">
<xsl:variable name="categories" select="mdo:xml('xsltdb_categories', 'cat', mdo:request('ParentID'))" />
<xsl:for-each select="$categories//cat">
<span id="{CategoryID}" closed="{Closed}" name="{Name}" />
</xsl:for-each>
</callable>
Create nodes factory (simplest for the article)
var alwaysClosed = "{{mdo:param('dont-check-children')}}";
function addNode(nodeText, hasChildren, parent)
{
var node = new Telerik.Web.UI.RadTreeNode();
node.set_text(nodeText);
if ( hasChildren || alwaysClosed == "on" )
{
node.set_expandMode(Telerik.Web.UI.TreeNodeExpandMode.ServerSideCallBack);
}
else
{
node.loaded = true;
node.set_expandMode(Telerik.Web.UI.TreeNodeExpandMode.ClientSide);
}
node.set_imageUrl("/images/folder.gif");
parent.get_nodes().add(node);
return node;
}
function ensureLoaded(parent, callback)
{
if ( ! parent.loaded ) {
parent.showLoadingStatus(treeView.get_loadingMessage(), treeView.get_loadingStatusPosition());
parent.loaded = true;
getCategories(parent.category||-1, function(children){
parent.hideLoadingStatus();
parent.set_expandMode(Telerik.Web.UI.TreeNodeExpandMode.ClientSide);
$("#tmp").html(children);
$("#tmp").find("span").each(function(){
var newNode = addNode($(this).attr("name"), $(this).attr("closed"), parent);
newNode.category = $(this).attr("id");
});
if ( callback ) callback();
});
}
else if ( callback ) callback();
}
And subscribe on populating and expanded events
treeView.add_nodePopulating(function(sender, args){
var node = args.get_node();
if ( node.loaded ) return;
ensureLoaded(node);
args.set_cancel(true);
});
treeView.add_nodeExpanded(function(sender, args){
var node = args.get_node();
node.set_expandMode(Telerik.Web.UI.TreeNodeExpandMode.ClientSide);
node.set_expanded(true);
});
Some words about key lines in code above.
alwaysClosed. This flag is used to setup a children preview. If we have a "static" tree that is changed not very often we would like to show "+" only if there are children under the particular node. The Live Demo can work in both pure lazy load mode and children preview mode.
$("#tmp") is used to temporary store nodes in browser's DOM. This is good approach as we don’t need to remember about HTML or javascript encoding/decoding operations when we work with free strings. XSL/jQuery do all required conversions.
At the "populating" event we perform args.set_cancel(true). This is required to inform the tree that we actually loaded all nodes and it doesn’t have to load and populate nodes.
ensureLoaded. This function loads nodes from server and populates RadTreeView parent node on client. This function must support chaining as far as we may want to make some processing exactly after nodes are loaded.
Finally we have to manually set expanded state and change ExpandMode to ClientSide as we want to tell the RadTreeView that this node must not be reloaded/repopulated.
Drag And Drop
Now we can create / delete / rename nodes, read nodes from server. One thing is left to do – implement drag and drop hierarchy modifications. This is done by subscribing on nodeDroppind event
treeView.add_nodeDropping(function(sender, args){
var sourceNode = args.get_sourceNodes()[0];
var destinationNode = args.get_destNode();
ensureLoaded(destinationNode, function(){
moveCategory(sourceNode.category, destinationNode.category||-1, args.get_dropPosition(), function(){
sourceNode.collapse();
sourceNode.get_parent().get_nodes().remove(sourceNode);
if(args.get_dropPosition() == "over")
destinationNode.get_nodes().add(sourceNode);
var destinationParent = destinationNode.get_parent();
var index = destinationParent.get_nodes().indexOf(destinationNode);
if(args.get_dropPosition() == "above" )
destinationParent.get_nodes().insert(index, sourceNode);
if(args.get_dropPosition() == "below")
destinationParent.get_nodes().insert(index+1, sourceNode);
});
});
});
Here we sequentially make 3 steps:
- Load children of the node that will be a parent of the dropped node.
- Update tree structure in database
- Update tree structure on client
Finally, as you see, Hierarchy management is a very simple task with DotNetNuke 5.2 and later.
Conclusion
When to use this approach
Of course if you are creating large sophisticated web application you will use Visual Studio and create tree controls in C# or VB. But if you have a small budget, lack of time and not so many features to develop – this approach is for you. You don’t need to create a Visual Studio project, you don’t need to think about deployment. But you can use server side coding and you can instantiate ASP.NET controls! After the Telerik controls was added to the DotNetNuke it is very simple to create a tree management or article rating without using different third-party or open source components for each particular task.
Using files from attachment
- Install or upgrade XsltDb to 01.01.23 or higher
- Create database objects. Copy content of RadTreeView.sql to clipboard, navigate to Host/SQL page, insert script, check "Run As Script" and click Execute.
- Create a new page, put an instance of XsltDb module on it. Click "Edit XSLT" link. Paste content of RadTreeView.xslt into XSLT edit box. Click Update & Publish.
If everithing is correct you should get a page similar to live demo.
Environment and tools used:
- Windows Server 2008 / IIS 7.5 / .NET 3.5
- DotNetNuke 5.4.2
- XsltDb DotNetNuke integration module
Links