要想让Combobox列表显示像树,只要按Ext JS树的形式组织显示文本就行了。因而先用Firebug分析一下Ext JS树的格式就可以了。在浏览器打开Ext JS示例中的Check Tree示例,依次展开Grocery List和Ebergy Foods节点,会看到图53的效果。
图53 Check Tree示例的效果
因为Combobox中返回的是一个完整的树,其节点全部是展开的,因而可以知道,展开后的节点的图标是模拟效果所需的。将Firebug切换到HTML面板,然后单击选择页面元素的按钮,选择展开后的那个黑色小三角图标,在Firebug中会看到如图54中的效果。
图54 Firebug中黑色小三角图标的HTML代码
从图54可以看到,Grocery List节点的构成是2个图标加上文字。现在要做的就是把两个图标的HTML代码复制出来。接着选择Coffee节点,会看到它是由3个空白图标、1个叶子图标、1个复选框和文字构成的,这里面的空白图标和叶子图标也是需要的,复制出来。
从书中可以知道,这些图标src中的图片都是空白图片,起占位符的作用,实际显示的是它们的背景图片。(这也说明,这个在IE旧版本中是没有效果的)
在HTML面板中,选中这些图片,然后在右边的样式中就可看到它们的样式了,例如如图55那样,选择叶子图标,会看它主要由2个样式决定显示方式。第1个就是顶部的img的样式,这个很重要,如果没有该样式,图标和文字就不能对齐,会上下错位。第2个样式“x-tree-icon-leaf”了,它用来显示背景图片。
图55 叶子图标的样式
笔者测试过,直接保留样式和整个结构搬过去并不行,还是要重新定义一下样式。因为Ext.view.BoundList的每个列表行的起始样式是x-boundlist-item,因而从这个样式开始重新定义一下样式就行了。切换到app.css,添加以下样式:
.x-boundlist-item span
{
line-height:19px;
}
.x-boundlist-item img
{
display:inline;
vertical-align: top;
}
.x-boundlist-item .x-tree-elbow-plus
{
background-image:url("../../../extjs/resources/themes/images/default/tree/arrows.gif");
background-position: -16px 0;
width:16px;
}
.x-boundlist-item .x-tree-icon-parent {
background-image:url("../../../extjs/resources/themes/images/default/tree/folder-open.gif");
width:16px;
}
.x-boundlist-item .x-tree-icon-leaf
{
background-image: url("../../../extjs/resources/themes/images/default/tree/leaf.gif");
width:16px;
}
第1个样式是为文字对齐用的,如果不套一个span,直接设置x-boundlist-item的line-height,会影响其它的Combobox,因而这里套一个span来对齐文字。第2个样式是用来定义图标显示方式的;第3个用来显示展开的小三角图标;第4个用来显示打开的文件夹图标;第6个用来显示叶子图标,也就是在图55看到的样式。
现在切换到Category控制器,先新增几个私有变量来指定图片,代码如下:
private string blank = "<imgclass='x-tree-elbow'src='data:image/gif;base64,R0lGODlhAQABAID/AMDAwAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=='>";
private string folder = "<imgclass='x-tree-icon x-tree-icon-parent ' src='data:image/gif;base64,R0lGODlhAQABAID/AMDAwAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=='>";
private string leaf = "<imgclass='x-tree-icon x-tree-icon-leaf'src='data:image/gif;base64,R0lGODlhAQABAID/AMDAwAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=='>";
private string plus = "<img class='x-tree-elbow-plusx-tree-expander'src='data:image/gif;base64,R0lGODlhAQABAID/AMDAwAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=='>";
重载一个writeNode方法来生成新的数据结构,代码如下:
private JObject writeNode(int id, string text, boolisLeaf, int level)
{
stringoffset= "";
if(level>0)
{
offset = string.Join(string.Empty, Enumerable.Repeat(blank,level).ToArray());
}
stringlistText = "<span>" + offset + (isLeaf ? blank + leaf : plus+folder)+text +"</span>";
JObjectjo = new JObject
{
newJProperty("id",id),
newJProperty("text",text),
newJProperty("listText",listText)
};
returnjo;
}
因为两个writeNode参数相同,且参数类型也一样,因而第2个方法把3和4参数对调了一下。参数level的作用就是用来生成空白图标的。
Enumerable对象的Repeat方法可生成一个重复值的队列,非常方便。把队列转换为数组,就可通过字符串的Join方法将数组转换为字符串了。
用来显示的listText,先套一个span,然后加上偏移量,也就是空白图标。如果是叶子,就加多一个空白图标,再加上叶子图标。如果不是叶子,就加一个展开的小三角图标和打开的文件夹图标。最后加上文本。
CategoryCombo提交的是All方法,现在完成All方法,代码如下:
public JObject All()
{
boolsuccess = false;
stringmsg = "";
JArray ja= new JArray();
int total= 0;
try
{
ja.Add(writeNode(-1,"文章类别",false,0));
ja.Add(writeNode(10000, "未分类", true, 1));
var q = dc.T_Category.Where(m =>m.State == 0 & m.CategoryId!=10000).OrderBy(m => m.FullPath);
foreach (var c in q)
{
bool leaf = c.Childs.Count() > 0 ? false : true;
ja.Add(writeNode(c.CategoryId, c.Title, leaf,(int)c.Hierarchylevel+1));
}
success = true;
}
catch(Exception e)
{
msg =e.Message;
}
returnHelper.MyFunction.WriteJObjectResult(success, total, msg, ja);
}
还是重复得不能再重复的代码。因为有全路径,因而通过FullPath排序就可直接把树排列好了。这里还是要先把“未分类”排除在外,让它显示在前面。因为要把根目录显示出来,因而要多添加一个“文章分类”作为根。也因为根目录才是第1层,而Hierarchylevel是以没有父id的列为根的,因而要加1。
生成一下解决方案,然后展开父类选择,就可看到如图56的效果。笔者认为这还行。
图56 类似树的下拉选择效果
定义父类的Combobox时,忘记加上查询功能了,只要添加配置项queryMode,值为local就行了。因为是必选的,因而,还要加上forceSelection配置项,值为true。
现在来完成保存按钮和重置按钮的操作。先完成重置按钮的,这个简单,调用表单的reset方法就行了,代码如下:
onReset: function () {
var me =this;
me.form.getForm().reset();
},
最后完成保存按钮,主要就是做表单提交,这个问题不大,直接调用表单的submit提交就行了,提交地址会在窗口显示前进行设置。现在要考虑的是提交成功后,怎么处理树的更新?新增的话比较简单,先检查它的父节点是否已经显示,如果还没有,就直接通过fullpath找到最顶层的根节点,然后展开它的全部子节点。如果已经存在,且已经展开,则调用appendChild方法追加就行了;如果存在,但没展开,则展开该节点就行了。
编辑操作比较复杂,很难处理,除非模仿树的拖放操作做相应的处理,这里就不做研究了,直接刷新Store。或者一种替代方式是编辑时禁止修改父类,只允许通过拖放方式改变父类,这里也不再做研究了,有兴趣自己做一下。
现在,先把提交过程写出来,再考虑刷新问题,代码如下:
onSave: function () {
var me = this,
f= me.form.getForm();
if(f.isValid()) {
f.submit({
//waitMsg:"正在保存,请等待……",
//waitTitle:"正在保存",
success: function (form, action) {
var me = this;
},
failure: SimpleCMS.FormSubmitFailure,
scope: me
});
}
}
因为Ext JS表单提交的错误返回方式是雷同的,因而可以统一对这些错误进行处理,因而在这里会看到failure配置项的值为 “SimpleCMS.FormSubmitFailure”。这个当时在登录窗口时没做,现在可以完成它了。在Index.html文件中,添加以下代码:
SimpleCMS.FormSubmitFailure = function(form,action) {
if(action.failureType === "connect") {
Ext.Msg.alert('错误',
'状态:' + action.response.status + ': ' +
action.response.statusText);
return;
}
if(action.result) {
if(action.result.Msg)
Ext.Msg.alert('错误',action.result.Msg);
}
}
好了,现在考虑提交成功后的处理,首先是要区分现在的状态是编辑状态还是添加状态,这个可通过CategoryId的值来判断,值大于10000的肯定是编辑状态,除非新建模型的时候,把该值设置为了大于10000的值,这个,估计只有傻瓜才会这样干。
新增的时候,根据服务器端返回的数据值,在父节点下添加一个新节点就行了。这里还有个技术问题,就是要找到TreeStore才能找到父节点,这是个难题。还是用老办法,在控制器的init方法内,为窗口添加一个store属性,并指向TreeStroe好了。因为还需要用到视图展开节点,因而,还要添加属性view,指向TreeView,具体修改代码如下:
var me = this,
win =SimpleCMS.view.Content.CategoryEdit,
panel =me.getContentPanel();
me.view = Ext.widget("contentview");
panel.add(me.view);
win.view = me.view.down("treeview");
win.store = me.getCategoriesTreeStore();
现在来完成success中的代码,代码如下:
success: function (form, action) {
var me =this,
values = form.getValues(),
data= action.result.data[0];
if(values.CategoryId > 10000) {
me.store.load();
} else {
if(data.parentId) {
var parentNode = me.store.getNodeById(data.parentId);
if (parentNode) {
if (parentNode.isExpanded()) {
parentNode.appendChild(data);
} else {
parentNode.expand();
}
}else {
parentNode = me.store.getNodeById(data.fullpath.substr(1, 5));
me.view.expand(parentNode, true);
}
}else{
me.store.getRootNode().appendChild(data);
}
}
me.form.down("combobox").store.load();
me.close();
},
代码先调用getValues方法返回表单中所有字段的值,然后判断CategoryId的值是否大于10000,如果是,说明是编辑状态,直接刷新树。否则,是新增状态。然后根据返回的数据,判断父id是否存在,如果不存在,说明是顶层节点,直接将新节点追加到根节点。如果父id存在,则调用getNodeById方法获取节点,如果返回null,说明节点还没展开,则通过fullpath取得顶层节点,然后调用视图的expand方法展开顶层节点的所有节点。如果父节点存在,且已展开,就追加方式追加节点,否则展开节点。
现在来完成onCategoryAdd方法,在这里要做的就是设置表单的提交地址,设置窗口的标题,利用loadRecord方法为表单加载一个新记录,最后显示窗口,具体代码如下:
onCategoryAdd: function () {
var me = this,
win =SimpleCMS.view.Content.CategoryEdit,
model= me.getCategoryModel();
win.form.getForm().url = "/Category/Add";
win.setTitle("新增文章类别");
win.form.loadRecord(new model);
win.show();
},
因为是表单提交,因而可以利用MVC的模型认证进行服务器端验证,这形式不错,省了很多功夫。首先要做的是在Models目录创建一个模型。在Models目录添加一个名称为CategoryModel的类,在类内添加以下属性定义就行了:
public class CategoryModel
{
publicint CategoryId { get; set; }
[Required]
[Display(Name = "父类")]
publicint ParentId { get; set; }
[Display(Name = "题图")]
[StringLength(255)]
publicstring Image { get; set; }
[Display(Name = "排序序数")]
publicint SortOrder { get; set; }
[Required]
[Display(Name = "标题")]
[StringLength(255)]
publicstring Title { get; set; }
[Display(Name= "内容")]
[StringLength(8000)]
publicstring Content { get; set; }
}
以上不懂的,可以网上搜索一下,资料很多,学MVC必须会的。
接着切换到Category控制器,添加Add方法,代码结构有点类似登录时的代码,具体代码如下:
[HttpPost]
[AjaxAuthorize(Roles = "普通用户,系统管理员")]
public JObject Add(CategoryModel model)
{
boolsuccess = false;
JObjecterrors = new JObject();
if(ModelState.IsValid)
{
try
{
T_Category rec = new T_Category
{
Content = model.Content,
Created = DateTime.Now,
Image = model.Image,
SortOrder = model.SortOrder,
Title = model.Title
};
if (dc.T_Category.Select(m => m.CategoryId).Contains(model.ParentId))
{
rec.ParentId=model.ParentId;
}
dc.T_Category.AddObject(rec);
dc.SaveChanges();
return MyFunction.WriteJObjectResult(true, 0, "", new JArray(
new JObject (
new JProperty("id",rec.CategoryId),
new JProperty("text", rec.Title),
new JProperty("parentId", rec.ParentId),
new JProperty("fullpath", rec.ParentId==null ? "" :rec.Parent.FullPath)
)
));
}
catch(Exception e)
{
return MyFunction.WriteJObjectResult(false, 0, e.Message, null);
}
}
else
{
MyFunction.ModelStateToJObject(ModelState, errors);
}
returnMyFunction.WriteJObjectResult(success, errors);
}
特性HTTP说明Add方法只接收Post提交的数据,提交后的数据会字段转换为CategoryModel模型的实例。模型验证如果有错误,处理方法和登录时一样。如果没有验证错误,则新增一个T_Category实例,父类不能直接加进新建实例中,要判断在数据库中是否有该记录,如果没有,说明是顶层节点,要保持ParentId的值为null。保存数据,就返回一个包含新增数据的对象。因为新增后,没那么快更新记录的fullpath(通过存储过程更新的),因而,这里fullpath需要用到它的父类的fullpath。如果ParentId为null,说明是顶层节点,直接返回空值就行。
生成一下解决方案,就可以测试了,在这里就不进行测试了。
说明:经测试,如果Combobox设置了配置项forceSelection为true,则存在相同标题的类别时,提交后的类别均为最靠前的那个类别,与实际选择不符。造成这个问题的原因是在Combobox无论任何状态下,在验证时,都会搜索一遍在列表中是否存在与显示文本相同的记录,找到后就将它设置为选择值了,也就改变了原有的值。因而,不建议使用该配置项。