在團隊設計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源碼中有關模型綁定和模型校驗的代碼全部刪除,並完美運行實例,性能和開銷都少了不少,有興趣的朋友可以去試試!
