65.9K
CodeProject 正在变化。 阅读更多。
Home

在 DotNetNuke 中使用 Telerik RadTreeView 管理层次结构

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.69/5 (7投票s)

2010年5月25日

CPOL

9分钟阅读

viewsIcon

45943

downloadIcon

627

文章展示了如何通过创建/重命名/删除节点、拖放、节点延迟(懒加载)来创建层级管理。

XsltDbCategories.PNG

引言

在许多情况下,您都需要编辑树形结构:文章或目录分类、文件和目录结构等。DotNetNuke 5.2 提供了一个出色的 TreeView 控件 – RadTreeView,它支持您编辑或显示层级结构所需的一切。拖放、内联重命名、懒加载等。

在这篇文章中,我将一步步向您展示如何使用 Telerik RadTreeView 控件创建完整的文章分类管理解决方案。该解决方案将支持

  • 将分类树存储在门户数据库中
  • 拖放分类
  • 创建/重命名/删除分类
  • 子分类的懒加载

我使用了一个 XsltDb DotNetNuke 模块 作为集成平台。它可以实例化 ASP.NET 控件,并提供灵活的 JavaScript 和 XML 客户端 API。它将帮助我们通过 JavaScript 代码查询数据库。

本文的在线演示可以在这里找到: http://xsltdb.com/Tree/RadTreeView.aspx

创建数据库

首先,我们需要数据库表和存储过程

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;
此数据库能够存储定向分类树并执行创建/更新/移动/删除操作。要执行脚本,您必须以 DotNetNuke 的 host 用户身份登录,导航到 **Host/SQL** 菜单,粘贴代码,勾选“Run As Script”选项,然后点击 Execute。

创建 RadTreeView 并实现简单操作

现在是时候登录 DotNetNuke 了。首先,您需要将 XsltDb 安装/升级到 01.01.21 或更高版本。创建一个新页面,并在上面放置一个 XsltDb 模块。通过点击“Edit XSLT”链接打开 XsltDb 配置。XsltDb 允许我们以类似于 ascx 文件的方式创建 ASP.NET 控件。因此,我们必须像下面这样创建 <telerik:RadTreeView />

<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> 
正如您所见,RadTreeView 控件的创建方式与您可能在 Visual Studio 中进行的方式相同。我们实际在这里做了什么
  • 声明 **telerik** 标签前缀并引用 Telerik.Web.UI 程序集。
  • 使用 XsltDb 的 <mdo:asp/> 标签创建 ASP.NET 控件区域。在该标签中,我们必须声明所有命名空间(标签前缀)。否则,XSL 转换会将命名空间注入 ASP.NET 控件,这可能导致页面加载失败。
  • 为 RadTreeView 类属性赋值。这通过向 <telerik:RadTreeView/> 标签添加属性来完成。
  • 设置初始节点结构。为此,我们在 <telerik:RadTreeView/> 标签内创建 <Nodes/>。我们还指定了一些节点的属性
    • Text – 节点的显示名称
    • ExpandMode – 使用 ServerSideCallBack 强制 AJAX 加载子节点。
    • AllowEdit 设置为 false,因为我们不想重命名假根节点。
  • 使用 <ContextMenus/> 部分设置节点菜单。RadTreeView 允许我们为不同类型的节点创建多个菜单。我们在这里创建一个带有 3 个项目的菜单,并指定以下属性
    • Text – 菜单项的显示标题
    • Value – 标识菜单项在菜单处理程序中的字符串键
    • ImageUrl – 菜单项图像的 URL(相对或绝对)

如果我们现在保存 XsltDb 配置,我们将看到一个名为“XsltDb Categories”的根节点,该节点带有一个包含创建、重命名和删除项目的上下文菜单。

工作原理:XsltDb 允许我们实例化任何非抽象的 ASP.NET 控件。这是通过使用 Control.ParseControl 函数实现的。此函数安全且快速。因为它从不导致编译。这使我们可以确定 XsltDb 无法执行任何恶意 .NET 代码。实际上,XsltDb 模块执行 XSL 转换,并将结果发送给 ParseControls。尽管这在 .NET 代码方面是安全的方法,但在 SQL 方面却不是。您只需创建一个 SqlDataSource 即可获得对数据库的无限制访问。因此,您必须选中 XsltDb 的 Super Module 选项才能使其正常工作。

现在是时候创建菜单处理程序了。为此,我们可以使用 pageLoad() 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;
    }
  });
}

在 pageLoad 中,我们使用 XsltDb 的 mdo:client-id() 扩展获取 RadTreeView ASP.NET 控件的客户端 ID。我们将此 ID 包含在 JavaScript 代码中。然后,我们使用 telerik 的 $find 函数来检索树视图控件本身。使用 $find 返回的引用,我们可以将处理程序附加到菜单项的点击事件。如您所见,我们将每个菜单项映射到一个单独的 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);
}

创建这些函数后,我们就可以添加和删除节点了。更改不会保存到数据库,也没有验证。但我们展示了在 DotNetNuke 中使用 RadTreeView 的简单性。

更新数据库

为了将创建的节点保存到数据库,我们使用 XsltDb JavaScript API。这非常简单。只需将以下代码添加到配置中

<callable js="createCategory(categoryName, parentID)">
  <xsl:value-of select="mdo:xml('xsltdb-category-create', 'cat',
     mdo:request('parentID'),
     mdo:request('categoryName')
  )//CategoryID" />
</callable> 

这段代码实际做了什么?当页面加载时,这段代码不会被执行,但会创建一个名为“createCategory”的 JavaScript 函数。如果您在客户端调用此函数,它将把参数值发送到服务器并执行 XSL 转换。我们这里看到的 XSLT 只是一个存储过程调用。由于我们使用标识列作为行 ID,该过程将返回新创建分类的 ID,然后 createCategory 将其发送给调用者。

