Click here to Skip to main content
15,885,035 members
Articles / Programming Languages / XSLT

Using Telerik RadTreeView With DotNetNuke To Manage Hierarchies

Rate me:
Please Sign up or sign in to vote.
4.69/5 (7 votes)
13 Jun 2010CPOL9 min read 45.3K   609   10  
Article shows how to create a hierarchy management with create / rename / delele nodes, drag and drop, nodes deffered (lazy) load.

This article appears in the Third Party Products and Tools section. Articles in this section are for the members only and must not be used to promote or advertise products in any way, shape or form. Please report any spam or advertising.

XsltDbCategories.PNG

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:

SQL
create table {databaseOwner}XsltDb_Category(
    CategoryID int identity primary key,
    ParentID int,
    PortalID int,
    Position float,
    Name nvarchar(128)
)
GO


-- we use float position to let nodes be inserted at any point
-- by assignind position = prev/next +/- 0.5.
-- After this operation we renumber positions to make
-- nodes have round position as before insertion.
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, -- Moving category
    @RefCategoryID int, -- Move oprration destination 
    @relation nvarchar(max) -- Destination type: "inside/over", "before/above", "after/below"
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



-- This procedure selects childern of particular node
-- and preview children of each selected nodes.
-- This is very convinient for "static" trees
-- as it shows "+" sign only if the noe has children.
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

XML
<xsl:text disable-output-escaping="yes">
  <![CDATA[
    <%@ Register TagPrefix="telerik" Namespace="Telerik.Web.UI" Assembly="Telerik.Web.UI" %>
  ]]>
</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.

JavaScript
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:

JavaScript
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

XML
<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:

JavaScript
var newId = createCategory("New Root Category", -1);
alert(newId);

and asyncronous

JavaScript
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:

JavaScript
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:

XML
<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)

JavaScript
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

JavaScript
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

JavaScript
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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Web Developer
Russian Federation Russian Federation
I have started as a C++ developer in far 2000. I'd been developing banking and treasury software for 3 years using C++. In 2003 I switched to .NET at DELL. After that I worked as a project manager on different projects (internal audit, treasury automation, publishing house automation, etc.). Since 2009 I own a small software company specialized in SaaS services and develop a DotNetNuke modules.

Comments and Discussions

 
-- There are no messages in this forum --