CMS:文章管理之视图(3)

要想让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;    

    16px;

}

.x-boundlist-item .x-tree-icon-parent {

   background-image:url("../../../extjs/resources/themes/images/default/tree/folder-open.gif");

    16px;

}

.x-boundlist-item .x-tree-icon-leaf

{

   background-image: url("../../../extjs/resources/themes/images/default/tree/leaf.gif");

    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无论任何状态下,在验证时,都会搜索一遍在列表中是否存在与显示文本相同的记录,找到后就将它设置为选择值了,也就改变了原有的值。因而,不建议使用该配置项。

原文地址:https://www.cnblogs.com/hainange/p/6334226.html