createCategory 可以同步调用

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

也可以异步调用

create Category("New Root Category", -1, function(newId){
  alert(newId);
});

同步调用很简单,但它们会阻塞浏览器,并且在创建分类时不会执行其他脚本、动画 GIF 等。因此,在生产环境中,建议使用异步调用。

现在我们需要调用 createCategory 将新分类保存到数据库。但我们必须在用户输入正确名称后进行。所以我们需要附加一个“nodeEdited”事件的处理程序

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);
});

这是如何工作的?在 JavaScript 中,我们可以在运行时向任何对象添加属性。因此,我们使用“category”属性将 CategoryID 与运行时 JavaScript 节点关联起来。如果我们看到分类 ID 已经被分配,我们就进行更新。否则,我们创建一个新节点,并在创建后分配 ID。节点懒加载(延迟加载)。现在创建延迟节点加载机制。RadTreeView 为我们提供了以下节点加载方法

  • 页面方法
  • Web服务
  • 在“populating”事件处理程序中进行客户端节点创建

页面方法会导致完整的页面创建和 ASP.NET 页面生命周期执行。因此,如果我们不想陷入完整的页面重新计算,我们必须使用 IsPostBack 和 IsInAsyncPostBack 标志来确定在 Page_Load 和 Page_Init 中该做什么。页面上的所有其他模块也必须使用这些标志。因此,对于模块化环境来说,这不是一个好的方法。

Web 服务是一个快速的好方法,但它需要为 DotNetNuke 创建和部署一个 asmx 文件。而且每次更改算法时,您都需要重新部署服务。目前,我不知道有哪个模块可以作为通用 Web 服务(即允许设置 SQL 查询和元数据并根据设置充当服务的模块)。也许 XsltDb 将来会成为这样的服务。

因此,我们切换到 **客户端节点生成**。我们可以使用多种方法查询服务器、获取数据并创建 JavaScript 对象。由于我们处于 XML/XSL 环境中 – 最简单的方法是在服务器上构建 XML 并在客户端进行分析。我们还可以返回 JSON 对象或纯 JavaScript 代码,但我们不会这样做。在客户端获得 XML 后,我们还可以选择如何查询 XML。我们可以使用浏览器的 XML API,或者将 XML 嵌入到当前文档的 DOM 中,并通过 jQuery 访问它。任何方法都可以接受。我们在这里选择嵌入到浏览器的 DOM 中并使用 jQuery。

因此,我们创建了以下 JavaScript 接口

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

创建节点工厂(本文中最简单的方法)

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();
} 

并订阅 populating 和 expanded 事件

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);
  }); 

关于上面代码中关键行的几点说明。

alwaysClosed。此标志用于设置子节点预览。如果我们有一个不经常更改的“静态”树,我们希望仅在特定节点下存在子节点时才显示“+”。实时演示可以在纯延迟加载模式和子节点预览模式下工作。

$("#tmp") 用于在浏览器的 DOM 中临时存储节点。这是一个很好的方法,因为我们在处理纯字符串时不需要担心 HTML 或 JavaScript 的编码/解码操作。XSL/jQuery 会完成所有必需的转换。

在“populating”事件中,我们执行 args.set_cancel(true)。这是必需的,以告知树我们实际上已加载所有节点,它无需加载和填充节点。

ensureLoaded。此函数从服务器加载节点并在客户端填充 RadTreeView 父节点。此函数必须支持链式调用,因为我们可能希望在节点加载后立即进行一些处理。

最后,我们必须手动设置展开状态并将 ExpandMode 设置为 ClientSide,因为我们想告诉 RadTreeView 这个节点不应该被重新加载/重新填充。

拖放

现在我们可以创建/删除/重命名节点,并从服务器读取节点。还剩最后一件事要做——实现拖放层级修改。这是通过订阅 nodeDroppind 事件来完成的

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

      });
    });
  }); 

这里我们按顺序进行 3 个步骤

  • 加载将成为被拖放节点父节点的节点的子节点。
  • 更新数据库中的树结构
  • 更新客户端的树结构
最后,正如您所见,使用 DotNetNuke 5.2 及更高版本,层级管理是一个非常简单的任务。

结论

何时使用此方法

当然,如果您正在创建一个大型复杂的 Web 应用程序,您将使用 Visual Studio 并用 C# 或 VB 创建树控件。但如果您预算有限,时间紧迫,开发功能不多 – 这种方法适合您。您无需创建 Visual Studio 项目,也无需考虑部署。但您可以使用服务器端代码,并且可以实例化 ASP.NET 控件!在 Telerik 控件被添加到 DotNetNuke 后,无需使用不同的第三方或开源组件来处理每个特定任务,就可以轻松创建树管理或文章评分。

使用附件中的文件

  • 将 XsltDb 安装或升级到 01.01.23 或更高版本
  • 创建数据库对象。将 RadTreeView.sql 的内容复制到剪贴板,导航到 Host/SQL 页面,插入脚本,勾选“Run As Script”并点击 Execute。
  • 创建一个新页面,并在上面放置一个 XsltDb 模块实例。点击“Edit XSLT”链接。将 RadTreeView.xslt 的内容粘贴到 XSLT 编辑框中。点击 Update & Publish。

如果一切正确,您应该会看到一个类似于在线演示的页面。

使用的环境和工具

  • Windows Server 2008 / IIS 7.5 / .NET 3.5
  • DotNetNuke 5.4.2
  • XsltDb DotNetNuke 集成模块

链接

© . All rights reserved.