MVC常遇见的几个场景代码分享
本次主要分享几个场景的处理代码,有更好处理方式多多交流,相互促进进步;代码由来主要是这几天使用前端Ace框架做后台管理系统,这Ace是H5框架里面的控件效果挺多的,做兼容也很好,有点遗憾是控件效果基本都是写一起的,分离起来挺麻烦的;这次主要说的是后端代码,以后可以分享下这个框架的使用。
以上是个人的看法,下面来正式分享今天的文章吧:
. 扩展HtmlHelper,枚举转化select下拉框效果
. 自定义ActionFilter,验证登陆和权限访问
. 扩展HtmlHelper,无限递归生成菜单栏
. Global中增加全局Application_Error监控404异常
. 实现及使用缓存工厂(最新缓存工厂代码在上一篇分享的缓存工厂之Redis缓存)
下面一步一个脚印的来分享:
. 扩展HtmlHelper,枚举转化select下拉框效果
枚举转化select下拉框效果,这个效果估计很多同学都遇到过,也一定有自己的想法与实践;因为这里是MVC框架,所以这里我直接扩展HtmlHelper,这样在页面使用起来也很方便;这里了解一下这样的场景,通常枚举在方法中传递都只能是定义好的某一个枚举,这样来生成select标签扩展性就不强;这个时候有朋友就想到如果使用enum来当做方法的参数呢,这样是不行的,enum不能直接用来当做方法参数(看官们可以试试),所以这样看就没法定义一个公共的参数来传递不同枚举了,当然万能的Type给了我们一点曙光,下面我们就使用Type当做参数来传递枚举;
首先,我们既然要扩展HtmlHelper,那必须遵循一定的规则:
1.定义扩展类的类名通常使用Extension结尾,这里咋们定义个名称为HtmlHelperExtension的扩展类
2.扩展方法参数中使用this HtmlHelper html作为第一个参数
3.扩展方法返回MvcHtmlString把内容输出到试图View中
再来,自定义方法如:public static MvcHtmlString DrpDownByEnum(this HtmlHelper html, Type ty, string name = "Status", bool isAll = true);第二个参数就是上面说到的Type,她针对说有类型不仅仅局限于枚举,因为这里说的是使用枚举所以这里的职责就是负责枚举类型的传入,第三个参数是每个html标签都应该具备的name属性值,第四个参数是是否增加全选的选项;下面再来分享下具体代码:
1 /// <summary> 2 /// 根据枚举获取DrpDownList 3 /// </summary> 4 /// <param name="ty"></param> 5 /// <param name="name"></param> 6 /// <param name="isAll"></param> 7 /// <returns></returns> 8 public static MvcHtmlString DrpDownByEnum(this HtmlHelper html, Type ty, string name = "Status", bool isAll = true) 9 { 10 var sbHtml = new StringBuilder(string.Empty); 11 sbHtml.AppendFormat("<select class='form-control' name='{0}'>", name); 12 if (isAll) 13 { 14 15 sbHtml.AppendFormat("<option value='{0}'>{1}</option>", 16 "-1", 17 "==全部=="); 18 } 19 var vals = Enum.GetValues(ty); 20 for (int i = 0; i < vals.Length; i++) 21 { 22 var val = vals.GetValue(i); 23 var text = Enum.Parse(ty, val.ToString()).ToString(); 24 sbHtml.AppendFormat("<option value='{0}'>{1}</option>", 25 (int)val, 26 text); 27 } 28 sbHtml.Append("</select>"); 29 return MvcHtmlString.Create(sbHtml.ToString()); 30 }
最后,定义一个枚举名称为ComStatus,然后在试图View中@using引用咋们自定义的扩展方法命名空间,这样咋们就能直接使用@html.DrpDownByEnum()我们定义的方法了,使用代码如下:
@Html.DrpDownByEnum(typeof(StageEnumHelper.ComStatus))
. 自定义ActionFilter,验证登陆和权限访问
首先,自定义个ActionFilter类名称为CheckActionLoginAttribute并且继承ActionFilterAttribute,重写OnActionExecuting方法,先来看如下整个实现的代码:
1 /// <summary> 2 /// action验证登陆 + 菜单权限 3 /// </summary> 4 public class CheckActionLoginAttribute : ActionFilterAttribute 5 { 6 7 private bool _IsAuthor = false; 8 9 /// <summary> 10 /// 是否验证访问权限(默认需要) 11 /// </summary> 12 /// <param name="IsAuthor"></param> 13 public CheckActionLoginAttribute(bool IsAuthor = true) 14 { 15 _IsAuthor = IsAuthor; 16 } 17 18 public override void OnActionExecuting(ActionExecutingContext filterContext) 19 { 20 filterContext.Result = CheckLoginAndMenu(filterContext.ActionDescriptor, filterContext, _IsAuthor); 21 } 22 23 /// <summary> 24 /// 验证登陆 + 菜单权限 方法 25 /// </summary> 26 /// <param name="descript">Action文档描述</param> 27 /// <param name="context">访问上下文</param> 28 /// <param name="IsAuthor">是否需要验证访问权限</param> 29 /// <returns></returns> 30 public static ActionResult CheckLoginAndMenu(ActionDescriptor descript, ControllerContext context, bool IsAuthor) 31 { 32 ActionResult result = null; 33 var returnUrl = context.HttpContext.Request.Path; 34 var session = CacheRepository.Current(CacheType.BaseCache).GetCache<StageModel.MoUserData>(); 35 if (session == null) 36 { 37 38 //跳转登录页面 39 result = new RedirectResult( 40 string.Format("{0}?returnUrl={1}", 41 "/User/Login", 42 returnUrl 43 ) 44 ); 45 } 46 else if (IsAuthor) 47 { 48 //获取请求的Action路径 49 returnUrl = string.Format("/{0}/{1}", 50 descript.ControllerDescriptor.ControllerName, 51 descript.ActionName); 52 //验证是否有访问权限 53 var isAllow = session.Menus.Any(b => b.Link.ToUpper().IndexOf(returnUrl.ToUpper()) == 0); 54 if (!isAllow) 55 { 56 //无访问权限 57 result = new RedirectResult( 58 string.Format("{0}", 59 "/Error" 60 ) 61 ); 62 } 63 } 64 return result; 65 } 66 }
然后,分析代码可以看到我们提取了一个方法
public static ActionResult CheckLoginAndMenu(ActionDescriptor descript, ControllerContext context, bool IsAuthor)
参数说明:ActionDescriptor:用来获取路由请求的Action和Controller的名称信息;ControllerContext:用来获取当前请求的路由地址,方便后面登陆成功后直接跳转到该次访问的路由地址;bool参数:用来控制是否需要做菜单访问权限的验证;
逻辑说明:
1.咋们做登陆验证的时候使用缓存工厂CacheRepository获取Session数据,如果为null不存在直接通过ActionResult返回跳转登录页面结果,这个结果传递给OnActionExecuting重写时的参数ActionExecutingContext的Result属性,这样就能验证没有登录跳转到登陆页面去并且通过returnUrl参数传递给登陆页面,登录成功后跳转此路由地址
2.由自定义构造函数传入是否需要验证菜单访问权限,这样方便配置管理,有人会问为什么不直接都验证访问权限呢,这个看不通的需求吧;菜单权限验证主要是根据用户访问的路由地址Controller+Action与登陆用户session保存的菜单权限的路径地址相互对比,以此来做菜单权限验证,这里要获取Action,Controller路由名称主要是通过ActionExecutingContext.ActionDescriptor参数获取到的;
最后,来看下CheckActionLogin过滤器怎么调用:
/// <summary> /// 用户中心 /// </summary> /// <returns></returns> [CheckActionLogin] public ActionResult UserCenter() { var userData = CacheRepository.Current(CacheType.BaseCache).GetCache<StageModel.MoUserData>(); return PartialView(userData); } /// <summary> /// 修改密码 /// </summary> /// <returns></returns> [CheckActionLogin(false)] public ActionResult ChangeUser() { var userData = CacheRepository.Current(CacheType.BaseCache).GetCache<StageModel.MoUserData>(); return PartialView(userData); }
效果图:
. 扩展HtmlHelper,无限递归生成菜单栏
首先,因为这里和第一节点讲述的内容都使用了HtmlHelper来扩展,扩展步奏就不多说了,主要来看递归的方法,递归名称解释就是自己可以调用自己,无限的深入层级,我们先来看整体方法:
1 /// <summary> 2 /// 获取menu菜单html格式 3 /// </summary> 4 /// <param name="menusId">角色对应的权限Id集合</param> 5 /// <param name="listMenu">第一次全部菜单数据</param> 6 /// <param name="parentName">父级菜单的名称,多个层级关系名称使用‘|’拼接</parentName> 7 /// <param name="ulClass"></param> 8 /// <returns></returns> 9 public static string GetMenuHtml(List<StageModel.MoMenu> menus, List<StageModel.MoMenu> listMenu = null, string parentName = "", string ulClass = "nav nav-list") 10 { 11 12 //全部菜单数据,第一次全部菜单数据 13 listMenu = listMenu ?? StageClass.GetAllMenus(); 14 if (listMenu == null || listMenu.Count <= 0 || menus.Count <= 0) { return ""; } 15 16 //获取当前请求路径 17 var currentPath = HttpContext.Current.Request.Path; 18 //html结构 19 var sbHtml = new StringBuilder(string.Empty); 20 21 sbHtml.AppendFormat("<ul class='submenu {0}'>", ulClass); 22 foreach (var item in listMenu) 23 { 24 25 //查询是否有对应Id的菜单 26 var isExists = menus.Any(b => b.Id == item.Id); 27 var nowName = parentName + "|" + item.Name; 28 if (!isExists) 29 { 30 //不存在 31 if ((item.ListMenu == null || item.ListMenu.Count <= 0)) { continue; } 32 //还有子级,继续递归 33 var currentNav = currentPath.Contains(item.Link) ? "nav-show" : "nav-hide"; 34 35 sbHtml.AppendFormat(@" 36 37 <li class=''> 38 <a href='#' data-url='{0}' class='dropdown-toggle'> 39 <i class='menu-icon fa {3}'></i> 40 <span class='menu-text'> {1} </span> 41 <b class='arrow fa fa-angle-down'></b> 42 </a> 43 <b class='arrow'></b> 44 {2} 45 </li> 46 ", 47 "", 48 item.Name, 49 GetMenuHtml(menus, item.ListMenu, nowName, currentNav), 50 string.IsNullOrEmpty(item.Icon) ? StageModel.MoIcon.Default : item.Icon); 51 } 52 else 53 { 54 55 //存在 56 //无子级,本级已经是最后一级 57 sbHtml.AppendFormat(@" 58 59 <li class=''> 60 <a href='#' data-url='{0}' data-menus='{3}' data-des='{4}'> 61 <i class='menu-icon fa {2}'></i> 62 <span class='menu-text'> {1} </span> 63 </a> 64 65 <b class='arrow'></b> 66 </li> 67 ", 68 item.Link, 69 item.Name, 70 string.IsNullOrEmpty(item.Icon) ? StageModel.MoIcon.Default : item.Icon, 71 nowName, 72 item.Des); 73 } 74 75 } 76 sbHtml.Append("</ul>"); 77 78 return sbHtml.ToString(); 79 }
参数说明:
第一个List<StageModel.MoMenu> menus:这里传递的是登陆用户具有的菜单权限集合
第二个List<StageModel.MoMenu> listMenu:本次循环的菜单集合(第一次进入方法的时候,这里传递的是系统所有菜单的集合数据)
string parentName:父级菜单的名称,多个层级关系名称使用‘|’拼接,主要用处是在页面点击节点的时候可以直接获取此节点的层级节点名称,方便展示
string ulClass = "nav nav-list":css样式控制参数(这里使用的是Ace样式)
方法体里面的代码,每个关键点和思路都有备注,大家可以查看下
最后,咋们来看一下效果截图:
. Global中增加全局Application_Error监控404异常
首先,这里Application_Error监控对于mvc就好增加了,直接在Global里面增加代码如:
/// <summary> /// 捕获不到达Action的错误 /// </summary> protected void Application_Error() { var lastError = Server.GetLastError(); if (lastError != null) { var httpError = lastError as HttpException; if (httpError != null) { switch (httpError.GetHttpCode()) { case 404: Response.Redirect("/Error"); break; } } } }
这样就能获取出用户访问不存在路由时候提示的404code错误,然后跳转到自定义路由Error中去,我们这里测试访问一个我这里存在的地址:http://localhost:5050/home 和不存在的地址:http://localhost:5050/home1,后者会自动跳转到我定义的Error路由对应的试图中去,效果大家可以自行体验;
上面是全局的404错误,那么Action出错怎么获取信息呢,我们可以重写HandleErrorAttribute中的void OnException(ExceptionContext filterContext)方法,这里直接给出具体代码:
/// <summary> /// 捕获Action错误 /// </summary> public class ExceptionPageAttribute : HandleErrorAttribute { public override void OnException(ExceptionContext filterContext) { var code = 505; var lastError = filterContext.Exception; if (lastError != null) { var httpError = lastError as HttpException; if (httpError != null) { code = httpError.GetHttpCode(); } } filterContext.Result = new RedirectResult("/Error?code=" + code); } }
自定义完后,我们需要在项目根目录下的App_Start/FilterConfig.cs文件中的void RegisterGlobalFilters(GlobalFilterCollection filters)方法中添加我们的自定义错误拦截器,这样如果Action或者Controller提示异常的时候会自动进入自定义ExceptionPage拦截器方法中,跳转到我们的Error路由视图中去;
. 实现及使用缓存工厂(最新缓存工厂代码在上一篇分享的缓存工厂之Redis缓存)
在做后台系统的时候用到了前面封装的缓存工厂,这里主要是拿过来修改了部分信息,最新代码已经更新到上篇缓存工厂之Redis缓存文章中,如果有需要的朋友可以点击链接;
本次的分享就到这里了,不知不觉凌晨了,该说睡觉的时候了,谢谢各位观赏,欢迎多多点赞。