異常處理是每一個系統都必須要有的功能,尤其對於Web系統而言,簡單、統一的異常處理模式尤為重要,當打算使用ASP.NET MVC來做項目時,第一個資料錄入頁面就遇到了這個問題。
在之前的ASP.NET WebForm項目中,一般情況下都是在Application_Error事件處理器和ScriptManager_AsyncPostBackError事件處理器裡面進行,在ASP.NET MVC中用這兩種方法似乎都不合適了,該放在哪兒呢?總不至於在每個Action裡面都放一個try{...}catch{...}吧。
在ScottGu的部落格裡面提到了一個類:HandleErrorAttribute,似乎是用於處理異常的,於是使用HandleErrorAttribute來做個嘗試,(說明,如果使用了該類型,並且想把異常顯示在自已指定的View,則必須在web.config裡面的<system.web>節點加上<customErrors mode="On" />)發現HandleError的確比較好用,可以使用其View屬性指定異常後跳轉的頁面,可以針對不同的異常類型跳到不同的異常顯示View,而且也可以不跳轉到異常顯示View,顯示到當前View,例:
[HttpPost] [HandleError(View = "Create", ExceptionType = typeof(Exception))] public ActionResult Create(string someParam) { throw new Exception("oops..."); }
當異常發生時,頁面還會跳回到Create,只是這裡有點小問題,使用者在頁面上輸入了很多東西,你提示個異常不至於把他辛辛苦苦輸了半天的東西都沒有了吧,把這樣的項目送出去,遲早是要改回來的。
開啟HandleErrorAttribute的原始碼可以看其關鍵區段:
public virtual void OnException(ExceptionContext filterContext) { if (filterContext == null) { throw new ArgumentNullException("filterContext"); } if (filterContext.IsChildAction) { return; } // If custom errors are disabled, we need to let the normal ASP.NET exception handler // execute so that the user can see useful debugging information. if (filterContext.ExceptionHandled || !filterContext.HttpContext.IsCustomErrorEnabled) { return; } Exception exception = filterContext.Exception; // If this is not an HTTP 500 (for example, if somebody throws an HTTP 404 from an action method), // ignore it. if (new HttpException(null, exception).GetHttpCode() != 500) { return; } if (!ExceptionType.IsInstanceOfType(exception)) { return; } string controllerName = (string)filterContext.RouteData.Values["controller"]; string actionName = (string)filterContext.RouteData.Values["action"]; HandleErrorInfo model = new HandleErrorInfo(filterContext.Exception, controllerName, actionName); filterContext.Result = new ViewResult { ViewName = View, MasterName = Master, ViewData = new ViewDataDictionary<HandleErrorInfo>(model), TempData = filterContext.Controller.TempData }; filterContext.ExceptionHandled = true; filterContext.HttpContext.Response.Clear(); filterContext.HttpContext.Response.StatusCode = 500; // Certain versions of IIS will sometimes use their own error page when // they detect a server error. Setting this property indicates that we // want it to try to render ASP.NET MVC's error page instead. filterContext.HttpContext.Response.TrySkipIisCustomErrors = true; }
可以很清楚的看到,MVC實際上是使用剛才我們指定的View名稱建立了一個ViewResult,然後將這個ViewResult交給了InvokeActionResult方法,最終顯示給了使用者。在這個過程中,新的ViewResult的ViewData被設定為HandleErrorInfo了,沒有將Create上的資料放進ViewData,儘管在之後顯示的Create視圖的Request裡還儲存著之前的Params內容,但是資料卻沒有載入上去,我也沒有去深究,感覺如果在這裡直接把filterContext.Controller中的ViewData直接作為新的ViewResult的ViewData的話,肯定是可以顯示提交之前的資料的(因為如果將異常程式碼封裝在try...catch...裡面是可以在異常後顯示之前資料的)。
於是自已建立一個ExceptionFitler:
public class CustomHandleErrorAttribute : FilterAttribute, IExceptionFilter { public void OnException(ExceptionContext filterContext) { filterContext.Controller.ViewData["Exception"] = filterContext.Exception; filterContext.Result = new ViewResult() { ViewName = filterContext.Controller.ControllerContext.RouteData.Values["Action"].ToString(), ViewData = filterContext.Controller.ViewData }; filterContext.ExceptionHandled = true; filterContext.HttpContext.Response.TrySkipIisCustomErrors = true; } }
類名起的不咋的,將就著用吧:)
將原來的Action修改如下:
[HttpPost] [CustomHandleError] public ActionResult Create(string Name) { throw new Exception("oops..."); }
Create.csthml中加入如下代碼:
if (ViewData["Exception"] != null) { var ex = ViewData["Exception"] as Exception; @ex.Message }
F5,果然在提交後又回到了原來視圖,而且之前填寫的資料都還在。
3月19日完善如下:-----------------------------------------
namespace System.Web.Mvc{ public class HandleExceptionAttribute : HandleErrorAttribute, IExceptionFilter { #region IExceptionFilter Members public override void OnException(ExceptionContext filterContext) { if (filterContext == null) { throw new ArgumentNullException("filterContext"); } if (filterContext.IsChildAction) { return; } // If custom errors are disabled, we need to let the normal ASP.NET exception handler // execute so that the user can see useful debugging information. if (filterContext.ExceptionHandled || !filterContext.HttpContext.IsCustomErrorEnabled) { return; } Exception exception = filterContext.Exception; // If this is not an HTTP 500 (for example, if somebody throws an HTTP 404 from an action method), // ignore it. if (new HttpException(null, exception).GetHttpCode() != 500) { return; } if (!ExceptionType.IsInstanceOfType(exception)) { return; } string actionName = (string)filterContext.RouteData.Values["action"]; filterContext.Controller.ViewData["Exception"] = exception; filterContext.Result = new ViewResult() { ViewName = actionName, ViewData = filterContext.Controller.ViewData }; filterContext.ExceptionHandled = true; filterContext.HttpContext.Response.Clear(); filterContext.HttpContext.Response.StatusCode = 500; filterContext.HttpContext.Response.TrySkipIisCustomErrors = true; } #endregion } public static class HandleExceptionHelper { public static Exception Exception(this HtmlHelper htmlhelper) { var exception = htmlhelper.ViewContext.Controller.ViewData["Exception"] as Exception; return exception; } }}
View運用如下:
if (@Html.Exception() != null) { @Html.Exception().Message }
3月20日添加產生jQuery錯誤樣式:------------------------------------------------
public static class HandleExceptionHelper { public static Exception Exception(this HtmlHelper htmlhelper) { var exception = htmlhelper.ViewContext.Controller.ViewData["Exception"] as Exception; return exception; } public static MvcHtmlString jQueryStyleError(this HtmlHelper htmlhelper) { var exception = Exception(htmlhelper); if (exception == null) { return null; } TagBuilder builder = new TagBuilder("div"); builder.GenerateId("editordescription"); builder.AddCssClass("ui-widget ui-state-error ui-corner-all"); builder.InnerHtml = string.Format(@"<p><span class=""ui-icon ui-icon-alert"" style=""float: left; margin-right: .3em;""></span><strong>{0}: </strong>{1}</p>", Resx.Error, string.IsNullOrEmpty(exception.Message) ? Resx.UnknowErrorMessage : exception.Message); return new MvcHtmlString(builder.ToString(TagRenderMode.Normal)); } }
View應用如下:
@Html.jQueryStyleError()
效果如下: