operamasks-ui2.0 +MVC4.0+EF5.0实战之四 部门管理功能及网格控件(datagrid)

  前几篇侧重点还是在布局,下面,主角出场,网格控件的地位和意义已无需再说,内容也比较多,预计得分几篇才能说完,本文是一些基础的东西,但不乏需要注意的地方。

  对于MIS系统来说,公司的组织架构是一个基础的功能(网站系统则没有所谓的部门及成员,而侧重于以个体为单位的会员),也即通常所说的部门。与前面说的菜单类似,通常也是采取自关联形成树形结构。为了方便维护,设计上采取左侧树,右侧网格的方式,先上效果图,以便有个直观的印象。

  先说一下后台基本工作。

  采用Code First模式,首先创建部门实体。  

View Code
View Code 

using Model.Framework;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.ComponentModel.DataAnnotations.Schema;
using System.ComponentModel.DataAnnotations;
using System.Web.Script.Serialization;
using System.Web.Mvc;

namespace Model.Sys
{
    public  class Department : BaseEntity
    {

        [DisplayName("内码")]
        public string ID { get; set; }

        [DisplayName("部门名称")]
        [StringLength(20)]
        [Required()]
        [Remote("CheckExistForName", "Department", AdditionalFields = "Name,ParentID,ID", ErrorMessage = "上级部门下已存在该名称的部门,请确认")]
        public string Name { get; set; }

        [DisplayName("电话")]       
        public string Tel { get; set; }

        [DisplayName("传真")]
        public string Fax { get; set; }

        [DisplayName("地址")]
        public string Address { get; set; }

        [DisplayName("描述")]
        public string Describe { get; set; }

        [DisplayName("创建时间")]        
        public Nullable<System.DateTime> CreateDate { get; set; }
        
        [DisplayName("使用标志")]
        [Required()]
        public string UseFlag { get; set; }

        [DisplayName("备注")]
        public string Remark { get; set; }

        [DisplayName("排序号")]
        public string SortNo { get; set; }

       
        public string ParentID { get; set; }
        [DisplayName("上级部门")]   
        [ForeignKey("ParentID")]   
        public  Department ParentDept { get; set; }


        public string CreateUserID { get; set; }
        [DisplayName("创建人")]
        [ForeignKey("CreateUserID")]        
        public  User CreateUser { get; set; }


        public   ICollection<Department> SonDepts { get; set; }      
        public   ICollection<User> Users { get; set; }

  继承的BaseEntity是为了方便以后为所有实体加统一的方法预留的,目前为空,你可以无视。另外,用了一些数据声明和验证的东西,暂不做详细说明。这里有一点必须注意,去除virtual关键字,否则在执行Json序列化时,就会报检测到循环引用的错误。

  然后在数据库里插入几条测试数据(以下是使用EntityFramework的迁移功能,在Configuration类的Seed方法里加入测试数据,关于迁移功能请参见我之前的一篇译稿前半部分 Asp.Net MVC4.0 官方教程 入门指南之八--为Movie模型和库表添加字段),当然你也可以在数据库里手工添加。          

View Code
            context.Department.AddOrUpdate(
                p => p.ID,
                //new Department { ID = "1", Name = "部门组织", ParentID = null, UseFlag = "4" },
                new Department { ID = "2", Name = "软件公司", ParentID = null, UseFlag = "4" },
                new Department { ID = "3", Name = "研发部", ParentID = "2", SortNo = "01", UseFlag = "4" },
                new Department { ID = "4", Name = "产品部", ParentID = "2", SortNo = "02", UseFlag = "4" },
                new Department { ID = "5", Name = "办公室", ParentID = "2", SortNo = "03", UseFlag = "4" },
                new Department { ID = "6", Name = "信息中心", ParentID = "5", UseFlag = "4" },
                new Department { ID = "11", Name = "部门11", ParentID = "4", SortNo = "11", UseFlag = "4" },
                new Department { ID = "12", Name = "部门12", ParentID = "4", SortNo = "12", UseFlag = "4" },
                new Department { ID = "13", Name = "部门13", ParentID = "4", SortNo = "13", UseFlag = "4" },
                new Department { ID = "14", Name = "部门14", ParentID = "4", SortNo = "14", UseFlag = "4" },
                new Department { ID = "15", Name = "部门15", ParentID = "4", SortNo = "15", UseFlag = "4" },
                new Department { ID = "16", Name = "部门16", ParentID = "4", SortNo = "16", UseFlag = "4" },
                new Department { ID = "17", Name = "部门17", ParentID = "4", SortNo = "17", UseFlag = "4" },
                new Department { ID = "18", Name = "部门18", ParentID = "4", SortNo = "18", UseFlag = "4" },
                new Department { ID = "19", Name = "部门19", ParentID = "4", SortNo = "19", UseFlag = "4" },
                new Department { ID = "20", Name = "部门20", ParentID = "4", SortNo = "20", UseFlag = "4" },
                new Department { ID = "21", Name = "部门21", ParentID = "4", SortNo = "21", UseFlag = "4" }
                );

