展现层实现增删改查
原文作者:圣杰
在原文作者上进行改正,适配ABP新版本。内容相同(本章未做任何更改)
这一章节将通过完善Controller、View、ViewModel,来实现展现层的增删改查。最终实现效果如下图:
一、定义Controller
ABP对ASP.NET MVC Controllers进行了集成,通过引入Abp.Web.Mvc命名空间,创建Controller继承自AbpController, 我们即可使用ABP附加给我们的以下强大功能:
- 本地化
- 异常处理
- 对返回的JsonResult进行包装
- 审计日志
- 权限认证([AbpMvcAuthorize]特性)
- 工作单元(默认未开启,通过添加[UnitOfWork]开启)
1,创建TasksController继承自AbpController
通过构造函数注入对应用服务的依赖。
1 [AbpMvcAuthorize] 2 public class TasksController : AbpController 3 { 4 private readonly ITaskAppService _taskAppService; 5 private readonly IUserAppService _userAppService; 6 7 public TasksController(ITaskAppService taskAppService, IUserAppService userAppService) 8 { 9 _taskAppService = taskAppService; 10 _userAppService = userAppService; 11 } 12 }
二、创建列表展示分部视图(_List.cshtml)
在分部视图中,我们通过循环遍历,输出任务清单。
1 @model IEnumerable<LearningMpaAbp.Tasks.Dtos.TaskDto> 2 <div> 3 <ul class="list-group"> 4 @foreach (var task in Model) 5 { 6 <li class="list-group-item"> 7 <div class="btn-group pull-right"> 8 <button type="button" class="btn btn-info" onclick="editTask(@task.Id);">Edit</button> 9 <button type="button" class="btn btn-success" onclick="deleteTask(@task.Id);">Delete</button> 10 </div> 11 12 <div class="media"> 13 <a class="media-left" href="#"> 14 <i class="fa @task.GetTaskLable() fa-3x"></i> 15 </a> 16 <div class="media-body"> 17 <h4 class="media-heading">@task.Title</h4> 18 <p class="text-info">@task.AssignedPersonName</p> 19 <span class="text-muted">@task.CreationTime.ToString("yyyy-MM-dd HH:mm:ss")</span> 20 </div> 21 </div> 22 23 </li> 24 } 25 </ul> 26 </div>
三,创建新增分部视图(_CreateTask.cshtml)
为了好的用户体验,我们采用异步加载的方式来实现任务的创建。
1,引入js文件
使用异步提交需要引入jquery.validate.unobtrusive.min.js
和jquery.unobtrusive-ajax.min.js
,其中jquery.unobtrusive-ajax.min.js
,需要通过Nuget安装微软的Microsoft.jQuery.Unobtrusive.Ajax
包获取。
然后通过捆绑一同引入到视图中。打开App_Start文件夹下的BundleConfig.cs,添加以下代码:
1 bundles.Add( 2 new ScriptBundle("~/Bundles/unobtrusive/js") 3 .Include( 4 "~/Scripts/jquery.validate.unobtrusive.min.js", 5 "~/Scripts/jquery.unobtrusive-ajax.min.js" 6 ) 7 );
找到Views/Shared/_Layout.cshtml,添加对捆绑的js引用。
1 @Scripts.Render("~/Bundles/vendor/js/bottom") 2 @Scripts.Render("~/Bundles/js") 3 //在此处添加下面一行代码 4 @Scripts.Render("~/Bundles/unobtrusive/js")
2,创建分部视图
其中用到了Bootstrap-Modal,Ajax.BeginForm,对此不了解的可以参考
Ajax.BeginForm()知多少
Bootstrap-Modal的用法介绍
该Partial View绑定CreateTaskInput模型。最终_CreateTask.cshtml
代码如下:
1 @model LearningMpaAbp.Tasks.Dtos.CreateTaskInput 2 3 @{ 4 ViewBag.Title = "Create"; 5 } 6 <div class="modal fade" id="add" tabindex="-1" role="dialog" aria-labelledby="createTask" data-backdrop="static"> 7 <div class="modal-dialog" role="document"> 8 <div class="modal-content"> 9 <div class="modal-header"> 10 <button type="button" class="close" data-dismiss="modal"><span aria-hidden="true">×</span><span class="sr-only">Close</span></button> 11 <h4 class="modal-title" id="myModalLabel">Create Task</h4> 12 </div> 13 <div class="modal-body" id="modalContent"> 14 15 @using (Ajax.BeginForm("Create", "Tasks", new AjaxOptions() 16 { 17 UpdateTargetId = "taskList", 18 InsertionMode = InsertionMode.Replace, 19 OnBegin = "beginPost('#add')", 20 OnSuccess = "hideForm('#add')", 21 OnFailure = "errorPost(xhr, status, error,'#add')" 22 })) 23 { 24 @Html.AntiForgeryToken() 25 <div class="form-horizontal"> 26 <h4>Task</h4> 27 <hr /> 28 @Html.ValidationSummary(true, "", new { @class = "text-danger" }) 29 <div class="form-group"> 30 @Html.LabelFor(model => model.AssignedPersonId, "AssignedPersonId", htmlAttributes: new { @class = "control-label col-md-2" }) 31 <div class="col-md-10"> 32 @Html.DropDownList("AssignedPersonId", null, htmlAttributes: new { @class = "form-control" }) 33 @Html.ValidationMessageFor(model => model.AssignedPersonId, "", new { @class = "text-danger" }) 34 </div> 35 </div> 36 37 <div class="form-group"> 38 @Html.LabelFor(model => model.Title, htmlAttributes: new { @class = "control-label col-md-2" }) 39 <div class="col-md-10"> 40 @Html.EditorFor(model => model.Title, new { htmlAttributes = new { @class = "form-control" } }) 41 @Html.ValidationMessageFor(model => model.Title, "", new { @class = "text-danger" }) 42 </div> 43 </div> 44 45 <div class="form-group"> 46 @Html.LabelFor(model => model.Description, htmlAttributes: new { @class = "control-label col-md-2" }) 47 <div class="col-md-10"> 48 @Html.EditorFor(model => model.Description, new { htmlAttributes = new { @class = "form-control" } }) 49 @Html.ValidationMessageFor(model => model.Description, "", new { @class = "text-danger" }) 50 </div> 51 </div> 52 53 <div class="form-group"> 54 @Html.LabelFor(model => model.State, htmlAttributes: new { @class = "control-label col-md-2" }) 55 <div class="col-md-10"> 56 @Html.EnumDropDownListFor(model => model.State, htmlAttributes: new { @class = "form-control" }) 57 @Html.ValidationMessageFor(model => model.State, "", new { @class = "text-danger" }) 58 </div> 59 </div> 60 61 <div class="form-group"> 62 <div class="col-md-offset-2 col-md-10"> 63 <button type="submit" class="btn btn-default">Create</button> 64 </div> 65 </div> 66 </div> 67 } 68 </div> 69 </div> 70 </div> 71 </div>
对应Controller代码:
1 [ChildActionOnly] 2 public PartialViewResult Create() 3 { 4 var userList = _userAppService.GetUsers(); 5 ViewBag.AssignedPersonId = new SelectList(userList.Items, "Id", "Name"); 6 return PartialView("_CreateTask"); 7 } 8 9 [HttpPost] 10 [ValidateAntiForgeryToken] 11 public ActionResult Create(CreateTaskInput task) 12 { 13 var id = _taskAppService.CreateTask(task); 14 15 var input = new GetTasksInput(); 16 var output = _taskAppService.GetTasks(input); 17 18 return PartialView("_List", output.Tasks); 19 }
四、创建更新分部视图(_EditTask.cshtml)
同样,该视图也采用异步更新方式,也采用Bootstrap-Modal,Ajax.BeginForm()技术。该Partial View绑定UpdateTaskInput模型。
1 @model LearningMpaAbp.Tasks.Dtos.UpdateTaskInput 2 @{ 3 ViewBag.Title = "Edit"; 4 } 5 6 <div class="modal fade" id="editTask" tabindex="-1" role="dialog" aria-labelledby="editTask" data-backdrop="static"> 7 8 <div class="modal-dialog" role="document"> 9 <div class="modal-content"> 10 <div class="modal-header"> 11 <button type="button" class="close" data-dismiss="modal"><span aria-hidden="true">×</span><span class="sr-only">Close</span></button> 12 <h4 class="modal-title" id="myModalLabel">Edit Task</h4> 13 </div> 14 <div class="modal-body" id="modalContent"> 15 16 @using (Ajax.BeginForm("Edit", "Tasks", new AjaxOptions() 17 { 18 UpdateTargetId = "taskList", 19 InsertionMode = InsertionMode.Replace, 20 OnBegin = "beginPost('#editTask')", 21 OnSuccess = "hideForm('#editTask')" 22 })) 23 { 24 @Html.AntiForgeryToken() 25 26 <div class="form-horizontal"> 27 <h4>Task</h4> 28 <hr /> 29 @Html.ValidationSummary(true, "", new { @class = "text-danger" }) 30 @Html.HiddenFor(model => model.Id) 31 32 <div class="form-group"> 33 @Html.LabelFor(model => model.AssignedPersonId, "AssignedPersonId", htmlAttributes: new { @class = "control-label col-md-2" }) 34 <div class="col-md-10"> 35 @Html.DropDownList("AssignedPersonId", null, htmlAttributes: new { @class = "form-control" }) 36 @Html.ValidationMessageFor(model => model.AssignedPersonId, "", new { @class = "text-danger" }) 37 </div> 38 </div> 39 40 <div class="form-group"> 41 @Html.LabelFor(model => model.Title, htmlAttributes: new { @class = "control-label col-md-2" }) 42 <div class="col-md-10"> 43 @Html.EditorFor(model => model.Title, new { htmlAttributes = new { @class = "form-control" } }) 44 @Html.ValidationMessageFor(model => model.Title, "", new { @class = "text-danger" }) 45 </div> 46 </div> 47 48 <div class="form-group"> 49 @Html.LabelFor(model => model.Description, htmlAttributes: new { @class = "control-label col-md-2" }) 50 <div class="col-md-10"> 51 @Html.EditorFor(model => model.Description, new { htmlAttributes = new { @class = "form-control" } }) 52 @Html.ValidationMessageFor(model => model.Description, "", new { @class = "text-danger" }) 53 </div> 54 </div> 55 56 <div class="form-group"> 57 @Html.LabelFor(model => model.State, htmlAttributes: new { @class = "control-label col-md-2" }) 58 <div class="col-md-10"> 59 @Html.EnumDropDownListFor(model => model.State, htmlAttributes: new { @class = "form-control" }) 60 @Html.ValidationMessageFor(model => model.State, "", new { @class = "text-danger" }) 61 </div> 62 </div> 63 64 <div class="form-group"> 65 <div class="col-md-offset-2 col-md-10"> 66 <input type="submit" value="Save" class="btn btn-default" /> 67 </div> 68 </div> 69 </div> 70 } 71 72 </div> 73 </div> 74 </div> 75 </div> 76 <script type="text/javascript"> 77 //该段代码十分重要,确保异步调用后jquery能正确执行验证逻辑 78 $(function () { 79 //allow validation framework to parse DOM 80 $.validator.unobtrusive.parse('form'); 81 }); 82 </script>
后台代码:
1 public PartialViewResult Edit(int id) 2 { 3 var task = _taskAppService.GetTaskById(id); 4 5 var updateTaskDto = AutoMapper.Mapper.Map<UpdateTaskInput>(task); 6 7 var userList = _userAppService.GetUsers(); 8 ViewBag.AssignedPersonId = new SelectList(userList.Items, "Id", "Name", updateTaskDto.AssignedPersonId); 9 10 return PartialView("_EditTask", updateTaskDto); 11 } 12 13 [HttpPost] 14 [ValidateAntiForgeryToken] 15 public ActionResult Edit(UpdateTaskInput updateTaskDto) 16 { 17 _taskAppService.UpdateTask(updateTaskDto); 18 19 var input = new GetTasksInput(); 20 var output = _taskAppService.GetTasks(input); 21 22 return PartialView("_List", output.Tasks); 23 }
五,创建Index视图
在首页中,我们一般会用来展示列表,并通过弹出模态框的方式来进行新增更新删除。为了使用ASP.NET MVC强视图带给我们的好处(模型绑定、输入校验等等),我们需要创建一个ViewModel来进行模型绑定。因为Abp提倡为每个不同的应用服务提供不同的Dto进行数据交互,新增对应CreateTaskInput,更新对应UpdateTaskInput,展示对应TaskDto。那我们创建的ViewModel就需要包含这几个模型,方可在一个视图中完成多个模型的绑定。
1,创建视图模型(IndexViewModel)
1 namespace LearningMpaAbp.Web.Models.Tasks 2 { 3 public class IndexViewModel 4 { 5 /// <summary> 6 /// 用来进行绑定列表过滤状态 7 /// </summary> 8 public TaskState? SelectedTaskState { get; set; } 9 10 /// <summary> 11 /// 列表展示 12 /// </summary> 13 public IReadOnlyList<TaskDto> Tasks { get; } 14 15 /// <summary> 16 /// 创建任务模型 17 /// </summary> 18 public CreateTaskInput CreateTaskInput { get; set; } 19 20 /// <summary> 21 /// 更新任务模型 22 /// </summary> 23 public UpdateTaskInput UpdateTaskInput { get; set; } 24 25 public IndexViewModel(IReadOnlyList<TaskDto> items) 26 { 27 Tasks = items; 28 } 29 30 /// <summary> 31 /// 用于过滤下拉框的绑定 32 /// </summary> 33 /// <returns></returns> 34 35 public List<SelectListItem> GetTaskStateSelectListItems() 36 { 37 var list=new List<SelectListItem>() 38 { 39 new SelectListItem() 40 { 41 Text = "AllTasks", 42 Value = "", 43 Selected = SelectedTaskState==null 44 } 45 }; 46 47 list.AddRange(Enum.GetValues(typeof(TaskState)) 48 .Cast<TaskState>() 49 .Select(state=>new SelectListItem() 50 { 51 Text = $"TaskState_{state}", 52 Value = state.ToString(), 53 Selected = state==SelectedTaskState 54 }) 55 ); 56 return list; 57 } 58 } 59 }
2,创建视图
Index视图,通过加载Partial View的形式,将列表、新增视图一次性加载进来。
1 @using Abp.Web.Mvc.Extensions 2 @model LearningMpaAbp.Web.Models.Tasks.IndexViewModel 3 4 @{ 5 ViewBag.Title = L("TaskList"); 6 ViewBag.ActiveMenu = "TaskList"; //Matches with the menu name in SimpleTaskAppNavigationProvider to highlight the menu item 7 } 8 @section scripts{ 9 @Html.IncludeScript("~/Views/Tasks/index.js"); 10 } 11 <h2> 12 @L("TaskList") 13 14 <button type="button" class="btn btn-primary" data-toggle="modal" data-target="#add">Create Task</button> 15 16 <a class="btn btn-primary" data-toggle="modal" href="@Url.Action("RemoteCreate")" data-target="#modal" role="button">(Create Task)使用Remote方式调用Modal进行展现</a> 17 18 <!--任务清单按照状态过滤的下拉框--> 19 <span class="pull-right"> 20 @Html.DropDownListFor( 21 model => model.SelectedTaskState, 22 Model.GetTaskStateSelectListItems(), 23 new 24 { 25 @class = "form-control select2", 26 id = "TaskStateCombobox" 27 }) 28 </span> 29 </h2> 30 31 <!--任务清单展示--> 32 <div class="row" id="taskList"> 33 @{ Html.RenderPartial("_List", Model.Tasks); } 34 </div> 35 36 <!--通过初始加载页面的时候提前将创建任务模态框加载进来--> 37 @Html.Action("Create") 38 39 <!--编辑任务模态框通过ajax动态填充到此div中--> 40 <div id="edit"> 41 42 </div> 43 44 <!--Remote方式弹出创建任务模态框--> 45 <div class="modal fade" id="modal" tabindex="-1" role="dialog" aria-labelledby="createTask" data-backdrop="static"> 46 <div class="modal-dialog" role="document"> 47 <div class="modal-content"> 48 49 </div> 50 </div> 51 </div>
3,Remote方式创建任务讲解
Remote方式就是,点击按钮的时候去加载创建任务的PartialView到指定的div中。而我们代码中另一种方式是通过@Html.Action("Create")
的方式,在加载Index的视图的作为子视图同步加载了进来。
感兴趣的同学自行查看源码,不再讲解。
1 <a class="btn btn-primary" data-toggle="modal" href="@Url.Action("RemoteCreate")" data-target="#modal" role="button">(Create Task)使用Remote方式调用Modal进行展现</a> 2 3 <!--Remote方式弹出创建任务模态框--> 4 <div class="modal fade" id="modal" tabindex="-1" role="dialog" aria-labelledby="createTask" data-backdrop="static"> 5 <div class="modal-dialog" role="document"> 6 <div class="modal-content"> 7 8 </div> 9 </div> 10 </div>
4,后台代码
1 public ActionResult Index(GetTasksInput input) 2 { 3 var output = _taskAppService.GetTasks(input); 4 5 var model = new IndexViewModel(output.Tasks) 6 { 7 SelectedTaskState = input.State 8 9 }; 10 return View(model); 11 }
5,js代码(index.js)
1 var taskService = abp.services.app.task; 2 3 (function ($) { 4 5 $(function () { 6 7 var $taskStateCombobox = $('#TaskStateCombobox'); 8 9 $taskStateCombobox.change(function () { 10 getTaskList(); 11 }); 12 13 var $modal = $(".modal"); 14 //显示modal时,光标显示在第一个输入框 15 $modal.on('shown.bs.modal', 16 function () { 17 $modal.find('input:not([type=hidden]):first').focus(); 18 }); 19 20 }); 21 })(jQuery); 22 23 //异步开始提交时,显示遮罩层 24 function beginPost(modalId) { 25 var $modal = $(modalId); 26 27 abp.ui.setBusy($modal); 28 } 29 30 //异步开始提交结束后,隐藏遮罩层并清空Form 31 function hideForm(modalId) { 32 var $modal = $(modalId); 33 34 var $form = $modal.find("form"); 35 abp.ui.clearBusy($modal); 36 $modal.modal("hide"); 37 //创建成功后,要清空form表单 38 $form[0].reset(); 39 } 40 41 //处理异步提交异常 42 function errorPost(xhr, status, error, modalId) { 43 if (error.length>0) { 44 abp.notify.error('Something is going wrong, please retry again later!'); 45 var $modal = $(modalId); 46 abp.ui.clearBusy($modal); 47 } 48 } 49 50 function editTask(id) { 51 abp.ajax({ 52 url: "/tasks/edit", 53 data: { "id": id }, 54 type: "GET", 55 dataType: "html" 56 }) 57 .done(function (data) { 58 $("#edit").html(data); 59 $("#editTask").modal("show"); 60 }) 61 .fail(function (data) { 62 abp.notify.error('Something is wrong!'); 63 }); 64 } 65 66 function deleteTask(id) { 67 abp.message.confirm( 68 "是否删除Id为" + id + "的任务信息", 69 function (isConfirmed) { 70 if (isConfirmed) { 71 taskService.deleteTask(id) 72 .done(function () { 73 abp.notify.info("删除任务成功!"); 74 getTaskList(); 75 }); 76 } 77 } 78 ); 79 80 } 81 82 function getTaskList() { 83 var $taskStateCombobox = $('#TaskStateCombobox'); 84 var url = '/Tasks/GetList?state=' + $taskStateCombobox.val(); 85 abp.ajax({ 86 url: url, 87 type: "GET", 88 dataType: "html" 89 }) 90 .done(function (data) { 91 $("#taskList").html(data); 92 }); 93 }
js代码中处理了Ajax回调函数,以及任务状态过滤下拉框更新事件,编辑、删除任务代码。其中getTaskList()函数是用来异步刷新列表,对应调用的GetList()Action的后台代码如下:
1 public PartialViewResult GetList(GetTasksInput input) 2 { 3 var output = _taskAppService.GetTasks(input); 4 return PartialView("_List", output.Tasks); 5 }
六、总结
至此,完成了任务的增删改查。展现层主要用到了Asp.net mvc的强类型视图、Bootstrap-Modal、Ajax异步提交技术。
其中需要注意的是,在异步加载表单时,需要添加以下js代码,jquery方能进行前端验证。
1 <script type="text/javascript"> 2 $(function () { 3 //allow validation framework to parse DOM 4 $.validator.unobtrusive.parse('form'); 5 }); 6 </script>