【问题标题】:ASP.NET MVC - Approach for global error handling?ASP.NET MVC - 全局错误处理的方法?
【发布时间】:2011-08-17 21:50:01
【问题描述】:

我想知道全局错误(不一定是错误,也可以是成功消息)处理程序的最佳实现是什么?让我用一个例子给你分解一下:

  1. 用户尝试删除记录
  2. 删除失败并记录错误
  3. 用户重定向到另一个页面
  4. 为用户显示错误消息(使用 HtmlHelper 或其他东西,不希望它成为特定的错误页面)

我只是好奇你们的想法。我一直在考虑 TempData、ViewData 和 Session,但它们各有优缺点。

TIA!

更新:

我将举例说明我的确切意思,也许我不够清楚。 这是在用户删除记录时添加消息的方法示例。 如果用户成功,用户将重定向到另一个页面

public ActionResult DeleteRecord(Record recordToDelete)
{
    // If user succeeds deleting the record
    if (_service.DeleteRecord(recordToDelete) 
    {
        // Add success message
        MessageHandler.AddMessage(Status.SUCCESS, "A message to user");

        // And redirect to list view
        return RedirectToAction("RecordsList");
    }
    else 
    {
        // Else return records details view
        return View("RecordDetails", recordToDelete);
    }
}

在“RecordsList”视图中,在 HtmlHelper 或其他东西中显示所有消息(错误和成功消息)会很酷。

<%= Html.RenderAllMessages %>

这可以通过多种方式实现,我只是好奇你们会做什么。

更新 2:

我创建了一个自定义错误(消息)处理程序。向下滚动可以看到代码。

【问题讨论】:

  • 这有点奇怪。在大多数情况下,没有错误消息表示成功。在大多数情况下,成功消息是多余的。如果有错误,你会留在同一页面上,所以我想我仍然不明白你想要做什么的逻辑。
  • 我不同意,我认为通知用户一个动作已经成功是非常重要的,否则用户可能会感到困惑。但我也明白你的意思,失败时通知用户更重要。
  • 所有已经完成的可用性研究都不同意你的观点。这是一件压力很大的事情。如果没有问题,用户不想被打扰。

标签: c# asp.net asp.net-mvc


【解决方案1】:

只是为了好玩,我创建了我自己的自定义错误(消息)处理程序,它的工作原理与 TempData 非常相似,但略有不同的是,该处理程序可以在整个应用程序中访问。

我不会解释代码的每一步,但总而言之,我使用 IHttpModule 为每个请求触发一个方法,并使用 Session 来保存数据。以下是代码,请随时编辑或提出改进建议。

Web.config(定义模块)

<httpModules>
  <add name="ErrorManagerModule" type="ErrorManagerNamespace.ErrorManager"/>
</httpModules>

<system.webServer>
  <modules runAllManagedModulesForAllRequests="true">
    <add name="ErrorManagerModule" type="ErrorManagerNamespace.ErrorManager"/>
  </modules>
</system.webServer>

ErrorManager.cs(错误管理器处理程序代码)

public class ErrorManager : IRequiresSessionState, IHttpModule
{
    private const string SessionKey = "ERROR_MANAGER_SESSION_KEY";

    public enum Type 
    {
        None,
        Warning,
        Success,
        Error
    }

    /*
     * 
     * Public methods
     * 
     */

    public void Dispose() 
    {
    }

    public void Init(HttpApplication context) 
    {
        context.AcquireRequestState += new EventHandler(Initiliaze);
    }

    public static IList<ErrorModel> GetErrors(ErrorManager.Type type = Type.None) 
    {
        // Get all errors from session
        var errors = GetErrorData();

        // Destroy Keep alive
        // Decrease all errors request count
        foreach (var error in errors.Where(o => type == ErrorManager.Type.None || o.ErrorType == type).ToList())
        {
            error.KeepAlive = false;
            error.IsRead = true;
        }

        // Save errors to session
        SaveErrorData(errors);

        //return errors;
        return errors.Where(o => type == ErrorManager.Type.None || o.ErrorType == type).ToList();
    }

    public static void Add(ErrorModel error) 
    {
        // Get all errors from session
        var errors = GetErrorData();
        var result = errors.Where(o => o.Key.Equals(error.Key, StringComparison.OrdinalIgnoreCase)).FirstOrDefault();

        // Add error to collection
        error.IsRead = false;

        // Error with key is already associated
        // Remove old error from collection
        if (result != null)
            errors.Remove(result);

        // Add new to collection
        // Save errors to session
        errors.Add(error);
        SaveErrorData(errors);
    }

    public static void Add(string key, object value, ErrorManager.Type type = Type.None, bool keepAlive = false) 
    {
        // Create new error
        Add(new ErrorModel()
        {
            IsRead = false,
            Key = key,
            Value = value,
            KeepAlive = keepAlive,
            ErrorType = type
        });
    }

    public static void Remove(string key) 
    {
        // Get all errors from session
        var errors = GetErrorData();
        var result = errors.Where(o => o.Key.Equals(key, StringComparison.OrdinalIgnoreCase)).FirstOrDefault();

        // Error with key is in collection
        // Remove old error
        if (result != null)
            errors.Remove(result);

        // Save errors to session
        SaveErrorData(errors);
    }

    public static void Clear() 
    {
        // Clear all errors
        HttpContext.Current.Session.Remove(SessionKey);
    }

    /*
     * 
     * Private methods
     * 
     */

    private void Initiliaze(object o, EventArgs e) 
    {
        // Get context
        var context = ((HttpApplication)o).Context;

        // If session is ready
        if (context.Handler is IRequiresSessionState || 
            context.Handler is IReadOnlySessionState)
        {
            // Load all errors from session
            LoadErrorData();
        }
    }

    private static void LoadErrorData() 
    {
        // Get all errors from session
        var errors = GetErrorData().Where(o => !o.IsRead).ToList();

        // If KeepAlive is set to false
        // Mark error as read
        foreach (var error in errors)
        {
            if (error.KeepAlive == false)
                error.IsRead = true;
        }

        // Save errors to session
        SaveErrorData(errors);
    }

    private static void SaveErrorData(IList<ErrorModel> errors) 
    {
        // Make sure to remove any old errors
        HttpContext.Current.Session.Remove(SessionKey);
        HttpContext.Current.Session.Add(SessionKey, errors);
    }

    private static IList<ErrorModel> GetErrorData() 
    {
        // Get all errors from session
        return HttpContext.Current.Session[SessionKey]
            as IList<ErrorModel> ??
            new List<ErrorModel>();
    }

    /*
     * 
     * Model
     * 
     */

    public class ErrorModel 
    {
        public string Key { get; set; }
        public object Value { get; set; }
        public bool KeepAlive { get; set; }
        internal bool IsRead { get; set; }
        public Type ErrorType { get; set; }
    }

HtmlHelperExtension.cs(呈现错误的扩展方法)

public static class HtmlHelperExtension
{
    public static string RenderMessages(this HtmlHelper obj, ErrorManager.Type type = ErrorManager.Type.None, object htmlAttributes = null) 
    {
        var builder = new TagBuilder("ul");
        var errors = ErrorManager.GetErrors(type);

        // If there are no errors
        // Return empty string
        if (errors.Count == 0)
            return string.Empty;

        // Merge html attributes
        builder.MergeAttributes(new RouteValueDictionary(htmlAttributes), true);

        // Loop all errors
        foreach (var error in errors)
        {
            builder.InnerHtml += String.Format("<li class=\"{0}\"><span>{1}</span></li>",
                error.ErrorType.ToString().ToLower(),
                error.Value as string);
        }

        return builder.ToString();
    }
}

用于创建错误

// This will only be available for one request
ErrorManager.Add("Key", "An error message", ErrorManager.Type.Error);

// This will be available for multiple requests
// When error is read, it will be removed
ErrorManager.Add("Key", "An error message", ErrorManager.Type.Error, true);

// Remove an error
ErrorManager.Remove("AnotherKey");

// Clear all error
ErrorManager.Clear();

用于渲染错误

// This will render all errors
<%= Html.RenderMessages() %>

// This will just render all errors with type "Error"
<%= Html.RenderMessages(ErrorManager.Type.Error) %>

【讨论】:

    【解决方案2】:

    我对这些步骤感到困惑:

    • 删除失败并记录错误
    • 用户重定向到另一个页面

    发生错误时为什么要重定向用户?这没有任何意义,除非我误解了什么。

    通常,我遵循以下准则:

    • 提交表单时出错(例如 HTTP POST):检查 ModelState.IsValid 并返回相同的视图并使用 @Html.ValidationSummary() 呈现错误
    • AJAX 调用出错:返回 JsonResult(如 @Tomas 所说),并使用基本的客户端脚本检查 JSON 并显示结果
    • 域/业务错误:抛出自定义异常并让控制器捕获它们并添加到ModelState 如上所述

    【讨论】:

    • 我知道这听起来很奇怪,但让我们说一下您的观看详细信息以作记录。假设您删除了记录,并且您希望将用户重定向到所有记录的列表视图,并向用户显示一条消息。当然,您可以使用 TempData,但最好使用某种通用错误处理,即使涉及请求,它也会记录您的所有消息。
    • @Kristoffer - 我明白了,你并不是真的在谈论错误消息本身,而是自定义消息。 TempData 是您唯一的选择。当未登录的用户尝试保存一些数据时,我会做这种事情。我将数据夹在 TempData 中,将用户重定向到登录名,然后再次返回并预填表单。我通过自定义操作过滤器做到这一点。
    • 没错,自定义消息是一个更好的定义。我知道如何从控制器和视图访问 TempData,但我可以从 HtmlHelper 访问它吗?
    • @Kristoffer - 你绝对可以。 HtmlHelper 可以“看到” View 所能看到的一切。
    【解决方案3】:

    我更喜欢将我的服务器层编写为一个发送 JSON 的 API——在 ASP.NET MVC 中这非常简单——你只需创建一堆嵌套的匿名对象,然后 return Json(data);。 JSON 对象随后被客户端层使用,客户端层由 html、css 和 javascript 组成(我经常使用 jQuery,但您可能更喜欢其他工具)。

    由于 javascript 是动态的,因此很容易在数据对象上拥有一个属性 status,客户端脚本可以解释它并根据需要显示状态或错误消息。

    例如,考虑以下操作方法:

    public ActionResult ListStuff()
    {
        var stuff = Repo.GetStuff();
    
        return Json(new { status = "OK", thestuff = stuff });
    }
    

    这将返回以下格式的 JSON:

    { "status": "OK", "thestuf": [{ ... }, { ... }] }
    

    其中...stuff 属性的占位符。现在,如果我想要错误处理,我可以这样做

    try
    {
        var stuff = Repo.GetStuff();
        return Json(new { status = "OK", thestuff = stuff});
    }
    catch (Exception ex)
    {
        Log.Error(ex);
        return Json(new { status = "Fail", reason = ex.Message });
    }
    

    由于 javascript 是动态的,因此两个匿名对象没有相同的属性并不重要。根据status 的值,我只会查找实际存在的属性。

    如果您创建自己的操作结果类(扩展JsonResult 并自动添加状态属性),这可以更好地实现。例如,您可以为在构造函数中接受异常的失败请求创建一个,为成功的请求创建一个而不接受匿名对象。

    【讨论】:

    • 是的,这是一种方法。当我使用 javascript 时,我经常倾向于使用这种技术,但是在我上面的场景中,这个例子不起作用(?),因为涉及到 HTTP 请求。这就是为什么我对如何通过 HTTP 请求保持错误的实现感到好奇。
    • @Max:我更喜欢将 HTTP 状态代码用于我没有预见到的事情。我的 JSON 对象中的状态与其说是“由于服务器错误导致请求失败”,不如说是“您尝试做的事情没有奏效,但这并不是因为应用程序中的错误。”
    • @Kristoffer:我使用这段代码来响应在 javascript 中创建的 HTTP 请求。
    【解决方案4】:

    如果您要做的只是将用户重定向到另一个页面,那么您可以使用任何 ActionMethod 来执行此操作并重定向到它。

    如果您需要全局错误,例如 500 或 403 或其他错误,则 MVC 3 默认模板会为您创建一个 _Error.cshtml 页面并在 global.asax 中注册错误处理程序。

    如果你想捕捉特定的错误,那么你可以在同一个地方注册额外的处理程序,并告诉系统哪个错误页面用于那个错误。

    【讨论】:

    • 嗨!我不知道这是否是我正在寻找的。我已经用更详细的示例更新了我的帖子。谢谢!
    猜你喜欢
    • 2012-03-16
    • 2022-01-07
    • 2012-03-26
    • 2011-12-14
    • 1970-01-01
    • 2010-11-24
    • 1970-01-01
    • 2013-05-04
    相关资源
    最近更新 更多