   以上是后台的基础性工作,关于前台调用的后台方法,跟前台一块描述,这样联系更紧密一些。

  新建一个控制器,命名为DepartmentContorller,空模板空支架,也就是完全自己控制,不用mvc脚手架自动生成。然后在其Index方法里右键,选择生成视图,命名为ListPag,打开ListPage.cshtml 

  1.在head标签内部加入对om相关css样式表的引用

    @Styles.Render("~/OperaMasksUI/css/default/om-default.css")     

  2.在</body>标签之前加入以下对js文件的引用

      @Scripts.Render("~/OperaMasksUI/js/jquery163.min.js")

          @Scripts.Render("~/OperaMasksUI/js/operamasks-ui200.min.js")

  3.部门管理功能我们想实现左侧树右侧网格的效果,因此需要用到前面已经说过的布局控件,如下所示          

  <div id="page" >
        <div id="west-panel">
            <ul id="tree"></ul>
        </div>
        <div id="center-panel">            
            <table id="datagrid"  ></table>
        </div>
    </div>

  对应的初始化js为

 function LoadLayout()
        {
            $('#page').omBorderLayout({
                panels: [
                    {
                        id: "west-panel",
                        title: "部门组织",
                        region: "west",
                        resizable: true,
                        collapsible: true,
                        width: 200
                    },
                {
                    id: "center-panel",
                    region: "center",
                    header: false
                }
                ],
                hideCollapsBtn: true,
                fit: true,
                spacing: 7
            });
        }

  前面已经详细学习过布局控件的使用,在此就不再啰唆,仅将相关代码贴出来,以上效果就是仅使用左右布局,且左侧区域可折叠。

  然后是左侧树,与前面菜单类似,同样只贴出代码。

  树初始化js:

 function LoadTree()
        {
            $("#tree").omTree({
                simpleDataModel: true,
                dataSource: '@Url.Action("Tree")',
                onClick: TreeNodeClick

            });
        }

  后台获取数据Tree方法为:

        //左侧部门树        
        public ActionResult Tree()
        {
            IQueryable<Department> all = DepartmentService.Query();
            var nodes = new List<TreeNode>();
            foreach (var item in all.ToList())
            {
                TreeNode node = new TreeNode();
                node.id = item.ID;
                node.pid = item.ParentID;
                node.text = item.Name;               
                node.expanded = "true";
                nodes.Add(node);
            }
            return Content(nodes.ToJsonString());   
        }

  下面,网格控件登场。

  初始化js:

     var rootID = "2";
        var defaultSort = { sortBy: 'Name', sortDir: 'asc' };
        function LoadDataGrid()
        {
   
            $('#datagrid').omGrid({
                method:'POST',
                title: '部门列表',
                extraData: $.extend({ id: rootID }, defaultSort),
                singleSelect: false,               
                dataSource: '@Url.Action("DataGrid")',
                colModel:
                [
                    { header: '@Html.DisplayNameFor(model => model.Name)', name: 'Name', width: 200, align: 'center', sort: 'serverSide' },
                    { header: '@Html.DisplayNameFor(model => model.Tel)', name: 'Tel', width: 100, align: 'center'},                   
                    { header: '@Html.DisplayNameFor(model => model.Describe)', name: 'Describe', width: 'autoExpand', align: 'center' },
                    { header: '@Html.DisplayNameFor(model => model.SortNo)', name: 'SortNo', width: 40, align: 'center', sort: 'serverSide' },
                    { header: '操作', name: 'ID', width: 100, align: 'center', renderer:DatagridOpColumn },

                ]               
            });
           
        }

