BrnShop开源网上商城第二讲:ASP.NET MVC框架

在团队设计BrnShop的web项目之初,我们碰到了两个问题,第一个是数据的复用和传递,第二个是大mvc框架和小mvc框架的选择。下面我依次来说明下。

首先是数据的复用和传递:对于BrnShop的每一次请求,程序都要分成好几个阶段执行,例如验证,执行动作方法等等,在各个阶段我们可能需要重复使用同一信息,而我们的愿景就是希望此信息只需获取一次,然后沿着流程管道一直流动,这样在后面的阶段中就可以直接使用,不用再重新获取了,提高程序的性能。举例来说:在授权验证阶段,我们为对用户进行验证,从而获取了用户信息,当验证结束后,此用户信息并不被抛弃,而是保留下来,这样在后面的动作方法中我们就不需要再次获取用户信息,而是直接使用刚才在授权中保留下来的用户信息就可以了。

  具体实现是这样的:首先我们给这些需要公用的数据定义个上下文类,它们分别是BrnShop.Web.Framework项目中的WebWorkContext类和AdminWorkContext类,其中WebWorkContext是前台项目使用的上下文,AdminWorkContext是后台项目使用的上下文。代码很简单,就是定义了一些公共字段,具体如下:

 

 

  有了上下文类后,我们需要找一个可以保证上下文流动的地方。在翻看了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为后台控制器类,具体实现如下:

 

 

  到此事情还没完,那就是这个上下文是控制器的字段,在视图中如果想访问它需要强制类型转换下,代码为:((BaseWebController)(this.ViewContext.Controller)).WorkContext;试想一下我们每次访问上下文都需要这么长的一段代码那是怎样的煎熬呀?不过幸好有解决办法,那就是重写mvc的WebViewPage页(如果你不知道WebViewPage和mvc的编译过程请阅读大神“Artech”的相关文章,地址如下:http://www.cnblogs.com/artech/)。具体代码在BrnShop.Web.Framework项目中WebViewPage类和AdminViewPage类,其中WebViewPage为前台视图类,AdminViewPage为后台视图类:

 

 

  定义好新的视图类后,我们需要通知编译器使用这个新类,通知方式在视图文件的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源码中有关模型绑定和模型校验的代码全部删除,并完美运行实例,性能和开销都少了不少,有兴趣的朋友可以去试试!

时间: 2024-08-10 16:24:43

BrnShop开源网上商城第二讲:ASP.NET MVC框架的相关文章

BrnShop开源网上商城第一讲:架构设计

首先在此感谢大家对BrnShop项目的支持和鼓励!我们在发布BrnShop以前曾推测项目会受到不少园友的支持,但没想到园友们的支持大大超过我们的预测.4天6000次浏览,140个推荐,170个评论,8000次下载.看到这些数据后我们内心除了激动外,更多了一份责任.无论将来遇到多大的困难,我们一定要坚持把BrnShop坚持到底!! 如果你还不知道BrnShop是什么或还没有下载源码的可以点此下载,如果下载源码后发现商城有bug,也可以点此下载(什么?你还是1.0.**版本?我们现在都已经更新到1.

BrnShop开源网上商城第五讲:自定义视图引擎

今天这篇博文主要讲解自定义视图引擎,大家都知道在asp.net mvc框架中默认自带一个Razor视图引擎,除此之外我们也可以自定义自己的视图引擎,只需要实现IViewEngine接口,接口定义如下: ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache) ViewEngineResult FindPartialView(Co

BrnShop开源网上商城第六讲:扩展视图功能

在正式讲解扩展视图功能以前,我们有必要把视图的工作原理简单说明下.任何一个视图都会被翻译成一个c#类,并保存到指定的位置,然后被编译.这也就是为什么能在视图中包含c#代码片段的原因.下面我们通过一个项目具体的了解一下这个过程,首先我们新建一个mvc3项目,它的Index.cshtml视图文件的代码如下: 1 2 3 4 5 6 7 8 @{     ViewBag.Title = "主页"; } <h2>@ViewBag.Message</h2> <p&g

BrnShop开源网上商城第三讲:插件的工作机制

这几天BrnShop的开发工作比较多,所以这一篇文章来的晚了一些,还请大家见谅呀!还有通知大家一下BrnShop1.0.312版本已经发布,此版本添加了报表统计等新功能,需要源码的园友可以点此下载.好了,我们现在进入今天的正题.关于BrnShop插件内容比较多,所以我分成两篇文章来讲解,今天先讲第一部分内容:插件的工作机制. 对于任意一种插件机制来说,基本上只要解决以下三个方面的问题,这个插件机制就算成功了.这三个方面如下: 插件程序集的加载 视图文件的路径和编译 插件的部署 首先是插件程序集的

BrnShop开源网上商城第四讲:自定义插件

重要通知:BrnShop企业版NOSQL设计(基于Redis)已经开源!源码内置于最新版的BrnShop中,感兴趣的园友可以去下载来看看.官网地址:www.brnshop.com. 好了现在进入今天的正题:自定义插件.上一讲中我们已经阐述了BrnShop插件的工作机制,现在我们详细介绍下如何自定义插件.首先BrnShop的插件从功能上分为三类,分别是: 开放授权插件(OAuth) 支付插件 配送插件 对应的接口文件(注:位于BrnShop.Core项目的Plugin/Base文件夹中)依次如下:

写自己的ASP.NET MVC框架(下)

上篇博客[写自己的ASP.NET MVC框架(上)] 我给大家介绍我的MVC框架对于Ajax的支持与实现原理.今天的博客将介绍我的MVC框架对UI部分的支持. 注意:由于这篇博客是基于前篇博客的,因此有些已说过的内容将会直接跳过,也不会给出提示.所以,如果要想理解这篇博客,那么阅读上篇博客[写自己的ASP.NET MVC框架(上)]则是必要的. 回到顶部 MyMVC的特点 在开发MyMVC的过程中,我吸取了一些ASP.NET WebForm的使用经验,也参考了ASP.NET MVC,也接受了Ma

ASP.NET MVC框架下添加菜单栏及分页项目

原创声明:本文为作者原创,转载请注明出处:http://www.cnblogs.com/DrizzleWorm/p/7274866.html ,谢谢! 我是做前端开发的,之前用C#的三层架构(UI.BLL.DAL)做过一个网站,这是我第一次接触ASP.NET MVC框架,首先给大家分享别人整理的ASP.NET MVC框架的一组教程:http://www.cnblogs.com/powertoolsteam/archive/2015/08/13/4667892.html内容很齐全,我是在先看了其他

学习ASP.NET MVC框架揭秘笔记目录

学习ASP.NET MVC框架揭秘笔记目录 第一章     ASP.NET+MVC 1.1传统的MVC模式 持续更新中,,,,

【转】ASP.NET MVC框架下使用MVVM模式-KnockOutJS+JQ模板例子

KnockOutJS学习系列----(一) 好几个月没去写博客了,最近也是因为项目紧张,不过这个不是借口,J. 很多时候可能是因为事情一多,然后没法静下来心来去写点东西,学点东西. 也很抱歉,突然看到好多的短消息,真不知道该如何给大家回复... 最近试着晚上抽时间写一些knockoutjs和mvc的文章.这里先写一点knockoutjs的东西. 关于knockoutjs到底是什么,如果你不知道,可以看看几个月前我写的一篇文章介绍它. ASP.NET MVC框架下使用MVVM模式 我也是之前安装了