园子里的牛人,真是特别多,不得不让人佩服。很多又酷又帅的效果拿过来略作修改就可以用,爽之余还真得感谢他们的无私奉献。
话说今天看到了一个TreeView 效果真是帅呆了,截个图如下:
该博客的地址:http://www.cnblogs.com/xuanye/archive/2009/10/26/1590250.html
博主是:假正经哥哥
大家没有看的可以先去看一下。
从看到这个效果的那刻起,我就深深的被它吸引。自己平时很多的地方其实也是可以用到TreeView的,但因为自己做出来的东西自己都很不爽,所以一般的尽量用 DropDownList来处理类似的效果,虽说没有这个树好看,但开发起来比较快。我把作者的代码和js、css、图片 一个一个的拿下来,自己运行,感觉真的很爽,而看他的代码,是通过一个json的对象来填充数据项。既然这样,那我能不能在服务器端生成相应的json 呢?如果生成成功的话,那不是就可以轻松的把这种很酷的东西用到自己的项目中了?? 之前看过老赵的在服务器生成客户端验证的做法,很符合自己的想法,今天我也来试试,通过在服务器端生成的相应的数据来使用该牛人的树形控件:)。
首先说一下:这个控件还是原作者的(如果有版权也是他的),甚至客户端的代码都基本不用修改动,我要做的是轻松生成相应的数据。废话不说了,开始吧:
第一步:设计树节点类
根据原文 js 代码中的 json 格式进行分析,我想使用一个类来表示这个json 的,类的设计如下:
TreeItem
public class TreeItem
{
public TreeItem()
{
showcheck=true;
checkstate = 0;
complete = true;
isexpand = false;
}
属性#region 属性
/**//// <summary>
/// 项目ID
/// </summary>
public int id { get; set; }
/**//// <summary>
/// 项的文本
/// </summary>
public string text { get; set; }
/**//// <summary>
/// 项的值
/// </summary>
public string value { get; set; }
/**//// <summary>
/// 是否显示选择框
/// </summary>
public bool showcheck { set; get; }
/**//// <summary>
/// 子节点是否展开
/// </summary>
public bool isexpand { set; get; }
/**//// <summary>
/// 子节点选择框的状态
/// </summary>
public int checkstate { get; set; }
/**//// <summary>
/// 是否有子节点
/// </summary>
public bool hasChildren { get { return (ChildNodes != null && ChildNodes.Count > 0); } }
/**//// <summary>
/// 是否完成(用于区分是否要异步load数据)
/// 此处之前有点误解,以为是标注是否是某层的最后一个节点,如果是有在最后一个节点才给 true,其它的地方都是给的 true.
/// 但在生成的树中,点击含有子树的节点时,始终显示 "loading",通过分析 tree.js 才知道 这个地方的真正意义,对于非异步的该字段的取值都为1
/// </summary>
public bool complete { get; set; }
/**//// <summary>
/// 子节点列表
/// </summary>
public List<TreeItem> ChildNodes { get; set; }
#endregion 属性
}
类很简单,大家看代码就明白了。为了模拟json的嵌套结构,在类中使用了一个 List<TreeItem> ChildNodes 来实现这种嵌套。
第二步:为类添加相应的方法:
要把类和成json 字符串,最简单的方法是使用 System.Web.Script.Serialization 命名空间中的 JavaScriptSerializer 类,知道这些就很简单了,我们为类 TreeItem 添加一个 ToJsonString() 的公有方法,代码如下:
Code
/**//// <summary>
/// 生成 JSON 字符串,
/// </summary>
/// <returns></returns>
public string ToJsonString()
{
JavaScriptSerializer jss = new JavaScriptSerializer();
string sJson = jss.Serialize(this);
return "var treedata=[" + sJson + "];";
}
为了测试结果是否正确,可以写一个模拟测试的方法,看其输出。方法很简单,为了避免凑字之嫌,就不帖出来了,测试后生成的结果如下:
基本上和原文要求的json格式一致。但也发现了一个问题,当TreeView 类中 List<TreeItem> ChildNodes 没有数据时,添加子节点会出现 空引用的错误 ,如果在构造函数中先 new 一下的话,不会报错,但生成的 json 中 ChildNodes为[],而原来要求的是:ChildNodes:null。这个问题很好办:加上一个方法,来添加子节点,如果子节点没有分配内存,则先分配后添加,这样可以保证有子项时不会报错,没有子项时能生成 null.方法代码如下:
Code
/**//// <summary>
/// 添加一个子节点
/// </summary>
/// <param name="ti"></param>
/// <returns></returns>
public List<TreeItem> Add(TreeItem ti)
{
if (ChildNodes == null) ChildNodes = new List<TreeItem>();
ChildNodes.Add(ti);
return ChildNodes;
}
第三步:开始使用TreeView显示用服务器端生成的数据
好了,现在我们试试添加一个“一般处理程序”来生成相应的json子句。
添加 “GetTreeData.ashx” 代码如下:
ProcessRequest
public void ProcessRequest (HttpContext context) {
context.Response.ContentType = "text/plain";
TreeItem tdata = new TreeItem() { id = 0, checkstate = 0, complete = true, isexpand = true, showcheck = true, text = "所有项目", value = "" };
for (int i = 1; i <= 20; i++)
{
TreeItem t = new TreeItem() { id = i, text = i.ToString(), value = i.ToString(), showcheck = true, isexpand = false, complete = true };
if (i % 3 == 2)
{
//模拟多级树节点
t.ChildNodes = new List<TreeItem>();
t.ChildNodes.Add(new TreeItem() { id = i * 999 + 1, text = (i * 999 + 1).ToString(), value = (i * 999 + 1).ToString(), showcheck = true, complete = true });
t.ChildNodes.Add(new TreeItem() { id = i * 999 + 2, text = (i * 999 + 2).ToString(), value = (i * 999 + 2).ToString(), showcheck = true, complete = true });
t.ChildNodes.Add(new TreeItem() { id = i * 999 + 3, text = (i * 999 + 3).ToString(), value = (i * 999 + 3).ToString(), showcheck = true, complete = true });
}
if (t.hasChildren) t.ChildNodes[t.ChildNodes.Count - 1].complete = true;
tdata.Add(t);
}
tdata.ChildNodes[tdata.ChildNodes.Count - 1].complete = true;
context.Response.Write(tdata.ToJsonString());
}
代码中当 I 模 3余 2时,模拟生成了一些二级子项目。为的是能看到多级时的效果。
把原作者的代码修改一下,将原来引用json数据的代码改为引用我们生成的数据:
<!-- <script src="SampleData/tree2.js" type="text/javascript"></script> -->
<script src="Handler/GetTreeData.ashx" type="text/javascript"></script>
其他的都不用改动,运行程序后,看到效果如下:
至此,已经可以通过 一句“context.Response.Write(tdata.ToJsonString());”来使用这个酷树了。
看看是不是很简单的就可以重用了?
当然了,如果仅仅只是这样,也不算是什么扩展了,后面还会继续扩展,以使其他更适合更方便我们使用。
第四步:选中荐的扩展
在平时的使用中,经常要对树的选中项进行修改,这个时候就要先把原来选中的显示出来,我们为了方便在服务器端构造的json 能显示已经选中项,所以第一个扩展就是要增加一个选中项的方法。当然想这个方法时,为了能忠实的还原选项,我先写了一个方法,然后再对其进行了三次重构,以保证能正确的选中项,而且还能正确的还原上级节点的选中状态,过程就不多说了, 上代码:
SelectItem
/**//// <summary>
/// 选择相应的子节点
/// </summary>
/// <param name="arrId"></param>
/// <returns></returns>
public TreeItem SelectItem(int[] arrId)
{
return SelectImte(this, arrId);
}
private TreeItem SelectImte(TreeItem ti, int value)
{
ti.checkstate = 1;
if (ti.hasChildren)
{
for (int i = 0; i < ti.ChildNodes.Count; i++)
{
SelectImte(ti.ChildNodes[i], 1);
}
}
return ti;
}
private TreeItem SelectImte(TreeItem ti, int[] arrId)
{
if (arrId.Any(p => p.ToString() == ti.value))
{
ti.checkstate = 1;
if (ti.hasChildren)
{
for (int i = 0; i < ti.ChildNodes.Count; i++)
{
SelectImte(ti.ChildNodes[i], 1);
}
}
}
else
{
if (ti.hasChildren)
{
for (int i = 0; i < ti.ChildNodes.Count; i++)
{
SelectImte(ti.ChildNodes[i], arrId);
}
int nCount = ti.ChildNodes.Count(p => p.checkstate == 1);
if (nCount == 0)
{
ti.checkstate = 0;
}
else if (ti.ChildNodes.Count == nCount)
{
ti.checkstate = 1;
}
else
{
ti.checkstate = 2;
}
}
}
return ti;
}
说明一下:公有方法,只需要传入id数组就行了,两个重载的私有方法辅助选中项。还原时要注意的是:父项被选中,则所有的子项都选中,子项都没有选中则父项也不选中,子项有选中,但没有全选,则父项是半选状态。
现在我们修改 GetTreeData.ashx 中的代码,来实现选项还原:
Code
//
tdata.SelectItem(new int[] {2,3,4,5,6,7,8 });
tdata.ChildNodes[tdata.ChildNodes.Count - 1].complete = true;
context.Response.Write(tdata.ToJsonString());
效果:
看就这么一句 tdata.SelectItem(new int[] {2,3,4,5,6,7,8 }); 是不是很方便?
第五:对从数据库的取数据扩展
在平常的项目中,城市分级,商品类别分级、目录结构等,以及部门等都需要这样的数据树来达到直观的显示效果,现在我们以从级分类为例子,从数据库读取数据,并生成相应的json 数据。当然了,你每次要用到时候都象上面那样写也行,如果要想重用,并且使用时候修改尽量的少,那么最好是进行扩展。
在我们的项目中,用到较多的多级分类列表的数据表设计基本都差不多,在这里我们设计一个简单的但常见的表结构来存放分类数据,并最终实现从数据库中提取数据并生成需要的 json 数据。
数据表脚本如下:
Code
/**//* -- 生成表的脚本 -- */
CREATE TABLE [dbo].[TreeList](
[Id] [int] IDENTITY(1,1) NOT NULL,
[Text] [nvarchar](50) NULL,
[Parent] [int] NOT NULL
) ON [PRIMARY]
GO
ALTER TABLE [dbo].[TreeList] ADD CONSTRAINT [DF_TreeList_Parent] DEFAULT ((0)) FOR [Parent]
GO
为了直观,截个图:
1、 先建立一个静态类,来扩展TreeItem类:
public static class TreeItemHelper
{
}
2、 添加扩展方法:
Code
/**//// <summary>
/// 生成树类
/// </summary>
/// <param name="ti">要装载的树类根节点</param>
/// <param name="drs">数据表</param>
/// <param name="sId">父类ID</param>
/// <returns></returns>
private static TreeItem GetTreeItemFromDataTable(this TreeItem ti, EnumerableRowCollection<DataRow> drs, string sId)
{
ti.ChildNodes = new List<TreeItem>();
var drs0 = drs.Where(p => p.Field<int>("parent").ToString() == sId);
foreach (DataRow dr in drs0)
{
string parent = dr["Id"].ToString();
var ti0 = new TreeItem() { id = int.Parse(parent), value = parent, text = dr["text"].ToString() };
ti0 = ti0.GetTreeItemFromDataTable(drs, parent);
ti.Add(ti0);
}
return ti;
}
这是一个递归的方法,很简单。当然在调用时,我们可以更简单一点,因为第一级的分类的父ID都是从0开始的(呵呵我的这个表的逻辑是这样的:)),而且我们只需要传入 DataTable 就行了,所以我们重载一下:
Code
/**//// <summary>
/// 由DataTable 生成树类
/// </summary>
/// <param name="ti">要装载的树类根节点</param>
/// <param name="dt">数据表</param>
/// <param name="sId">父类ID</param>
/// <returns></returns>
public static TreeItem GetTreeItemFromDataTable(this TreeItem ti, DataTable dt, string sId)
{
return ti.GetTreeItemFromDataTable(dt.AsEnumerable(), sId);
}
3、 使用
我们添加一个新的处理程序 GetTreeDataFromDB.ashx ,并在其中添加如下代码:
Code
public void ProcessRequest (HttpContext context)
{
context.Response.ContentType = "text/plain";
//获取数据库中的分类数据
DataSet ds=new DataSet();
try
{
SqlDataAdapter da = new SqlDataAdapter("SELECT * FROM [TreeList]", ConfigurationManager.ConnectionStrings["ConnectionString"].ConnectionString);
da.Fill(ds);
TreeItem ti = new TreeItem() { id = 0, text = "所有选项", value = "0", isexpand=true};
ti.GetTreeItemFromDataTable(ds.Tables[0], "0");
context.Response.Write( ti.ToJsonString());
//tdata.SelectItem(new int[] { 2, 3, 4, 5, 6, 7, 8 });
}
catch
{
context.Response.Write("var treedata=null;");
}
}
4、 效果:
数据表中数据截图:
运行效果截图:
当然了,如果数据结构不一样,那么只需要简单的修改一下服务器端的代码就行了,丝毫可以不用去改到客户端的代码,就能达到完美的复用。
写到这里,突然想到了一点,也许这个以后在其他的项目中还要重用,为了能够快速的回忆起这个类怎么使用,以及数据表脚本是怎么样的,我做的服务基本上都会有一个 Help()方法,用来告诉用户这个类是干什么用的,或是怎么用。这样既可以备忘,又可以在别人使用这个类时不用看源代码就能很好的使用。做法很简单,我们再扩展两个方法,以备不时之需:
Code
/**//// <summary>
/// 获得本类的使用说明和修改注意事项,备忘:)
/// </summary>
/// <param name="ti"></param>
/// <returns></returns>
public static string Help(this TreeItem ti)
{
return @"是什么?怎么用?如何修改?";
}
/**//// <summary>
/// 获得生成数据库分类表的SQL脚本
/// </summary>
/// <param name="ti"></param>
/// <returns></returns>
public static string GetTableSql(this TreeItem ti)
{
return @"
/* -- 生成表的脚本 -- */
CREATE TABLE [dbo].[TreeList](
[Id] [int] IDENTITY(1,1) NOT NULL,
[Text] [nvarchar](50) NULL,
[Parent] [int] NOT NULL
) ON [PRIMARY]
GO
ALTER TABLE [dbo].[TreeList] ADD CONSTRAINT [DF_TreeList_Parent] DEFAULT ((0)) FOR [Parent]
GO
";
}
这样以后要重建表,就可以很方便的使用 ti.GetTableSql()获得表结构的脚本。
更有甚者,你可以在第一次使用时,检测有没有应相应的数据库表支持,没有而自动生成相应的表。具体的就不多写了,
最后感慨一下,写东西真累啊,所以还想再一次感谢 那位假正经哥哥的辛苦劳动和无私分享!以后都受用了。
附:原代码-TreeView.rar
开发运行环境:win7 + IE8 + VS2008 + SQL2008