最近我们的项目中需要用到树型菜单,以前使用WebForm时,树型菜单有微软提供的控件,非常方便,但现在需要在asp.netmvc中使用树形菜单,先说明下我们对树形菜单的需求:
1:需要支持CheckBox,允许对菜单项进行选择;
2:当选择父菜单时,它下面的子菜单全部选中;
3:当取消父菜单的选中状态时,下面的子菜单也全部取消;
4:要比较方便的与MVC结合。
初步思路:
思路一:jquery相关的树形菜单插件,由于项目中有应用到jquery,所以不考虑采用其它js框架的产品。
思路二:asp.net mvc相应的控件,这里指的控制就好比分页控件之类,基本思路就是扩展HtmlHelper来实现,Html逻辑一般都封闭在dll中。
经过一轮筛选后的结果:
思路一:基于js的树形菜单果然有很多,最终我选择了https://github.com/daredevel/jquery-tree,从demo展示来看,它完全能够满足我上面提到的前三个需求。
思路二:Telerik也有相应的树形菜单控件,它能够很好的结合MVC,最大特点是将View上的树形菜单数据传递给Controller很直观。
最终方案:
由于上面的两个产品不能完全符合我的要求,所以结合jquery-tree以及Telerik treeview设计理念来实现自己的树形菜单是最佳选择。
jquery-tree本身只是一个基于前端的菜单,我们要想传递数据给Controller,唯一比较方便的方法就是通过ajax,但这需要开发人员有比较强的js操纵能力。
Telerik treeview,有点杀鸡用牛刀的意思,Telerik提供了一整UI解决方案,如果我们只用其中的一个功能就引入它,有些不适时宜,但我非常喜欢它给出的思路,它能很好的将View的
菜单数据传递给Controller,但Telerik由于提供的是控件,我们不太方便对它的样式做修改,我们更希望能够自由的控制UI显示逻辑。
题外话:关于数据结构
记的有一年,我去一家公司面试,当时估计是技术人员都不在,所以安排了一位年龄上比较大的面试官来面试我,目测应该是位级别不低的领导,当时不免有点小紧张。经过一番自我
介绍以及工作经验介绍后,他现场给我出了一道题:
写一个程序,打印出公司的组织结构图,比如最上层是CEO,下面是VP......
我当时跟很大一部分.net程序员一样,做过几年项目后,什么数据结构啊,算法呀早就不记得了,那一刻只想到这是和树相关的数据结构,但不知为何,只想到了二叉树,心想总算想到
了,于是开始定义二叉树数据结构,采用递归遍历数据,总之脑袋一片空白,写了一会,领导见我还没写完,有些不耐烦了,问我写完吗,我回答说还差一点,他等了一会见我还没写完,就
说先给我看看,于时直接过来拿走我未写完的代码,我是多么的想争取那次机会呀,当时多么的舍不得交出去,最后的结果可想而知。
并不是面试官问的问题有多么难,他考的问题只不过是计算机最基础的东西,而我正好忘记了。到现在我也面试过一些候选人,我也比较注意候选人的基础知识,其实说实在的,像做一
些.net相关的企业级开发,大部分情况下我们是不会用到复杂数据结构的,算法就更不用说了,只有少数人会用到这些高级的东西。但这些数据结构以及算法知识会让你的视野更加开阔,在
特定情况下能够给你一些方向,如果你对那块领域没有接触过,你是不会朝那方面想的。
这次的树形菜单正好能够解答我以前那位面试官的问题。
树形菜单数据结构:
首先它属于树形数据结构,但不能定义成二叉树,因为一个总裁下面不可能只有两个副总,一个经理下面也不可能只有两个员工,下属应该是大于等于0的数据。
下面是我定义的菜单数据对象:
public class MyTreeViewItem
{
public IList<MyTreeViewItem> Items { get; set; }
public string Value { get; set; }
public bool Checked { get; set; }
public string Text { get; set; }
public string Index { get; set; }
public MyTreeViewItem Parent { get; set; }
public string HtmlDomName { get; set; }
}
字段说明:
Text:用于显示的文本,比如:总裁
Value:显示文本对应的ID,比如:0
Index:这个是结合表单的辅助属性,View上使用
Checked:当前菜单项是否被选中,用户提交表单后我们可以通过这个属性判断用户的选择项
Items:当前菜单下的子菜单集合
Parent:当前菜单的上级菜单
HtmlDomName:这个是结合表单的辅助属性,View上使用
树形菜单的输出:
我上次面试过程中的递归逻辑还是有用的,这里我们仍然采用递归来输出菜单项。我创建了一个Partial View
@model MvcTreeView.Controllers.MyTreeViewItem
<ul>
<li>
@if (null == Model.Parent)
{
Model.HtmlDomName = "TreeView1_checkedNodes[" + Model.Index + "]";
}
else
{
Model.HtmlDomName = Model.Parent.HtmlDomName + ".Items[" + Model.Index + "]";
}
<input name='@(Model.HtmlDomName + ".Checked")' type="checkbox" onchange='setValue(this)' value='False' />
<input name='@(Model.HtmlDomName + ".Text")' type="hidden" value="@Model.Text" />
<input name='@(Model.HtmlDomName + ".Index")' type="hidden" value="@Model.Index" />
<span>@Model.Text</span>
<input name='@(Model.HtmlDomName + ".Value")' type="hidden"
value="@Model.Value" />
@if (null != Model.Items)
{
for (var i = 0; i < Model.Items.Count; i++)
{
@Html.Partial("NodeItem", Model.Items[i])
}
}
</li>
</ul>
View向Controller的数据传递:
无论是ajax还是直接post表单,最终的目的都是接收View中的数据,要想传递比较复杂的数据类型,我们需要对ASP.NET MVC Model Binding 有一定了解,之前也讲解过
MyTreeViewItem的结果,有普通的数据类型,比如 string,bool,也是对象类型,比如MyTreeViewItem类型的Parent,也是基于集合的属性IList<MyTreeViewItem>,要想让表单中的数
据直接传递给Controller,我们需要对表单元素的name进行特殊处理才行。如果大家对这部分不太理解,这篇文章可以参考:Understanding-ASP-NET-MVC-Model-Binding
比如我是这样定义Model的:
public class AboutModel
{
public MyTreeViewItem NodeItem { get; set; }
}
View:这是主要是加载菜单,至于如何使用jquery-tree,这里就不多说了,大家下可以自己下载demo。
@using (Html.BeginForm())
{
<div id="accordion">
<h3>
<a href="#">All components in default behaviour</a></h3>
<div id="example-0">
<div>
@if (null != Model.NodeItem)
{
@Html.Partial("NodeItem", Model.NodeItem)
}
</div>
</div>
</div>
<input type="submit" id="example-0-button" />
}
Controller:这是最重要的就是接收参数,它是一个List类型,无论菜单项有多少层,都会按树形数据结构层次组织好,方便我们查询。
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult About(List<MyTreeViewItem> TreeView1_checkedNodes)
{
return View(this.GetAboutModel());
}