继上一篇"MVC无限级分类01,分层架构,引入缓存,完成领域模型与视图模型的映射",本篇开始MVC无限级分类的增删改查部分,源码在github。
显示和查询
使用datagrid显示数据,向控制器发出请求的时候,datagrid默认带上了当前页page和页容量rows这2个参数。如果项目中的其它界面也用datagrid展示数据,我们可以把page和rows封装成一个基类:
class="alt">namespace Car.Test.Portal.Models.QueryParams{public class QueryParamsBase
{public int PageSize { get; set; }
public int PageIndex { get; set; }
}
}
而当输入搜索条件时,datagrid的queryParams属性能接收json格式的搜索条件,这时候,datagrid向控制器发出请求,不仅仅有page和rows这2个参数,还包括了搜索条件中的键值对。可以把与每个模型对应的搜索条件封装成一个继承QueryParamsBase的类:
using System;namespace Car.Test.Portal.Models.QueryParams{public class CarCategoryQueryParams : QueryParamsBase
{public string Name { get; set; }
public DateTime? JoinStartTime { get; set; } public DateTime? JoinEndTime { get; set; }}
}
与搜索和显示相关的html部分为:
<!--查询开始-->
<div id="query" class="query" >
<span>类名:</span>
<input id="txtName" style="line-height:22px;border:1px solid #ccc" maxlength="10">
<span>提交时间从:</span>
<input type="text" name="startSubTime" id="startSubTime" style="line-height:22px;border:1px solid #ccc" />
<span>至:</span>
<input type="text" name="endSubTime" id="endSubTime" style="line-height:22px;border:1px solid #ccc" />
<a href="#" class="easyui-linkbutton" plain="false" id="btnSearch">搜索</a>
</div>
<!--查询结束-->
<!--表格开始-->
<div class="ctable" id="ctable">
<table id="tt"></table></div>
<!--表格结束-->
js部分需要完成的工作包括:
●限制搜索条件中类名的长度
●搜索条件中的时间用jqueryui的datapicker来实现,当在起始时间选择了一个日期,结束日期的选择范围相应变成该天以后
●起始日期和结束日期只能通过datapicker设置,所以要把2个文本框设置为只读
●点击搜索按钮,把搜索添加封装成json对象传递个datagrid的queryParams属性
<script type="text/javascript">$(function() {
//限制类名的长度 limitInputLength($('#txtActionName')); //限制时间搜索为只读$('#startSubTime').attr('readonly', true);
$('#endSubTime').attr('readonly', true);
//起始和结束日期fromDateToDate();
//显示列表initData();
//搜索$('.query').on("click", "#btnSearch", function () {
initData(initQuery());
});
});
//限制文本框长度 function limitInputLength(_input) { var _max = _input.attr('maxlength'); _input.bind('keyup change', function () {if ($(this).val().length > _max) {
($(this).val($(this).val().substring(0, _max)));
}
});
}
//起始和结束日期 function fromDateToDate() { $('#startSubTime').datepicker({ dateFormat: "yy-mm-dd", changeMonth: true, changeYear: true,numberOfMonths: 2,
onClose: function (selectedDate) {$("#endSubTime").datepicker("option", "minDate", selectedDate);
}
});
$('#endSubTime').datepicker({ dateFormat: "yy-mm-dd", changeMonth: true, changeYear: true,numberOfMonths: 2,
onClose: function (selectedDate) {$("#startSubTime").datepicker("option", "maxDate", selectedDate);
}
});
}
//获取查询表单的值组成json function initQuery() { var queryParams = { txtName: $('#txtName').val(), startSubTime: $('#startSubTime').val()};
return queryParams;}
//显示分类 function initData(params) { $('#tt').datagrid({ url: '@Url.Action("GetCarCategoryJson","Home")', title: '分类列表', //width: 800,height: 390,
fitColumns: true, nowrap: true, showFooter: true, idField: 'ID', loadMsg: '正在加载信息...', pagination: true, singleSelect: false, queryParams: params,pageSize: 10,
pageNumber: 1,
pageList: [10, 20, 30],
//toolbar: '#query',columns: [
[
{ field: 'ck', checkbox: true, align: 'center', width: 30 }, { field: 'ID', title: '编号' }, { field: 'Name', title: '类名' }, { field: 'PreLetter', title: '前缀字母' }, { field: 'ParentID', title: '父级ID' }, { field: 'Level', title: '层级' }, { field: 'IsLeaf', title: '是否叶节点' }, { field: 'DelFlag', title: '删除状态', formatter: function (value, row, index) {if (value == "0") {
return '使用中';
} else if (value == "1") {
return '逻辑删除';
} else {return '物理删除';
}
}
},
{ field: 'SubTime', title: '修改时间', formatter: function (value, row, index) {return eval("new " + value.substr(1, value.length - 2)).toLocaleDateString();
}
}
]
],
toolbar: [
{ id: 'btnAdd', text: '添加', iconCls: 'icon-add', handler: function () {showAdd();
}
}, '-', { id: 'btnUpdate', text: '修改', iconCls: 'icon-edit', handler: function () {var rows = $('#tt').datagrid("getSelections");
if (rows.length != 1) {$.messager.alert("提示", "只能选择一个分类进行编辑");
return;}
//修改方法showEdit(rows[0].ID);
}
}, '-', { id: 'btnDelete', text: '删除', iconCls: 'icon-remove', handler: function () {var rows = $('#tt').datagrid("getSelections");
if (rows.length < 1) {$.messager.alert("提示", "请选择要删除的分类");
return;}
$.messager.confirm("提示信息", "确定要删除吗?", function (r) {
if (r) { var strIds = ""; for (var i = 0; i < rows.length; i++) {strIds += rows[i].ID + '_'; //1_2_3
}
$.post("@Url.Action("DeleteCarCategories", "Home")", { ids: strIds }, function (data) {
if (data.msg == "no") {
$.messager.alert("提示", "删除失败!");
$('#tt').datagrid("clearSelections");
return;} else if (data.msg) {
$.messager.alert("提示", "删除成功");
initData(initQuery());
$('#tt').datagrid("clearSelections");
}
});
}
});
}
}
],
OnBeforeLoad: function (param) {return true;
}
});
}
</script>
控制器部分,把接收到的有关分页或查询的参数封装成一个类作为参数交给服务层的一个方法,这里为了方便,把本该在服务层的方法直接写在了控制器内。最后把得到的集合封装成json对象,并满足datagrid所期望的格式传递到前台视图。
private string txtName = string.Empty;
private DateTime? startSubTime = null;
private DateTime? endSubTime = null;
public ICarCategoryRepository CarCategoryRepository { get; set; } public HomeController(ICarCategoryRepository carCategoryRepository) { this.CarCategoryRepository = carCategoryRepository;}
public HomeController():this(new CarCategoryRepository()){}
#region 显示分类 public ActionResult Index() { return View();}
//显示所有分类并考虑查询和分页 public ActionResult GetCarCategoryJson() { //获取datagrid传来的2个参数int pageIndex = int.Parse(Request["page"]);
int pageSize = int.Parse(Request["rows"]);
//获取搜索参数if (!string.IsNullOrEmpty(Request["txtName"]))
{ txtName = Request["txtName"];}
if (!string.IsNullOrEmpty(Request["startSubTime"]))
{ startSubTime = DateTime.Parse(Request["startSubTime"]);}
if (!string.IsNullOrEmpty(Request["endSubTime"]))
{ endSubTime = DateTime.Parse(Request["endSubTime"]);}
//初始化查询实例 var temp = new CarCategoryQueryParams {PageIndex = pageIndex,
PageSize = pageSize,
Name = txtName,
JoinEndTime = endSubTime,
JoinStartTime = startSubTime
};
//获取所有满足条件的数据,并获得总记录数 int totalNum = 0; var allCarCategory = LoadPageCarCategoryData(temp, out totalNum); //投影出需要传递到前台的数据 var result = from c in allCarCategory select new {c.ID,
c.Name,
c.IsLeaf,
c.Level,
c.ParentID,
c.PreLetter,
c.Status,
c.DelFlag,
c.SubTime
};
//构建datagrid所需要的json格式 var jsonResult = new { total = totalNum, rows = result };return Json(jsonResult, JsonRequestBehavior.AllowGet);
}
//根据查询条件获得分页数据private IEnumerable<CarCategory> LoadPageCarCategoryData(CarCategoryQueryParams param, out int total)
{var allCarCategories =
CarCategoryRepository.LoadEntities(
c => c.Status == (short) StatusEnum.Enable && c.DelFlag == (short) DelFlagEnum.Normal);
if (!string.IsNullOrEmpty(param.Name))
{allCarCategories = allCarCategories.Where(c => c.Name.Contains(param.Name));
}
if (!string.IsNullOrEmpty(param.JoinStartTime.ToString()) &&
!string.IsNullOrEmpty(param.JoinEndTime.ToString())) {allCarCategories =
allCarCategories.Where(c => c.SubTime >= param.JoinStartTime && c.SubTime <= param.JoinEndTime);
}
total = allCarCategories.Count();
IEnumerable<CarCategory> result = allCarCategories
.OrderByDescending(c => c.ID)
.Skip(param.PageSize*(param.PageIndex - 1))
.Take(param.PageSize);
return result;}
#endregion
添加
在主视图,即datagrid所在页面视图,放置显示添加弹出窗口的div,里面有一个iframe。

