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






4.69/5 (7投票s)
文章展示了如何通过创建/重命名/删除节点、拖放、节点延迟(懒加载)来创建层级管理。
引言
在许多情况下,您都需要编辑树形结构:文章或目录分类、文件和目录结构等。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;
创建 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>
- 声明 **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 个步骤
- 加载将成为被拖放节点父节点的节点的子节点。
- 更新数据库中的树结构
- 更新客户端的树结构
结论
何时使用此方法
当然,如果您正在创建一个大型复杂的 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 集成模块
链接
- 在线演示可以在 http://xsltdb.com/Tree/RadTreeView.aspx 页面找到
- 完整代码已附加到文章中。