  前面有两个变量,一个是部门根目录id,另外一个是默认排序,这两部分,都通过初始化参数extraData属性传到后台,至于colModel则指明每一列,注意列显示名称没有写死,而是通过@Html.DisplayNameFor(model => model.Name)方式从模型里取的,这样一旦模型的DisplayName改了,所有的前台页面都会自动更新,要使用这种方式,需要在首行加上@model Model.Sys.Department。

  dataSource指明取数据的后台方法,如下:  

        //右侧部门列表
        [HttpPost]
        public ContentResult DataGrid(FormCollection form)
        {
            string id = form["id"];
            //若为根目录,查询所有部门,否则附加查询限制条件
            /***处理略,之后篇章里详述统一查询处理**/
//排序 Sort sort = new Sort(form["sortBy"], form["sortDir"]); //查询 IQueryable<Department> queryResult = DepartmentService.Query(model, sort); //分页 PageView pageView = new PageView(Int32.Parse(form["start"]), form["limit"]); //取得数据 var data = DataGrid<Department>.GetPageData(queryResult, pageView); //返回数据 return Content(data.ToJsonString()); }

  辅助的各个类如下:
  排序类Sort:

View Code
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Common.Query
{
    public class Sort
    {
        public string SortType { get; set; }
        public string Field { get; set; }

        public Sort(string field, string sortType = "asc")
        {
            Field = field;
            SortType = sortType;
        }
        public string Expression
        {
            get
            {
                return string.Format(" {0} {1} ", Field, SortType);
            }
        }
    }
}

  分页类PageView:

View Code
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Common.Query
{
    public class PageView
    {
        /// <summary>
        /// 页面索引
        /// </summary>
        public int PageIndex { get; set; }

        /// <summary>
        /// 页面记录数
        /// </summary>
        public int PageSize { get; set; }

        /// <summary>
        /// 记录起始数
        /// </summary>  
        public int RecordStart { get; set; }
        
        
        public PageView()
        {

        }
        public PageView(string pageIndex, string pageSize)
        {

            if (pageIndex != null )
            {
                try
                {
                    PageIndex = Int32.Parse(pageIndex);
                }
                catch 
                {
                    PageIndex =1;
                }
               
            }
            if (pageSize != null)
            {
                try
                {
                    PageSize = Int32.Parse(pageSize);
                }
                catch
                {
                    PageSize = 10;
                }                
            }
           
        }

        public PageView(int recordStart, string pageSize)
        {
            if (pageSize != null)
            {
                try
                {
                    PageSize = Int32.Parse(pageSize);
                }
                catch
                {
                    PageSize = 10;
                }
            }

            PageIndex = (int)Math.Ceiling((double)recordStart / (double)PageSize);

        }
      
    }
}

  其实,我原来的分页类里只有当前页码PageIndex和页面记录数PageSize,om传给后台的limit是页面记录数,而start居然是起始记录数(不得不说,om设计人员的思维模式……),因此不得不修改这个类来适应,加入重载初始化函数以及通过RecordStart和PageSize来换算出PageIndex。
  网格类DataGrid是为了前后台交换数据用的:

View Code
using Common.Query;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;

namespace Model.Json
{
    public class DataGridRow
    {
        public string id { get; set; }
        public List<string> cell { get; set; }
    }
    public class DataGrid<T>:Object
    {
         public int total { get; set; }
         public List<T> rows { get; set; }
         public static DataGrid<T> ConvertFromList(List<T> list) 
        {
            DataGrid<T> data = new DataGrid<T>();

            if (list != null)
            {
                data.total = list.Count;
                data.rows = list;               
            }
            else
            {
                data.total = 0;
            }
            
            return data;
        }

         public static DataGrid<T> GetAllData(IQueryable<T> query)
         {
             DataGrid<T> data = new DataGrid<T>();

             if (query != null)
             {
                 data.total = query.Count();
                 data.rows = query.ToList();
             }
             else
             {
                 data.total = 0;
             }

             return data;
         }
         public static DataGrid<T> GetPageData(IQueryable<T> query,PageView pageView)
         {
             DataGrid<T> data = new DataGrid<T>();
             if (query != null)
             {
                 data.total = query.Count();
                 if (pageView != null)
                 {

                     if (pageView.PageSize > 0 && pageView.PageIndex >= 0)
                     {

                         query = query.Skip(pageView.PageIndex * pageView.PageSize).Take(pageView.PageSize);
                     }

                 }      
                 data.rows = query.ToList();
             }
             else
             {
                 data.total = 0;
             }

             return data;
         }
      
    }
 
}


  前台还有一个部门树点击的事件处理,就是就部门的id传给后侧的网格:

      function TreeNodeClick(nodeData,event)
        {
            $('#datagrid').omGrid({ extraData: $.extend({ id: nodeData.id }, defaultSort) });
        }

   事实上,你看到的最终的结果,中间还是经历了一些曲折……

  首先一个问题跟本节内容关系不大,但是本节中暴露出来了,就是实战三中,点击功能菜单后,在右侧业务区域中动态添加tab页,嵌入iframe,相关的js如下  

  function TreeNodeClick(node, event)
 {
      $("#tabs").omTabs('add', {
      title: node.text,
      content: '<iframe scrolling="yes" frameborder="0"  src=' + node.url + ' style="width:100%;height:100%;"></iframe>',
      closable: true,
      tabId:node.id
      });
}

  咋看上去是没问题,添加了tab页,当时运行也没发现问题,加了内容后就出问题了,高度!高度不能自动适配,只显示大概几百像素,没有填充整个tab页,即使设置了style="width:100%;height:100%;也没用,而在easyui中就没这问题,直接可以实现完美的完全填充效果,查找资料,反复试验,最终采取下面这种方式勉强达到效果:

content: '<iframe id="frame" onload="$(this).height($(this).contents().find(' + "tabs" + ').height()-55)" scrolling="yes"  frameborder="0"  src=' + node.url + ' style="width:100%;height:100%"></iframe>'

  即使用js在iframe加载完成后,动态获取tab标签页的高度然后减去55px,设置为iframe的高度,至于为什么设置为55,一是tab标签头部自身25px,另外30是一些margin、border占用的,目测和试验55效果最好,未在多浏览器多显示器下测试,可能还有问题。若你有更好的解决方式,欢迎留言说明,先行谢过。

  第二个问题是关于服务器端排序问题,datagrid的colModel属性,可以设置各列的排序方式,客户端、服务器端或者自定义js函数,如果是采用服务器端排序,即设置 sort: 'serverSide'。另外,我后台分页,对IQureyable对象使用Skip方法,该方法要求必须有orderBy子句,从业务角度考虑,通常也需要在datagrid首次加载的时候设置一个默认排序字段和排序方式(asc或desc)。查看了官方示例和说明,datagrid自身属性没有排序相关内容,而是在其基础了外挂了一个排序插件,点击列标题的时候会向后台发送sortBy和sortDir。初始化的时候,则没有提供对外设置关于排序的方法和属性,因此,只能放到extraData属性中。结果问题就来了,调试时候发现,服务器端排序不起作用,发现前台传给后台的排序参数,始终是初始化中设置的Name和asc,点击列头根本不起作用。无奈之下只能查看om源码,幸好源码是开放的且注释比较多,找到了omGrid的_populate方法,大概在11040行,发现了问题所在,合并参数的时候,用初始化的参数,把排序两字段覆盖了,源代码如下: 

 var param =$.extend(true,{},this._extraData,op.extraData,{
                start : limit * (nowPage - 1),
                limit: limit,
                 _time_stamp_ : new Date().getTime()
            }); 

  问题是找到了,但是om没有提供任何关于排序的方法或属性,用于控制点击列头来排序这个过程,全部内置了。无奈之下,只有修改源代码,把排序两个字段传了过去  

 var param =$.extend(true,{},this._extraData,op.extraData,{
                start : limit * (nowPage - 1),
                limit: limit,
                sortBy: this._extraData.sortBy,
                sortDir: this._extraData.sortDir,
                _time_stamp_ : new Date().getTime()
            });            

  小改动,改完后服务器端排序总算正常了,理论上对其他地方也没影响,应该不会因为改动带来新的问题。要设置初始加载后的默认排序字段,自身属性不提供,只能通过extraData这里加上,若自定义名字,不跟sortBy和sortDir重名,则后台方法就要分别处理,还要区分两种情况,若重名,则又会被初始参数覆盖,左右为难,这应该算一个BUG吧?

  本篇到此为止,这就是网格控件,实现了取数、展现、排序和分页,下节介绍增、删、改、查。

  最后,祝园子里各位新年快乐!

posted on 2013-02-09 11:25  大浪淘沙  阅读(4497)  评论(9编辑  收藏  举报

导航