项目架构开发:展现层(上)
上次我们创建了项目的服务层,服务层在业务逻辑简单,或项目运行初期不是很容易体现出他的价值;传送门:项目架构开发:服务层(上)
服务层专门处理非业务逻辑的一些功能,比如缓存、异常处理、组织多个应用逻辑等;这次我们搭建最上层的展现层,用到的知识面包括以下:
asp.net mvc5 + bootstrap + autofac + AutoMapper
这次我们没有用服务层,而是直接调用应用逻辑层接口方法,其实对小项目来说,这样已经足够了;服务层我们下次再讲吧
现在开始吧!
1、创建MVC + UnitTest
先搭建个框架,网上找的后台模板
2、ViewModel
UI的数据载体最好新建一个viewmodel,这样就不用依赖DTO或PO,因为页面上显示的数据实体一般比较大,会封装比DTO多的多的属性
LoginUserViewModel.cs
1 using Infrastructure.Common; 2 using System; 3 using System.Collections.Generic; 4 5 namespace Presentation.MVC.Models 6 { 7 public class LoginUserViewModel 8 { 9 public int RowNumber { get; set; } 10 11 public Guid Id { get; set; } 12 public string LoginName { get; set; } 13 public short? IsEnabled { get; set; } 14 public DateTime? CreateTime { get; set; } 15 } 16 17 public class LoginUserListViewModel 18 { 19 public List<LoginUserViewModel> Items { get; set; } 20 } 21 22 public class LoginUserPageViewModel : PageViewModelBase 23 { 24 public List<LoginUserViewModel> Items { get; set; } 25 } 26 }
PageViewModelBase.cs (这个是分页时候用的,如上边标红实体)
1 using Infrastructure.Common; 2 3 namespace Presentation.MVC.Models 4 { 5 public class PageViewModelBase 6 { 7 public bool IsFirst { get; set; } 8 public bool IsLast { get; set; } 9 10 public Page Page { get; set; } 11 public int Total { get; set; } 12 public int TotalPage 13 { 14 get 15 { 16 return (Total % Page.PageSize) == 0 ? Total / Page.PageSize : (Total / Page.PageSize) + 1; 17 } 18 } 19 20 public int PrePage 21 { 22 get 23 { 24 if (Page.PageIndex == 1) 25 { 26 IsFirst = true; 27 return 1; 28 } 29 else 30 { 31 IsFirst = false; 32 return Page.PageIndex - 1; 33 } 34 } 35 } 36 public int NextPage 37 { 38 get 39 { 40 if (Page.PageIndex == TotalPage) 41 { 42 IsLast = true; 43 return TotalPage; 44 } 45 else 46 { 47 IsLast = false; 48 return Page.PageIndex + 1; 49 } 50 } 51 } 52 } 53 }
3、映射
主要是DTO映射成ViewModel,这里我们用的是AutoMapper
AutoMapperConfiguration.cs,AutoMapper用法很简单,引用他,然后像下边代码那样写,然后再应用启动的时候加载
1 using AutoMapper; 2 using Business.ReverseDDD.Model; 3 using Presentation.MVC.Models; 4 5 namespace Presentation.MVC.Mappings 6 { 7 public class AutoMapperConfiguration 8 { 9 public static void Configure() 10 { 11 Mapper.CreateMap<LoginUser, LoginUserViewModel>(); 12 13 } 14 } 15 }
Global.asax.cs
1 protected void Application_Start() 2 { 3 AreaRegistration.RegisterAllAreas(); 4 FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); 5 RouteConfig.RegisterRoutes(RouteTable.Routes); 6 BundleConfig.RegisterBundles(BundleTable.Bundles); 7 8 AutoMapperConfiguration.Configure(); 9 }
4、视图
为了方便,CURD都在一个页面实现了
1 @model Presentation.MVC.Models.LoginUserPageViewModel 2 @{ 3 ViewBag.Title = "Index"; 4 Layout = "~/Views/Shared/_Bootstrap.cshtml"; 5 } 6 7 <div class="container"> 8 <h2></h2> 9 <div class="panel panel-default"> 10 <div class="panel-heading"> 11 <h4 class="panel-title"> 12 <a data-toggle="collapse" data-parent="#accordion" 13 href="#collapseOne" id="actionType"> 14 新增用户 15 </a> 16 </h4> 17 </div> 18 <div id="collapseOne" class="panel-collapse collapse"> 19 <div class="panel-body"> 20 21 @using (Html.BeginForm("Add", "LoginUser", null, FormMethod.Post, new { @id = "formLoginUser", @class = "form-horizontal", role = "form" })) 22 { 23 @Html.AntiForgeryToken() 24 25 <div class="form-group"> 26 <label for="firstname" class="col-sm-2 control-label">登录名</label> 27 <div class="col-sm-10"> 28 <input type="text" class="form-control" name="LoginName" id="LoginName" 29 placeholder="请输入登录账户名" /> 30 </div> 31 </div> 32 <div class="form-group"> 33 <label for="lastname" class="col-sm-2 control-label">登录密码</label> 34 <div class="col-sm-10"> 35 <input type="text" class="form-control" name="Password" id="Password" 36 placeholder="请输入登录密码" /> 37 </div> 38 </div> 39 <div class="form-group"> 40 <div class="col-sm-offset-2 col-sm-10"> 41 <div class="checkbox"> 42 <label> 43 <input type="checkbox" name="IsEnabled" id="IsEnabled" value="1" /> 是否有效 44 </label> 45 </div> 46 </div> 47 </div> 48 49 <div class="form-group"> 50 <div class="col-sm-offset-2 col-sm-10"> 51 <input type="hidden" name="Id" id="Id" /> 52 <button type="submit" class="btn btn-default">提交</button> 53 </div> 54 </div> 55 } 56 57 </div> 58 </div> 59 </div> 60 61 62 <h4>用户列表</h4> 63 <table id="list" class="table table-striped table-bordered table-hover table-condensed"> 64 <thead> 65 <tr> 66 <th>序号</th> 67 <th>登录名</th> 68 <th>是否有效</th> 69 <th>创建时间</th> 70 <th>操作</th> 71 </tr> 72 </thead> 73 <tbody> 74 @foreach (var item in Model.Items) 75 { 76 <tr> 77 <td>@item.RowNumber</td> 78 <td>@item.LoginName</td> 79 <td>@item.IsEnabled</td> 80 <td>@item.CreateTime</td> 81 <td> 82 <button type="button" class="btn btn-link btn-xs" key="@item.Id" action="edit">修改</button> 83 <button type="button" class="btn btn-link btn-xs" key="@item.Id" action="delete">删除</button> 84 </td> 85 </tr> 86 } 87 </tbody> 88 </table> 89 90 <ul class="pagination"> 91 <li><a href="/LoginUsers/@Model.PrePage">«</a></li> 92 @for (int index = 1; index <= Model.TotalPage; index++) 93 { 94 if (Model.Page.PageIndex == index) 95 { 96 <li class="active"><a href="/LoginUsers/@index">@index</a></li> 97 } 98 else 99 { 100 <li><a href="/LoginUsers/@index">@index</a></li> 101 } 102 } 103 <li><a href="/LoginUsers/@Model.NextPage">»</a></li> 104 </ul> 105 106 <script type="text/javascript"> 107 $(function () { 108 $(".btn-link").click(function () { 109 var obj = $(this); 110 var key = obj.attr("key"); 111 if (key == undefined || key == null || key == "") return; 112 113 var action = obj.attr("action"); 114 115 if (action == "delete") { 116 CreateDeleteWindow(function () { 117 location.href = "/LoginUser/Delete/" + key; 118 }); 119 } 120 121 if (action == "edit") { 122 $.ajax({ 123 url: "/LoginUser/Edit/" + key, 124 type: "GET", 125 dataType: 'json', 126 success: function (result) { 127 $("#Id").val(result.Id); 128 $("#LoginName").val(result.LoginName); 129 if (result.IsEnabled == 1) $("#IsEnabled").attr("checked", "true"); 130 else $("#IsEnabled").removeAttr("checked"); 131 132 $('#collapseOne').collapse('show'); 133 $("#formLoginUser").attr("action", "/LoginUser/Edit"); 134 $("#actionType").html("修改用户"); 135 }, 136 error: function (e) { 137 alert(e); 138 } 139 }); 140 } 141 }); 142 143 }); 144 </script> 145 146 </div>
其实也没什么特别的就是用了Bootstrap美化页面样式,对Bootstrap不懂的请点击这里
5、控制器
LoginUserController.cs
1 using AutoMapper; 2 using Business.DTO.Request; 3 using Business.ReverseDDD.IApplication; 4 using Infrastructure.Common; 5 using LingExtensions; 6 using Presentation.MVC.Models; 7 using System; 8 using System.Collections.Generic; 9 using System.Web.Mvc; 10 11 namespace Presentation.MVC.Controllers 12 { 13 public class LoginUserController : Controller 14 { 15 private ILoginUserApplication loginUserApplication; 16 17 public LoginUserController(ILoginUserApplication loginUserApplication) 18 { 19 this.loginUserApplication = loginUserApplication; 20 } 21 22 [Route("LoginUsers/{PageIndex=1}")] 23 public ActionResult Index(string PageIndex) 24 { 25 Page page = new Page(); 26 page.PageIndex = PageIndex.ToInt(1); 27 28 var model = new LoginUserPageViewModel(); 29 var soure = this.loginUserApplication.GetPage(page, w => w.OrderByDescending(t => t.CreateTime)); 30 31 model.Items = Mapper.Map<List<LoginUserViewModel>>(soure.Item2); 32 model.Total = soure.Item1; 33 model.Page = page; 34 35 return View(model); 36 } 37 38 [HttpPost] 39 [ValidateAntiForgeryToken] 40 public ActionResult Add(LoginUserCURequest entity) 41 { 42 this.loginUserApplication.Add(new LoginUserCURequest() 43 { 44 Id = Guid.NewGuid(), 45 LoginName = entity.LoginName, 46 Password = entity.Password, 47 IsEnabled = entity.IsEnabled 48 }); 49 50 return RedirectToAction("Index"); 51 } 52 53 [HttpGet] 54 public ActionResult Delete(string id) 55 { 56 this.loginUserApplication.Delete(Guid.Parse(id)); 57 58 return RedirectToAction("Index"); 59 } 60 61 [HttpGet] 62 public ActionResult Edit(Guid id) 63 { 64 var soure = this.loginUserApplication.Get(id); 65 66 return Json(soure, JsonRequestBehavior.AllowGet); 67 } 68 69 [HttpPost] 70 [ValidateAntiForgeryToken] 71 public ActionResult Edit(LoginUserCURequest entity) 72 { 73 this.loginUserApplication.Update(new LoginUserCURequest() 74 { 75 Id = entity.Id, 76 LoginName = entity.LoginName, 77 Password = entity.Password, 78 IsEnabled = entity.IsEnabled 79 }); 80 81 return RedirectToAction("Index"); 82 } 83 } 84 }
都是演示CURD的功能,大家不要在意这些细节。。
看标红的地方,意思是将soure.Item2(是Tuple<int, IEnumerable<LoginUser>>类型)转换成List<LoginUserViewModel>
这就是AutoMapper的用法
还有是这里没有依赖具体应用逻辑组件的,只依赖了业务逻辑接口using Business.ReverseDDD.IApplication;
这个是为了解耦,而且对分层并行开发很有用,项目前端后端开发都不用依赖谁开发完才能往下继续;
控制器我们用的是依赖注入Autofac组件:
6、UnitTest
LoginUserControllerTest.cs, 记得也要Mapping哦
测试通过了
7、UI,我们来看看界面功能
7.1 新增用户
用户新增成功,列表正常显示数据
7.2 修改数据
修改成功
7.3 分页正常
至此,展现层完成了
8、完整项目架构如下