在视图刚加载完的时候,先把添加div隐藏掉。
$(function() { //隐藏元素initialHide();
});
//隐藏元素 function initialHide() {$('.addContent').css("display", "none");
$('.editContent').css("display", "none");
}
当点击datagrid添加按钮,向控制器方法/Home/AddCarCategory发送一个ajax的get请求,如果请求到数据,ifram指向到新的视图,显示添加div,并以模态窗口的形式弹出。
//添加对话框 function showAdd() { var url = "/Home/AddCarCategory"; $.ajax({ cache: false,url: url,
contentType: 'application/html;charset=utf-8', type: "GET", success: function (result) { if (result) {$("#frameAdd").attr("src", url);
$("#addContent").css("display", "block");
$('#addContent').dialog({toolbar: [],
maximizable: false, draggable: true, resizable: false, closable: false, modal: true, cache: false, //height: '100%',width: 560,
height: 480,
top:50,
title: "添加类别", onOpen: function () {},
buttons: []
});
}
},
error: function (xhr, status) { alert("加载内容失败" + status);}
});
}
当iframe指向的视图执行完毕后还是要返回到datagrid所在视图界面的,可以把iframe指向的视图页看作子窗口,从datagrid主视图跳出的模态窗口看作是父窗口,让子窗口执行完毕后,调用父窗口的方法。父窗口被子窗口调用的添加成功或取消添加的方法为:
//添加成功后 function refreshAfterAdd() {initData();
$('#tt').datagrid("clearSelections");
$('.addContent').dialog("close");
}
//取消添加function cancelAdd() {
$('.addContent').dialog("close");
}
添加div中iframe指向的视图页面,在逻辑上可以分成2部分,一部分是以视图模型以及模型验证有关的,我们把它方在一个强类型部分视图中,再通过jquery异步加载它。另外一部分就是添加按钮和取消,点击它们,调用父窗口的方法。
控制器中的 AddCarCategory()用来显示添加div中iframe指向的视图页面,AddCarCategory(CarCategoryVm carCategoryVm)用来处理添加,AddModel()用来显示强类型部分视图,在AddCarCategory.cshtml视图中通过jquery异步加载。控制器部分为:
#region 添加分类 public ActionResult AddCarCategory() { //return PartialView(new CarCategoryVm()); return View();}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult AddCarCategory(CarCategoryVm carCategoryVm) { if (ModelState.IsValid) { carCategoryVm.Status = (short)StatusEnum.Enable; carCategoryVm.DelFlag = (short)DelFlagEnum.Normal;carCategoryVm.SubTime = DateTime.Now;
CarCategory carCategory = AutoMapper.Mapper.DynamicMap<CarCategoryVm, CarCategory>(carCategoryVm);
CarCategoryRepository.AddEntity(carCategory);
CarCategoryRepository.SaveChanges();
return Json(new {msg = true});
}
else { //return PartialView(carCategoryVm);return PartialView("AddModel", carCategoryVm);
}
}
//[ChildActionOnly] public ActionResult AddModel() {return PartialView(new CarCategoryVm());
}
#endregion #region 显示所有分类的树public string LoadAllCategories()
{var temp =
CarCategoryRepository.LoadEntities(
c => c.DelFlag == (short) DelFlagEnum.Normal && c.Status == (short) StatusEnum.Enable);
var result = from c in temp select new {c.ID, c.Name, c.ParentID}; //return Json(result, JsonRequestBehavior.AllowGet); return JsonSerializeHelper.SerializeToJson(result);}
#endregion AddModel.cshtml强类型部分视图,我们希望点击id="getCategory"的input的时候,显示zTree树,而点击zTree节点的时候,id="getCategory"的input显示节点名称,把真正的ID保存到id="categoryId" name="ParentID"的隐藏域中。
@model Car.Test.Portal.Models.CarCategoryVm
@using (Html.BeginForm("AddCarCategory", "Home", FormMethod.Post, new { id = "addForm" }))
{@Html.AntiForgeryToken()
<ul>
<li>
<span class="fdwith">
@Html.LabelFor(c => c.Name)
</span>
@Html.EditorFor(c => c.Name)
@Html.ValidationMessageFor(c => c.Name)
</li>
<li>
<span class="fdwith">
@Html.LabelFor(c => c.PreLetter)
</span>
@Html.EditorFor(c => c.PreLetter)
@Html.ValidationMessageFor(c => c.PreLetter)
</li>
<li>
<span class="fdwith">
@Html.LabelFor(c => c.ParentID)
</span>
<input id="getCategory" type="text" value="==请选择分类==" />
@Html.ValidationMessageFor(c => c.ParentID)
<input type="hidden" id="categoryId" name="ParentID"/>
</li>
<li>
<span class="fdwith">
@Html.LabelFor(c => c.Level)
</span>
@Html.EditorFor(c => c.Level)
@Html.ValidationMessageFor(c => c.Level)
</li>
<li>
<span class="fdwith">
@Html.LabelFor(c => c.IsLeaf)
</span>
@Html.DropDownListFor(c => c.IsLeaf,new[] {new SelectListItem(){Text = "是",Value = bool.TrueString},
new SelectListItem(){Text = "否",Value = bool.FalseString}
},"==选择是否为叶节点==",new {id="ld"})
@Html.ValidationMessageFor(c => c.IsLeaf)
</li>
</ul>
}
AddCarCategory.cshtml视图中,js和css部分需注意:
● zTree所需要的1个css文件和2个js文件一个都不能少
● zTree需要在ul的class一定是ztree,即<ul id="tree" class="ztree"...,否则会影响树的显示,本人就吃过这个苦头,擅自把ztree改成tree,为这个问题纠结了一天!
● 关于jquery验证的jquery.validate.js文件和jquery.validate.unobtrusive.js顺序千万不能颠倒。
● zTree的属性较多,setting中的属性,本人也调了很多次,现在看到的还不错。
● 要把zTree所在的div设置成绝对定位,因为希望在显示的时候通过js控制,让它跑到对应的文本框正下方。
AddCarCategory.cshtml视图主要工作包括:
● 页面加载完毕,向控制器发送异步请求,把zTree树先加载上,由于隐藏了zTree所在的div,这时候还看不到
● 页面加载完毕,项控制器发送异步请求,把强类型部分视图加载到本页。由于是动态加载的内容,所以需要$.validator.unobtrusive.parse("form")此语句,让动态部分视图内容恢复客户端验证功能。
● 点击取消按钮,调用父窗口的取消添加方法
● 点击添加按钮,调用父窗口添加成功方法
● 由于生成的是动态内容,根据"冒泡原理",需要不添加按钮up等的点击事件注册到它的父级元素,类似这样:$('#operate').on("click", "#up", function() {}
● 点击部分视图的id="getCategory"的input的时候,显示zTree树,而点击zTree节点的时候,id="getCategory"的input显示节点名称,把真正的ID保存到id="categoryId" name="ParentID"的隐藏域中。
logs_code_Collapse">展开
修改
在主视图,即datagrid所在页面视图,放置显示修改弹出窗口的div,里面有一个iframe。

在视图刚加载完的时候,先把修改div隐藏掉。
$(function() { //隐藏元素initialHide();
});
//隐藏元素 function initialHide() {$('.addContent').css("display", "none");
$('.editContent').css("display", "none");
}
当点击datagrid修改按钮,向控制器方法"/Home/EditCarCategory?发送一个ajax的get请求,如果请求到数据,ifram指向到新的视图,显示修改div,并以模态窗口的形式弹出。
//修改对话框 function showEdit(carCatId) { var url = "/Home/EditCarCategory?id=" + carCatId; $.ajax({ cache: false,url: url,
contentType: 'application/html;charset=utf-8', type: "GET", success: function (result) { if (result) {$("#frameEdit").attr("src", url);
$("#editContent").css("display", "block");
$('#editContent').dialog({toolbar: [],
maximizable: false, draggable: true, resizable: false, closable: false, modal: true, cache: false, //height: '100%',width: 560,
title: "修改类别", onOpen: function () {},
buttons: []
});
}
},
error: function (xhr, status) { alert("加载内容失败" + status);}
});
}
//修改成功后 function refreshAfterEdit() {initData();
$('#tt').datagrid("clearSelections");
$('.editContent').dialog("close");
}
//取消修改 function candelEdit() {$('#tt').datagrid("clearSelections");
$('.editContent').dialog("close");
}
修改的控制器部分把从前台视图拿到的参数id存放到ViewData中,传到到编辑子窗口,异步加载强类型部分视图的时候再用到这个id。
#region 编辑分类public ActionResult EditCarCategory(int id)
{ ViewData["id"] = id; return View();}
public ActionResult EditModel(int id)
{var carCategoryDb = CarCategoryRepository.LoadEntities(c => c.ID == id).FirstOrDefault();
CarCategoryVm carCategoryVm = AutoMapper.Mapper.DynamicMap<CarCategory, CarCategoryVm>(carCategoryDb);
return PartialView(carCategoryVm);}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult EditCarCategory(CarCategoryVm carCategoryVm) { if (ModelState.IsValid) { carCategoryVm.Status = (short)StatusEnum.Enable; carCategoryVm.DelFlag = (short)DelFlagEnum.Normal;carCategoryVm.SubTime = DateTime.Now;
CarCategory carCategory = AutoMapper.Mapper.DynamicMap<CarCategoryVm, CarCategory>(carCategoryVm);
CarCategoryRepository.UpdateEntity(carCategory);
CarCategoryRepository.SaveChanges();
return Json(new {msg = true});
}
else {return PartialView("EditModel", carCategoryVm);
}
}
#endregion 编辑强类型部分视图中, @Html.HiddenFor(c => c.ParentID,new {id = "categoryId"})隐藏域用来显示由控制器方法传递的视图模型中的ParentID值。并且,会根据这个ParentID把对应的节点名称显示到<input id="getCategory" type="text" value="==请选择分类==" />上。
@model Car.Test.Portal.Models.CarCategoryVm
@using (Html.BeginForm("EditCarCategory", "Home", FormMethod.Post, new { id = "editForm" }))
{@Html.AntiForgeryToken()
@Html.HiddenFor(c => c.ID)
<ul>
<li>
<span class="fdwith">
@Html.LabelFor(c => c.Name)
</span>
@Html.EditorFor(c => c.Name)
@Html.ValidationMessageFor(c => c.Name)
</li>
<li>
<span class="fdwith">
@Html.LabelFor(c => c.PreLetter)
</span>
@Html.EditorFor(c => c.PreLetter)
@Html.ValidationMessageFor(c => c.PreLetter)
</li>
<li>
<span class="fdwith">
@Html.LabelFor(c => c.ParentID)
</span>
<input id="getCategory" type="text" value="==请选择分类==" />
@Html.ValidationMessageFor(c => c.ParentID)
@Html.HiddenFor(c => c.ParentID,new {id = "categoryId"})
</li>
<li>
<span class="fdwith">
@Html.LabelFor(c => c.Level)
</span>
@Html.EditorFor(c => c.Level)
@Html.ValidationMessageFor(c => c.Level)
</li>
<li>
<span class="fdwith">
@Html.LabelFor(c => c.IsLeaf)
</span>
@Html.DropDownListFor(c => c.IsLeaf,new[] {new SelectListItem(){Text = "是",Value = bool.TrueString},
new SelectListItem(){Text = "否",Value = bool.FalseString}
},"==选择是否为叶节点==",new {id="ld"})
@Html.ValidationMessageFor(c => c.IsLeaf)
</li>
</ul>
}
编辑视图的原理与添加视图类似,只不过要根据从控制器方法传递过来的视图模型中的ParentID所对应的节点名称显示到强类型部分视图的<input id="getCategory" type="text" value="==请选择分类==" />上。
展开
批量删除
在前台视图中,把拿到的id拼接成"1,2,3,"形式,然后在控制器方法中,把最后的逗号去掉,再Split成数组,把其中的元素放到List<int>集合中作为服务层方法的参数,对领域模型实施逻辑删除。视图部分在查询和显示部分已有,控制器部分如下:
[HttpPost]
public ActionResult DeleteCarCategories() { var strIds = Request["ids"];strIds = strIds.Substring(0, strIds.Length - 1);
string[] ids = strIds.Split('_');
List<int> list = new List<int>();
foreach (var item in ids)
{int id = int.Parse(item);
list.Add(id);
}
if (DeleteCarCategoriesByBatch(list) > 0) {return Json(new {msg = true});
}
else {return Json(new { msg = "no"});
}
}
private int DeleteCarCategoriesByBatch(List<int> ids)
{var carCategories = CarCategoryRepository.LoadEntities(c => ids.Contains(c.ID)).ToList();
for (int i = 0; i < carCategories.Count(); i++)
{ carCategories[i].DelFlag = (short)DelFlagEnum.Delete;carCategories[i].SubTime = DateTime.Now;
}
return CarCategoryRepository.SaveChanges();}
#endregion