BrnShop开源网上商城第二讲:ASP.NET MVC框架
在团队设计BrnShop的web项目之初,我们碰到了两个问题,第一个是数据的复用和传递,第二个是大mvc框架和小mvc框架的选择。下面我依次来说明下。
首先是数据的复用和传递:对于BrnShop的每一次请求,程序都要分成好几个阶段执行,例如验证,执行动作方法等等,在各个阶段我们可能需要重复使用同一信息,而我们的愿景就是希望此信息只需获取一次,然后沿着流程管道一直流动,这样在后面的阶段中就可以直接使用,不用再重新获取了,提高程序的性能。举例来说:在授权验证阶段,我们为对用户进行验证,从而获取了用户信息,当验证结束后,此用户信息并不被抛弃,而是保留下来,这样在后面的动作方法中我们就不需要再次获取用户信息,而是直接使用刚才在授权中保留下来的用户信息就可以了。
具体实现是这样的:首先我们给这些需要公用的数据定义个上下文类,它们分别是BrnShop.Web.Framework项目中的WebWorkContext类和AdminWorkContext类,其中WebWorkContext是前台项目使用的上下文,AdminWorkContext是后台项目使用的上下文。代码很简单,就是定义了一些公共字段,具体如下:
using System; using System.Collections.Generic; using BrnShop.Core; namespace BrnShop.Web.Framework { /// <summary> /// 商城前台工作上下文类 /// </summary> public class WebWorkContext { public ShopConfigInfo ShopConfig = BSPConfig.ShopConfig;//商城配置信息 public bool IsHttpAjax;//当前请求是否为ajax请求 public string IP;//用户ip public RegionInfo Region;//区域信息 public string Url;//当前url public string UrlReferrer;//上一次访问的url public string Sid;//用户sid public int Uid = -1;//用户id public string UserName;//用户名 public string UserEmail;//用户邮箱 public string UserMobile;//用户手机号 public string NickName;//用户昵称 public string Avatar;//用户头像 public string Password;//用户密码 public string PayCreditName;//支付积分名称 public int PayCreditCount = 0;//支付积分数量 public string RankCreditName;//等级积分名称 public int RankCreditCount = 0;//等级积分数量 public PartUserInfo PartUserInfo;//用户信息 public int UserRid = -1;//用户等级id public UserRankInfo UserRank;//用户等级信息 public string UserRTitle;//用户等级标题 public int AdminGid = -1;//用户管理员组id public AdminGroupInfo AdminGroup;//用户管理员组信息 public string AdminGTitle;//管理员组标题 public string Controller;//控制器 public string Action;//动作方法 public string PageKey;//页面标示符 public string ThemeName;//当前主题名称 public string ImageDir;//图片目录 public string CSSDir;//css目录 public string ScriptDir;//脚本目录 public int OnlineUserCount = 0;//在线总人数 public int OnlineMemberCount = 0;//在线会员数 public int OnlineGuestCount = 0;//在线游客数 public string SearchWord;//搜索词 public int SCProductCount = 0;//购物车中商品数量 public List<CategoryInfo> CategoryList;//分类列表 public List<NavInfo> NavList;//导航列表 public FriendLinkInfo[] FriendLinkList;//友情链接列表 public List<HelpInfo> HelpList;//帮助列表 public DateTime StartExecuteTime;//页面开始执行时间 public double ExecuteTime;//页面执行时间 public int ExecuteCount = 0;//执行的sql语句数目 public string ExecuteDetail;//执行的sql语句细节 public string ShopVersion = BSPVersion.SHOP_VERSION;//商城版本 public string ShopCopyright = BSPVersion.SHOP_COPYRIGHT;//商城版权 } }
using System; using BrnShop.Core; namespace BrnShop.Web.Framework { /// <summary> /// 商城后台工作上下文类 /// </summary> public class AdminWorkContext { public ShopConfigInfo ShopConfig = BSPConfig.ShopConfig;//商城配置信息 public bool IsHttpAjax;//当前请求是否为ajax请求 public string IP;//用户ip public RegionInfo Region;//区域信息 public string Url;//当前url public string UrlReferrer;//上一次访问的url public string Sid;//用户sid public int Uid = -1;//用户id public string UserName;//用户名 public string UserEmail;//用户邮箱 public string UserMobile;//用户手机号 public string NickName;//用户昵称 public string Avatar;//用户头像 public string Password;//用户密码 public PartUserInfo PartUserInfo;//用户信息 public int UserRid = -1;//用户等级id public UserRankInfo UserRank;//用户等级信息 public string UserRTitle;//用户等级标题 public int AdminGid = -1;//用户管理员组id public AdminGroupInfo AdminGroup;//用户管理员组信息 public string AdminGTitle;//管理员组标题 public string Controller;//控制器 public string Action;//动作方法 public string PageKey;//页面标示符 } }
有了上下文类后,我们需要找一个可以保证上下文流动的地方。在翻看了asp.net mvc的源码后,我们找到一个好地方,这个地方就在控制器的基类Controller中。在Controller中微软定义了六个方法,具体如下:
- protected override void Initialize(RequestContext requestContext);说明:初始化调用构造函数后可能不可用的数据。
- protected virtual void OnAuthorization(AuthorizationContext filterContext);说明:在进行授权时调用。
- protected virtual void OnActionExecuted(ActionExecutedContext filterContext);说明:在调用操作方法后调用。
- protected virtual void OnActionExecuting(ActionExecutingContext filterContext);说明:在调用操作方法前调用。
- protected virtual void OnResultExecuted(ResultExecutedContext filterContext);说明:在执行由操作方法返回的操作结果后调用。
- protected virtual void OnResultExecuting(ResultExecutingContext filterContext);说明:在执行由操作方法返回的操作结果前调用。
这些都是虚方法,所以我们可以定义一个继承自Controller的新控制器,然后重写这些方法。由于这些方法是在同一个类中,所以它们可以共享同一个字段(这个字段就是上下文),而且其他的控制器都是继承自这个新控制器类,所以在动作方法中也是可以访问这个共享字段(父类的字段)。新控制器类分别是BrnShop.Web.Framework项目中BaseWebController类和BaseAdminController类,其中BaseWebController为前台控制器类,BaseAdminController为后台控制器类,具体实现如下:
using System; using System.Text; using System.Web.Mvc; using System.Web.Routing; using System.Collections.Generic; using BrnShop.Core; using BrnShop.Services; namespace BrnShop.Web.Framework { /// <summary> /// 商城前台基础控制器类 /// </summary> public class BaseWebController : Controller { //工作上下午 public WebWorkContext WorkContext = new WebWorkContext(); protected override void Initialize(RequestContext requestContext) { base.Initialize(requestContext); WorkContext.IsHttpAjax = WebHelper.IsAjax(); WorkContext.IP = WebHelper.GetIP(); WorkContext.Region = Regions.GetRegionByIP(WorkContext.IP); WorkContext.Url = WebHelper.GetUrl(); WorkContext.UrlReferrer = WebHelper.GetUrlReferrer(); //获得用户唯一标示符sid WorkContext.Sid = ShopUtils.GetSidCookie(); if (WorkContext.Sid.Length == 0) { //生成sid WorkContext.Sid = Sessions.GenerateSid(); //将sid保存到cookie中 ShopUtils.SetSidCookie(WorkContext.Sid); } PartUserInfo partUserInfo; //获得用户id int uid = ShopUtils.GetUidCookie(); if (uid < 1)//当用户为游客时 { //创建游客 partUserInfo = Users.CreatePartGuest(); } else//当用户为会员时 { //获得保存在cookie中的密码 string password = ShopUtils.GetPasswordCookie(); //防止用户密码被篡改为危险字符 if (password.Length == 0 || !SecureHelper.IsBase64String(password)) { //创建游客 partUserInfo = Users.CreatePartGuest(); ShopUtils.SetUidCookie(-1); ShopUtils.SetPasswordCookie(""); } else { partUserInfo = Users.GetPartUserByUidAndPwd(uid, password); if (partUserInfo != null) { //发放登陆积分 Credits.SendLoginCredits(ref partUserInfo, DateTime.Now); } else//当会员的账号或密码不正确时,将用户置为游客 { partUserInfo = Users.CreatePartGuest(); ShopUtils.SetUidCookie(-1); ShopUtils.SetPasswordCookie(""); } } } //设置用户等级 if (UserRanks.IsBanUserRank(partUserInfo.UserRid) && partUserInfo.LiftBanTime <= DateTime.Now) { UserRankInfo userRankInfo = UserRanks.GetUserRankByCredits(partUserInfo.PayCredits); Users.UpdateUserRankByUid(partUserInfo.Uid, userRankInfo.UserRid); partUserInfo.UserRid = userRankInfo.UserRid; } WorkContext.PartUserInfo = partUserInfo; WorkContext.Uid = partUserInfo.Uid; WorkContext.UserName = partUserInfo.UserName; WorkContext.UserEmail = partUserInfo.Email; WorkContext.UserMobile = partUserInfo.Mobile; WorkContext.Password = partUserInfo.Password; WorkContext.NickName = partUserInfo.NickName; WorkContext.Avatar = partUserInfo.Avatar; WorkContext.PayCreditName = Credits.PayCreditName; WorkContext.PayCreditCount = partUserInfo.PayCredits; WorkContext.RankCreditName = Credits.RankCreditName; WorkContext.RankCreditCount = partUserInfo.RankCredits; WorkContext.UserRid = partUserInfo.UserRid; WorkContext.UserRank = UserRanks.GetUserRankById(partUserInfo.UserRid); WorkContext.UserRTitle = WorkContext.UserRank.Title; //设置用户管理员组 WorkContext.AdminGid = partUserInfo.AdminGid; WorkContext.AdminGroup = AdminGroups.GetAdminGroupById(partUserInfo.AdminGid); WorkContext.AdminGTitle = WorkContext.AdminGroup.Title; //设置当前控制器类名 WorkContext.Controller = RouteData.Values["controller"].ToString().ToLower(); //设置当前动作方法名 WorkContext.Action = RouteData.Values["action"].ToString().ToLower(); WorkContext.PageKey = string.Format("/{0}/{1}", WorkContext.Controller, WorkContext.Action); //当前商城主题名称 WorkContext.ThemeName = WorkContext.ShopConfig.ThemeName; //设置图片目录 WorkContext.ImageDir = string.Format("{0}/Themes/{1}/Images", WorkContext.ShopConfig.ImageCDN, WorkContext.ThemeName); //设置css目录 WorkContext.CSSDir = string.Format("{0}/Themes/{1}/CSS", WorkContext.ShopConfig.CSSCDN, WorkContext.ThemeName); //设置脚本目录 WorkContext.ScriptDir = string.Format("{0}/Scripts", WorkContext.ShopConfig.ScriptCDN); //在线总人数 WorkContext.OnlineUserCount = OnlineUsers.GetOnlineUserCount(); //在线游客数 WorkContext.OnlineGuestCount = OnlineUsers.GetOnlineGuestCount(); //在线会员数 WorkContext.OnlineMemberCount = WorkContext.OnlineUserCount - WorkContext.OnlineGuestCount; //搜索词 WorkContext.SearchWord = string.Empty; //购物车中商品数量 WorkContext.SCProductCount = Orders.GetShopCartProductCountCookie(); //分类列表 WorkContext.CategoryList = Categories.GetCategoryList(); //设置导航列表 WorkContext.NavList = Navs.GetNavList(); //设置友情链接列表 WorkContext.FriendLinkList = FriendLinks.GetFriendLinkList(); //设置帮助列表 WorkContext.HelpList = Helps.GetHelpList(); } protected override void OnAuthorization(AuthorizationContext filterContext) { //不能应用在子方法上 if (filterContext.IsChildAction) return; //商城已经关闭 if (WorkContext.ShopConfig.IsClosed == 1 && WorkContext.AdminGid == 1 && WorkContext.PageKey != "/account/login" && WorkContext.PageKey != "/account/logout") { filterContext.Result = PromptView(WorkContext.ShopConfig.CloseReason); return; } //当前时间为禁止访问时间 if (ValidateHelper.BetweenPeriod(WorkContext.ShopConfig.BanAccessTime) && WorkContext.AdminGid == 1 && WorkContext.PageKey != "/account/login" && WorkContext.PageKey != "/account/logout") { filterContext.Result = PromptView("当前时间不能访问本商城"); return; } //当用户ip在被禁止的ip列表时 if (ValidateHelper.InIPList(WorkContext.IP, WorkContext.ShopConfig.BanAccessIP)) { filterContext.Result = PromptView("您的IP被禁止访问本商城"); return; } //当用户ip不在允许的ip列表时 if (!string.IsNullOrEmpty(WorkContext.ShopConfig.AllowAccessIP) && !ValidateHelper.InIPList(WorkContext.IP, WorkContext.ShopConfig.AllowAccessIP)) { filterContext.Result = PromptView("您的IP被禁止访问本商城"); return; } //当用户IP被禁止时 if (BannedIPs.CheckIP(WorkContext.IP)) { filterContext.Result = PromptView("您的IP被禁止访问本商城"); return; } //当用户等级是禁止访问等级时 if (WorkContext.UserRid == 1) { filterContext.Result = PromptView("您的账号当前被锁定,不能访问"); return; } //判断目前访问人数是否达到允许的最大人数 if (WorkContext.OnlineUserCount > WorkContext.ShopConfig.MaxOnlineCount && WorkContext.AdminGid == 1 && (WorkContext.Controller != "account" && (WorkContext.Action != "login" || WorkContext.Action != "logout"))) { filterContext.Result = PromptView("商城人数达到访问上限, 请稍等一会再访问!"); return; } } protected override void OnActionExecuting(ActionExecutingContext filterContext) { //不能应用在子方法上 if (filterContext.IsChildAction) return; #if DEBUG //清空执行的sql语句数目 RDBSHelper.ExecuteCount = 0; //清空执行的sql语句细节 RDBSHelper.ExecuteDetail = ""; #endif //页面开始执行时间 WorkContext.StartExecuteTime = DateTime.Now; //当用户为会员时,更新用户的在线时间 if (WorkContext.Uid > 0) Users.UpdateUserOnlineTime(WorkContext.Uid); //更新在线用户 Asyn.UpdateOnlineUser(WorkContext.Uid, WorkContext.Sid, WorkContext.IP, WorkContext.Region.RegionId); //更新PV统计 if (WorkContext.ShopConfig.UpdatePVStatTimespan != 0) Asyn.UpdatePVStat(WorkContext.Uid, WorkContext.Region.RegionId, WebHelper.GetBrowserType(), WebHelper.GetOSType()); } protected override void OnActionExecuted(ActionExecutedContext filterContext) { //不能应用在子方法上 if (filterContext.IsChildAction) return; #if DEBUG //执行的sql语句数目 WorkContext.ExecuteCount = RDBSHelper.ExecuteCount; //执行的sql语句细节 if (RDBSHelper.ExecuteDetail == string.Empty) WorkContext.ExecuteDetail = "当前页面没有和数据库的任何交互"; else WorkContext.ExecuteDetail = "<div>数据查询分析:</div>" + RDBSHelper.ExecuteDetail; #endif //页面执行时间 WorkContext.ExecuteTime = DateTime.Now.Subtract(WorkContext.StartExecuteTime).TotalMilliseconds / 1000; } protected override void OnException(ExceptionContext filterContext) { ShopUtils.WriteLogFile(filterContext.Exception); if (WorkContext.IsHttpAjax) filterContext.Result = new ContentResult { Content = "error" }; else filterContext.Result = new ViewResult() { ViewName = "Error" }; } /// <summary> /// 获得路由中的值 /// </summary> /// <param name="key">键</param> /// <param name="defaultValue">默认值</param> /// <returns></returns> protected string GetRouteString(string key, string defaultValue) { object value = RouteData.Values[key]; if (value != null) return value.ToString(); else return defaultValue; } /// <summary> /// 获得路由中的值 /// </summary> /// <param name="key">键</param> /// <returns></returns> protected string GetRouteString(string key) { return GetRouteString(key, ""); } /// <summary> /// 获得路由中的值 /// </summary> /// <param name="key">键</param> /// <param name="defaultValue">默认值</param> /// <returns></returns> protected int GetRouteInt(string key, int defaultValue) { return TypeHelper.ObjectToInt(RouteData.Values[key], defaultValue); } /// <summary> /// 获得路由中的值 /// </summary> /// <param name="key">键</param> /// <returns></returns> protected int GetRouteInt(string key) { return GetRouteInt(key, 0); } /// <summary> /// 提示信息视图 /// </summary> /// <param name="message">提示信息</param> /// <returns></returns> protected ViewResult PromptView(string message) { return View("Prompt", new PromptModel(message)); } /// <summary> /// 提示信息视图 /// </summary> /// <param name="backUrl">返回地址</param> /// <param name="message">提示信息</param> /// <returns></returns> protected ViewResult PromptView(string backUrl, string message) { return View("Prompt", new PromptModel(backUrl, message)); } /// <summary> /// 获得验证错误列表 /// </summary> /// <returns></returns> protected string GetVerifyErrorList() { if (ModelState.Count == 0) return "null"; StringBuilder errorList = new StringBuilder("["); foreach (KeyValuePair<string, ModelState> item in ModelState) { errorList.AppendFormat("{0}'key':'{1}','msg':'{2}'{3},", "{", item.Key, item.Value.Errors[0].ErrorMessage, "}"); } errorList.Remove(errorList.Length - 1, 1); errorList.Append("]"); return errorList.ToString(); } } }
using System; using System.Web; using System.Web.Mvc; using System.Web.Routing; using BrnShop.Core; using BrnShop.Services; namespace BrnShop.Web.Framework { /// <summary> /// 商城后台基础控制器类 /// </summary> public class BaseAdminController : Controller { //工作上下午 public AdminWorkContext WorkContext = new AdminWorkContext(); protected override void Initialize(RequestContext requestContext) { base.Initialize(requestContext); WorkContext.IsHttpAjax = WebHelper.IsAjax(); WorkContext.IP = WebHelper.GetIP(); WorkContext.Region = Regions.GetRegionByIP(WorkContext.IP); WorkContext.Url = WebHelper.GetUrl(); WorkContext.UrlReferrer = WebHelper.GetUrlReferrer(); //获得用户唯一标示符sid WorkContext.Sid = ShopUtils.GetSidCookie(); if (WorkContext.Sid.Length == 0) { //生成sid WorkContext.Sid = Sessions.GenerateSid(); //将sid保存到cookie中 ShopUtils.SetSidCookie(WorkContext.Sid); } PartUserInfo partUserInfo; //获得用户id int uid = ShopUtils.GetUidCookie(); if (uid < 1)//当用户为游客时 { //创建游客 partUserInfo = Users.CreatePartGuest(); } else//当用户为会员时 { //获得保存在cookie中的密码 string password = ShopUtils.GetPasswordCookie(); //防止用户密码被篡改为危险字符 if (password.Length == 0 || !SecureHelper.IsBase64String(password)) { //创建游客 partUserInfo = Users.CreatePartGuest(); ShopUtils.SetUidCookie(-1); ShopUtils.SetPasswordCookie(""); } else { partUserInfo = Users.GetPartUserByUidAndPwd(uid, password); if (partUserInfo != null) { //发放登陆积分 Credits.SendLoginCredits(ref partUserInfo, DateTime.Now); } else//当会员的账号或密码不正确时,将用户置为游客 { partUserInfo = Users.CreatePartGuest(); ShopUtils.SetUidCookie(-1); ShopUtils.SetPasswordCookie(""); } } } //设置用户等级 if (UserRanks.IsBanUserRank(partUserInfo.UserRid) && partUserInfo.LiftBanTime <= DateTime.Now) { UserRankInfo userRankInfo = UserRanks.GetUserRankByCredits(partUserInfo.PayCredits); Users.UpdateUserRankByUid(partUserInfo.Uid, userRankInfo.UserRid); partUserInfo.UserRid = userRankInfo.UserRid; } WorkContext.PartUserInfo = partUserInfo; WorkContext.Uid = partUserInfo.Uid; WorkContext.UserName = partUserInfo.UserName; WorkContext.UserEmail = partUserInfo.Email; WorkContext.UserMobile = partUserInfo.Mobile; WorkContext.Password = partUserInfo.Password; WorkContext.NickName = partUserInfo.NickName; WorkContext.Avatar = partUserInfo.Avatar; WorkContext.UserRid = partUserInfo.UserRid; WorkContext.UserRank = UserRanks.GetUserRankById(partUserInfo.UserRid); WorkContext.UserRTitle = WorkContext.UserRank.Title; //设置用户管理员组 WorkContext.AdminGid = partUserInfo.AdminGid; WorkContext.AdminGroup = AdminGroups.GetAdminGroupById(partUserInfo.AdminGid); WorkContext.AdminGTitle = WorkContext.AdminGroup.Title; //设置当前控制器类名 WorkContext.Controller = RouteData.Values["controller"].ToString().ToLower(); //设置当前动作方法名 WorkContext.Action = RouteData.Values["action"].ToString().ToLower(); WorkContext.PageKey = string.Format("/{0}/{1}", WorkContext.Controller, WorkContext.Action); } protected override void OnAuthorization(AuthorizationContext filterContext) { //不能应用在子方法上 if (filterContext.IsChildAction) return; //当用户ip不在允许的后台访问ip列表时 if (!string.IsNullOrEmpty(WorkContext.ShopConfig.AdminAllowAccessIP) && !ValidateHelper.InIPList(WorkContext.IP, WorkContext.ShopConfig.AdminAllowAccessIP)) { if (WorkContext.IsHttpAjax) filterContext.Result = new ContentResult { Content = "404" }; else filterContext.Result = new RedirectResult("/"); return; } //当用户IP被禁止时 if (BannedIPs.CheckIP(WorkContext.IP)) { if (WorkContext.IsHttpAjax) filterContext.Result = new ContentResult { Content = "404" }; else filterContext.Result = new RedirectResult("/"); return; } //当用户等级是禁止访问等级时 if (WorkContext.UserRid == 1) { if (WorkContext.IsHttpAjax) filterContext.Result = new ContentResult { Content = "404" }; else filterContext.Result = new RedirectResult("/"); return; } //如果当前用户没有登录 if (WorkContext.Uid < 1) { if (WorkContext.IsHttpAjax) filterContext.Result = new ContentResult { Content = "404" }; else filterContext.Result = new RedirectResult("/"); return; } //如果当前用户不是管理员 if (WorkContext.AdminGid == 1) { if (WorkContext.IsHttpAjax) filterContext.Result = new ContentResult { Content = "404" }; else filterContext.Result = new RedirectResult("/"); return; } //判断当前用户是否有访问当前页面的权限 if (WorkContext.Controller != "home" && !AdminGroups.CheckAuthority(WorkContext.AdminGid, WorkContext.Controller, WorkContext.PageKey)) { if (WorkContext.IsHttpAjax) filterContext.Result = new ContentResult { Content = "notpermit" }; else filterContext.Result = PromptView("你没有当前操作的权限!"); return; } } protected override void OnActionExecuting(ActionExecutingContext filterContext) { //不能应用在子方法上 if (filterContext.IsChildAction) return; //当用户为会员时,更新用户的在线时间 if (WorkContext.Uid > 0) Users.UpdateUserOnlineTime(WorkContext.Uid); //更新在线用户 Asyn.UpdateOnlineUser(WorkContext.Uid, WorkContext.Sid, WorkContext.IP, WorkContext.Region.RegionId); //更新PV统计 if (WorkContext.ShopConfig.UpdatePVStatTimespan != 0) Asyn.UpdatePVStat(WorkContext.Uid, WorkContext.Region.RegionId, WebHelper.GetBrowserType(), WebHelper.GetOSType()); } protected override void OnException(ExceptionContext filterContext) { ShopUtils.WriteLogFile(filterContext.Exception); if (WorkContext.IsHttpAjax) filterContext.Result = new ContentResult { Content = "error" }; else filterContext.Result = new ViewResult() { ViewName = "Error" }; } /// <summary> /// 提示信息视图 /// </summary> /// <param name="message">提示信息</param> /// <returns></returns> protected ViewResult PromptView(string message) { return View("Prompt", new PromptModel(ShopUtils.GetAdminRefererCookie(), message)); } /// <summary> /// 提示信息视图 /// </summary> /// <param name="backUrl">返回地址</param> /// <param name="message">提示信息</param> /// <returns></returns> protected ViewResult PromptView(string backUrl, string message) { return View("Prompt", new PromptModel(backUrl, message)); } /// <summary> /// 提示信息视图 /// </summary> /// <param name="backUrl">返回地址</param> /// <param name="message">提示信息</param> /// <param name="isAutoBack">是否自动返回</param> /// <returns></returns> protected ViewResult PromptView(string backUrl, string message, bool isAutoBack) { return View("Prompt", new PromptModel(backUrl, message) { IsAutoBack = isAutoBack }); } /// <summary> /// 添加后台操作日志 /// </summary> /// <param name="operation">操作行为</param> protected void AddAdminOperateLog(string operation) { AddAdminOperateLog(operation, ""); } /// <summary> /// 添加后台操作日志 /// </summary> /// <param name="operation">操作行为</param> /// <param name="description">操作描述</param> protected void AddAdminOperateLog(string operation, string description) { AdminOperateLogs.CreateAdminOperateLog(WorkContext.Uid, WorkContext.UserName, WorkContext.AdminGid, WorkContext.AdminGTitle, WorkContext.IP, operation, description); } } }
到此事情还没完,那就是这个上下文是控制器的字段,在视图中如果想访问它需要强制类型转换下,代码为:((BaseWebController)(this.ViewContext.Controller)).WorkContext;试想一下我们每次访问上下文都需要这么长的一段代码那是怎样的煎熬呀?不过幸好有解决办法,那就是重写mvc的WebViewPage页(如果你不知道WebViewPage和mvc的编译过程请阅读大神“Artech”的相关文章,地址如下:http://www.cnblogs.com/artech/)。具体代码在BrnShop.Web.Framework项目中WebViewPage类和AdminViewPage类,其中WebViewPage为前台视图类,AdminViewPage为后台视图类:
using System; using System.Text; using System.Web.Mvc; using System.Collections.Generic; namespace BrnShop.Web.Framework { /// <summary> /// 前台视图页面基类型 /// </summary> public abstract class WebViewPage<TModel> : System.Web.Mvc.WebViewPage<TModel> { public WebWorkContext WorkContext; public override void InitHelpers() { base.InitHelpers(); WorkContext = ((BaseWebController)(this.ViewContext.Controller)).WorkContext; } /// <summary> /// 获得验证错误列表 /// </summary> /// <returns></returns> public MvcHtmlString GetVerifyErrorList() { ModelStateDictionary modelState = ((Controller)(this.ViewContext.Controller)).ModelState; if (modelState == null || modelState.Count == 0) return new MvcHtmlString("null"); StringBuilder errorList = new StringBuilder("["); foreach (KeyValuePair<string, ModelState> item in modelState) { errorList.AppendFormat("{0}'key':'{1}','msg':'{2}'{3},", "{", item.Key, item.Value.Errors[0].ErrorMessage, "}"); } errorList.Remove(errorList.Length - 1, 1); errorList.Append("]"); return new MvcHtmlString(errorList.ToString()); } } /// <summary> /// 前台视图页面基类型 /// </summary> public abstract class WebViewPage : WebViewPage<dynamic> { } }
using System; namespace BrnShop.Web.Framework { /// <summary> /// 后台视图页面基类型 /// </summary> public abstract class AdminViewPage<TModel> : System.Web.Mvc.WebViewPage<TModel> { public AdminWorkContext WorkContext; public override void InitHelpers() { base.InitHelpers(); Html.EnableClientValidation(true);//启用客户端验证 Html.EnableUnobtrusiveJavaScript(true);//启用非侵入式脚本 WorkContext = ((BaseAdminController)(this.ViewContext.Controller)).WorkContext; } } /// <summary> /// 后台视图页面基类型 /// </summary> public abstract class AdminViewPage : AdminViewPage<dynamic> { } }
定义好新的视图类后,我们需要通知编译器使用这个新类,通知方式在视图文件的web.config中,具体见下图:
通过将"pageBaseType"的值设置为我们的新类名,我们就可以在视图文件中直接使用上下文了。例:@WorkContext.ShopConfig.SEOKeyword
说完了数据的复用和传递,我们再来说说大mvc框架和小mvc框架的问题。首先何为大mvc框架,何为小mvc框架?
- 大mvc框架指的是尽量完整的一套asp.net mvc框架,包含路由,控制器,模型绑定,模型校验,筛选器等等。
- 小mvc框架指的是只包含项目所必须使用的mvc部分,对于使用不到的部分尽量不用或移除。
大家可能觉得这有什么难的?但是对于一个开源项目来说这确实是一个很重要的问题,因为开源项目的产品面向的是全国甚至是全世界的开发者,大家的技术参差不齐,有的高,有个低。为了保证尽可能多的覆盖开发者,只有原汁原味的mvc才对开发者更亲切和熟悉,所以应该使用大mvc框架。可是一款优秀的产品不只是面向初级开发者,还需要面对高级开发者,对于高级开发者来说他们希望获得项目最大的可控权,所以框架应该尽量只使用最核心的mvc部分,这样留给开发者的空间才能更大,这样这样看来又应该使用小mvc框架。下面我从两个方面来说明我们是如何解决这个问题的。
首先是mvc筛选器:看过我们源码的园友已经发现,我们项目中没有定义任何一个筛选器类。那我们的筛选器在哪儿?答案就在上面的上下文流动中,在上面重写的筛选器方法中我们实现所有筛选。如果你想针对某个控制器A单独筛选你可以在A中再一次重写筛选器方法添加自己的代码。如果你想只针对某一方法进行筛选你只需要单独在方法中筛选就可以了。这样通过使用内置在controller中的筛选方法我们实现了和第三方筛选器的隔离,也减少了反射获取筛选器的次数。
其次是模型绑定和校验:我们首先通过手动获取request集合的方式去除所有模型绑定,以登陆代码为例:
/// <summary> /// 登录 /// </summary> public ActionResult Login()//注意此方面没有任何参数 { string returnUrl = WebHelper.GetQueryString("returnUrl"); if (returnUrl.Length == 0) returnUrl = "/"; if (WorkContext.ShopConfig.LoginType == "") return PromptView(returnUrl, "商城目前已经关闭登陆功能!"); if (WorkContext.Uid > 0) return PromptView(returnUrl, "您已经登录,无须重复登录!"); if (WorkContext.ShopConfig.LoginFailTimes != 0 && LoginFailLogs.GetLoginFailTimesByIp(WorkContext.IP) >= WorkContext.ShopConfig.LoginFailTimes) return PromptView(returnUrl, "您已经输入错误" + WorkContext.ShopConfig.LoginFailTimes + "次密码,请15分钟后再登陆!"); //get请求 if (WebHelper.IsGet()) { ViewData.Add("oAuthPluginList", Plugins.GetOAuthPluginList()); return View(new LoginModel()); } //post请求 LoginModel model = new LoginModel(); //模型绑定 手动绑定 model.AccountName = WebHelper.GetFormString(WorkContext.ShopConfig.ShadowName).Trim(); model.Password = WebHelper.GetFormString("password"); model.IsRemember = WebHelper.GetFormInt("isRemember"); model.VerifyCode = WebHelper.GetFormString("verifyCode"); //模型验证 PartUserInfo partUserInfo = VerifyLogin(model); if (!ModelState.IsValid)//验证失败时 { ViewData.Add("oAuthPluginList", Plugins.GetOAuthPluginList()); return View(model); } else//验证成功时 { //当用户等级是禁止访问等级时 if (partUserInfo.UserRid == 1) return PromptView("您的账号当前被锁定,不能访问"); //删除登陆失败日志 LoginFailLogs.DeleteLoginFailLogByIP(WorkContext.IP); //更新用户最后访问 int regionId = WorkContext.Region != null ? WorkContext.Region.RegionId : -1; Users.UpdateUserLastVisit(partUserInfo.Uid, WorkContext.IP, regionId, DateTime.Now); //更新购物车中用户id Orders.UpdateShopCartUidBySid(partUserInfo.Uid, WorkContext.Sid); //将用户信息写入cookie中 ShopUtils.SetUserCookie(partUserInfo, (WorkContext.ShopConfig.IsRemember == 1 && model.IsRemember == 1) ? 30 : -1); return Redirect(returnUrl); } }
其次是模型校验,校验又分为两部分。第一部分是验证,对此我们也是采用手动校验的方式,同样以登陆为例:
/// <summary> /// 登录验证 /// </summary> private PartUserInfo VerifyLogin(LoginModel model) { PartUserInfo partUserInfo = null; //验证账户名 if (string.IsNullOrWhiteSpace(model.AccountName)) { ModelState.AddModelError(WorkContext.ShopConfig.ShadowName, "账户名不能为空"); } else if (model.AccountName.Length < 4 || model.AccountName.Length > 50) { ModelState.AddModelError(WorkContext.ShopConfig.ShadowName, "账户名必须大于3且不大于50个字符"); } else if ((!SecureHelper.IsSafeSqlString(model.AccountName))) { ModelState.AddModelError(WorkContext.ShopConfig.ShadowName, "账户名不存在"); } //验证密码 if (string.IsNullOrWhiteSpace(model.Password)) { ModelState.AddModelError("password", "密码不能为空"); } else if (model.Password.Length < 4 || model.Password.Length > 32) { ModelState.AddModelError("password", "密码必须大于3且不大于32个字符"); } //验证验证码 if (CommonHelper.IsInArray(WorkContext.PageKey, WorkContext.ShopConfig.VerifyPages)) { if (string.IsNullOrWhiteSpace(model.VerifyCode)) { ModelState.AddModelError("verifyCode", "验证码不能为空"); } else if (model.VerifyCode.ToLower() != Sessions.GetValueString(WorkContext.Sid, "verifyCode")) { ModelState.AddModelError("verifyCode", "验证码不正确"); } } //当以上验证全部通过时 if (ModelState.IsValid) { if (BSPConfig.ShopConfig.LoginType.Contains("2") && ValidateHelper.IsEmail(model.AccountName))//邮箱登陆 { partUserInfo = Users.GetPartUserByEmail(model.AccountName); if (partUserInfo == null) ModelState.AddModelError(WorkContext.ShopConfig.ShadowName, "邮箱不存在"); } else if (BSPConfig.ShopConfig.LoginType.Contains("3") && ValidateHelper.IsMobile(model.AccountName))//手机登陆 { partUserInfo = Users.GetPartUserByMobile(model.AccountName); if (partUserInfo == null) ModelState.AddModelError(WorkContext.ShopConfig.ShadowName, "手机不存在"); } else if (BSPConfig.ShopConfig.LoginType.Contains("1"))//用户名登陆 { partUserInfo = Users.GetPartUserByName(model.AccountName); if (partUserInfo == null) ModelState.AddModelError(WorkContext.ShopConfig.ShadowName, "用户名不存在"); } //判断密码是否正确 if (partUserInfo != null && Users.CreateUserPassword(model.Password, partUserInfo.Salt) != partUserInfo.Password) { LoginFailLogs.AddLoginFailTimes(WorkContext.IP, DateTime.Now);//增加登陆失败次数 ModelState.AddModelError("password", "密码不正确"); } } return partUserInfo; }
通过上面代码大家可以看出所有的验证都是手动进行的。
校验的第二部分是验证信息显示,在mvc中大家经常使用Html.ValidationMessageFor之类的方法来显示验证信息,所以为了保证上述方法还能够正常使用,我们需要将所有验证信息都添加到ModelState中(因为Html.ValidationMessageFor之类的方法实现本质就是通过获取ModelState指定键值的内容来判断是否显示和显示什么内容)。到此我们已经有了校验数据,剩下的就是在视图中显示了。关于显示我们仍然可以使用Html.ValidationMessageFor之类的方法;如果你想获得更大的灵活性你可以使用视图页面的“GetVerifyErrorList”方法,此方法在我们新定义的视图基类中,它的功能就是将校验信息构建成一个json对象。代码如下:
/// <summary> /// 获得验证错误列表 /// </summary> /// <returns></returns> public MvcHtmlString GetVerifyErrorList() { ModelStateDictionary modelState = ((Controller)(this.ViewContext.Controller)).ModelState; if (modelState == null || modelState.Count == 0) return new MvcHtmlString("null"); StringBuilder errorList = new StringBuilder("["); foreach (KeyValuePair<string, ModelState> item in modelState) { errorList.AppendFormat("{0}'key':'{1}','msg':'{2}'{3},", "{", item.Key, item.Value.Errors[0].ErrorMessage, "}"); } errorList.Remove(errorList.Length - 1, 1); errorList.Append("]"); return new MvcHtmlString(errorList.ToString()); }
下面给出一个使用例子,代码是登陆视图的代码:
//脚本代码 <script type="text/javascript"> var verifyErrorList= @GetVerifyErrorList(); $(function(){ if (verifyErrorList != null) { for(var i = 0; i < verifyErrorList.length; i++){ $("#"+verifyErrorList[i].key+"Error").html(verifyErrorList[i].msg) } } }) </script> //html代码 <tr> <td>密码:</td> <td> <input type="password" name="password" id="password" value="@Model.Password"/> </td> <td><span style="color: Red;" id="passwordError"></span></td> </tr>
通过以上实现我们既保证框架能够兼容mvc各个功能,又为高级开发者提供了足够的扩展空间。PS:团队中有位同事曾经将asp.net mvc源码中有关模型绑定和模型校验的代码全部删除,并完美运行实例,性能和开销都少了不少,有兴趣的朋友可以去试试!