7

I was wondering what the best implementation for a global error (doesn't have to be errors, can also be success messages) handler would be? Let me break it down for you with an example:

  1. User tries to delete a record
  2. Deletion fails and an error is logged
  3. User redirects to another page
  4. Display error message for user (using a HtmlHelper or something, don't want it to be a specific error page)

I'm just curious what you guys think. I've been considering TempData, ViewData and Session but they all have their pros and cons.

TIA!

UPDATE:

I'll show an example what I exactly mean, maybe I wasn't clear enough. This is an example of a method that adds a message when user deletes a record. If user succeeds, user redirects to another page

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);
    }
}

And in the view "RecordsList", it would be kinda cool to show all messages (both error and success messages) in a HtmlHelper or something.

<%= Html.RenderAllMessages %>

This can be achieved in many ways, I'm just curious what you guys would do.

UPDATE 2:

I have created a custom error (message) handler. You can see the code if you scroll down.

3
  • That's kind of odd. In most cases, lack of an error message indicates success. Success messages are redundant in most cases. If there's an error you would stay on the same page, so I guess I still don't get the logic of what you're trying to do. Commented Aug 20, 2011 at 2:24
  • 1
    I disagree, I think it's very important to inform the user that an action has succeeded, otherwise the user can be confused. But I also understand what you mean, it's more important to inform user when failure. Commented Aug 20, 2011 at 10:31
  • 1
    All usability studies that have been done disagree with you. It's a very highly stressed thing. Users don't want to be bothered if there isn't a problem. Commented Aug 20, 2011 at 18:06

4 Answers 4

4

Just for fun, I created my own custom error (message) handler that works pretty much as TempData, but with the small difference that this handler is accessible all over the application.

I'm not going to explain every single step of code, but to sum it all up, I used IHttpModule to fire a method for every request and Session to save data. Below is the code, feel free to edit or give suggestions for improvements.

Web.config (Define module)

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

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

ErrorManager.cs (Error manager handler code)

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 (An extension method for rendering the errors)

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();
    }
}

Usage for creating errors

// 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();

Usage for rendering errors

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

// This will just render all errors with type "Error"
<%= Html.RenderMessages(ErrorManager.Type.Error) %>
Sign up to request clarification or add additional context in comments.

Comments

2

I'm confused by these steps:

  • Deletion fails and an error is logged
  • User redirects to another page

Why would you redirect the User when an error occurs? That doesnt make any sense, unless im misunderstanding something.

Generally, i follow these guidelines:

  • Error with form submission (e.g HTTP POST): check ModelState.IsValid and return the same View and render the error out with @Html.ValidationSummary()
  • Error with AJAX call: return JsonResult (like @Tomas says), and use basic client-side scripting to inspect the JSON and show the result
  • Error with domain/business: throw custom exceptions and let the controller catch them and add to ModelState as above

4 Comments

I know this sounds strange, but lets say your viewing details for an record. Lets say you delete the record and you want to redirect the user to a listview for all records, and display a message to the user. Of course, you can use TempData but it would be nice to use some kind of generic error handling, that will log all your messages, even if a request is involved.
@Kristoffer - i see, your not really talking about error messages per-se, but custom messages. TempData is your only option. I do this kind of thing when a non-logged in user tries to save some data. I chuck the data in TempData, redirect the user to the login, then back again and prefill the form. I do this via a custom action filter.
Exactly, custom messages is a better definition. I know how to access TempData from controllers and views, but can I access it from, lets say a HtmlHelper?
@Kristoffer - you definetely can. The HtmlHelper can "see" everything the View can.
1

I prefer writing my server layer as an API emitting JSON - in ASP.NET MVC that's real simple - you just create a bunch of nested anonymous objects, and return Json(data);. The JSON object is then consumed by the client layer, which consists of html, css and javascript (I use jQuery a lot, but you might prefer other tools).

Since javascript is dynamic, it is then real easy to just have a property status on the data object, and the client side script can interpret that and display status or error messages as needed.

For example, consider the following action method:

public ActionResult ListStuff()
{
    var stuff = Repo.GetStuff();

    return Json(new { status = "OK", thestuff = stuff });
}

This will return JSON in the following format:

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

where ... is a placeholder for the properties of stuff. Now, if I want error handling, I can just do

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 });
}

Since javascript is dynamic, it doesn't matter that the two anonymous objects don't have the same properties. Based on the value of status, I'll only look for properties that are actually there.

This can be implemented even better if you create your own action result classes, which extend JsonResult and add the status property automatically. For example, you can create one for failed requests that takes an exception in the constructor and one for successful ones than take an anonymous object.

3 Comments

Yes, that is one way of doing it. I often tend to use this technique when I'm working with javascript, but with my scenario above this example wouldn't work(?) because a HTTP request is involved. That's why I'm curious of implementations how to hold errors over HTTP requests.
@Max: I prefer to use the HTTP status codes for things that I didn't forsee. The status in my JSON object is not so much "the request failed because of a server error" as "What you tried to do didn't work, but it's not really because of an error in the application."
@Kristoffer: I use this code to respond to HTTP requests created in javascript.
0

If all you're going to do is redirect the user to another page, then you can use any ActionMethod to do so and just redirect to it.

If you want a global error, such as a 500 or 403 or some other error, then the MVC 3 default template creates an _Error.cshtml page for you and registers the error handler in the global.asax.

If you want to catch specific errors, then you can register additional handlers in the same place and tell the system which Error page to use for that error.

1 Comment

Hi! I don't relly know if that is what I'm looking for. I have updated my post with a more detailed example. Thanks!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.