views:

385

answers:

4

When implementing error-handling using the built-in validation-helpers on a strongly-typed view, you usually create a try/catch block within the controller and return a view with it's corresponding model as a parameter to the View() method:


The controller

public class MessageController : Controller
{
    [AcceptVerbs(HttpVerbs.Post)]
    public ActionResult Create(Models.Entities.Message message)
    {
        try
        {
            // Insert model into database
            var dc = new DataContext();
            dc.Messages.InsertOnSubmit(message);
            dc.SubmitChanges();

            return RedirectToAction("List");
        }
        catch
        {
            /* If insert fails, return a view with it's corresponding model to
               enable validation helpers */
            return View(message);
        }
    }
}



The view

<%@ Page
    Language="C#"
    Inherits="System.Web.Mvc.ViewPage<Models.Entities.Message>" %>

<%= Html.ValidationSummary("Fill out fields marked with *") %>

<% using (Html.BeginForm()) { %>

    <div><%= Html.TextBox("MessageText") %></div>

    <div><%= Html.ValidationMessage("MessageText", "*") %></div>

<% } %>



I've implemented a simple error-handler in the form of an ActionFilterAttribute, which will be able to either redirect to a generic error view, or redirect to the view which threw an exception, and let the validation-helpers spring to life.

Here's how my ActionFilterAttribute looks:

public class ErrorLoggingAttribute : ActionFilterAttribute, IExceptionFilter
{
    private Boolean _onErrorRedirectToGenericErrorView;

    /// <param name="onErrorRedirectToGenericErrorView">
    /// True: redirect to a generic error view.
    /// False: redirect back the view which threw an exception
    /// </param>
    public ErrorLoggingAttribute(Boolean onErrorRedirectToGenericErrorView)
    {
        _onErrorRedirectToGenericErrorView = onErrorRedirectToGenericErrorView;
    }

    public void OnException(ExceptionContext ec)
    {
        if (_onErrorRedirectToGenericErrorView)
        {
            /* Redirect back to the view where the exception was thrown and
               include it's model so the validation helpers will work */
        }
        else
        {
            // Redirect to a generic error view
            ec.Result = new RedirectToRouteResult(new RouteValueDictionary
            {
                {"controller", "Error"},
                {"action", "Index"}
            });

            ec.ExceptionHandled = true;
        }
    }
}

Redirecting to the view which threw the exception is fairly simple. But here's the kicker: In order for the validation helpers to work, you need to provide the view with it's model.

How would you return the view which threw an exception and provide the view with it's corresponding model? (In this case Models.Entities.Message).

A: 

Since you inherit from ActionFilterAttribute From OnActionExecuting, you can grab your model.

  public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        var model = filterContext.Controller.ViewData.Model as YourModel;

        ...
    }

But there is already HandleError defined in MVC system, why don't you use this one instead of baking your own.

I suggest you read this blog on this issue.

J.W.
How would you assign the model to a view and redirect to that view?
roosteronacid
ExceptionContext ec has a result property, you can assign your model there. ec.Result = new ViewResult { ViewName = View, MasterName = Master, ViewData = yourmodel, TempData = filterContext.Controller.TempData };
J.W.
There's only one prolblem: the model is null in both methods of IActionFilter _and_ IResultFilter. It seems like this can't be done.
roosteronacid
I found out how to make it work. Please excuse my temporary down-vote. I always vote up people who take the time to help me out. I will up-vote your answer as soon as the timer on the bounty expires. (I am not able to accept my own answer on a question that has a bounty attached).
roosteronacid
A: 

If your action throws exception, there's no way to pass the model to the view since the model is not probably created yet - or not fully created. That's probably why the result is null. You can't rely on the data after exception was thrown.

But you can pass pass "default" model to your action filter like this:

[ErrorLogging(new EmptyModel())] 
// or to create using Activator
[ErrorLogging(typeof(EmptyModel))]
// or even set view name to be displayed
[ErrorLogging("modelerror", new EmptyModel())]

This way your filter will pass this "error model" that you explicitely set to be displayed when an error happened.

queen3
I found out how to make it work. Please excuse my temporary down-vote. I always vote up people who take the time to help me out. I will up-vote your answer as soon as the timer on the bounty expires. (I am not able to accept my own answer on a question that has a bounty attached).
roosteronacid
While I always appreciate people playing fair to each other, I'm not really that much into SO vote system - that's not a big part of my life ;-) So I don't really mind down-votes. But thanks, anyway.
queen3
A: 
public class MessageController : Controller
{
  public ActionResult Create()
  {
    return View();
  }

  [AcceptVerbs(HttpVerbs.Post)]
  public ActionResult Create( Message message )
  {
    try
    {
      // Exceptions for flow control are so .NET 1.0 =)
      // ... your save code here
    }
    catch
    {
      // Ugly catch all error handler - do you really know you can fix the problem?  What id the database server is dead!?!
      return View();
    }
  }
}

The details of the model are already present in modelstate. Any errors should also already be present in modelstate. Your exception handler only needs to handle the case where you want to redirect to a generic error page. Better / more obvious is to throw the attribute away and if you want to redirect in the catch, return a redirect result.

Neal
I found out how to make it work. Please excuse my temporary down-vote. I always vote up people who take the time to help me out. I will up-vote your answer as soon as the timer on the bounty expires. (I am not able to accept my own answer on a question that has a bounty attached).
roosteronacid
A: 

I got it to work!

For some odd reason, all I needed to do was to pass on the ViewData to a new ResultView.

Here's the complete code:

public class ErrorLoggingAttribute : ActionFilterAttribute, IExceptionFilter
{
    private String _controllerName, _actionName;
    private Boolean _redirectToGenericView = false;


    public ErrorLoggingAttribute()
    {
    }


    public ErrorLoggingAttribute(String actionName, String controllerName)
    {
        _controllerName = controllerName;
        _actionName = actionName;
        _redirectToGenericView = true;
    }


    void IExceptionFilter.OnException(ExceptionContext ec)
    {
        // log error

        if (_redirectToGenericView)
        {
            ec.Result = new RedirectToRouteResult(new RouteValueDictionary
            {
                {"controller", _controllerName},
                {"action", _actionName}
            });
        }
        else
        {
            ec.Result = new ViewResult
            {
                ViewName = ((RouteData) ec.RouteData).Values["action"].ToString(),
                TempData = ec.Controller.TempData,
                ViewData = ec.Controller.ViewData
            };
        }

        ec.ExceptionHandled = true;
    }
}


Usage


Here's how you would use the attribute on a controller-action, to redirect to the same view (with it's associated model) to enable standard validation-helpers to kick in, when an exception occurs:

[ErrorLogging]
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create(Models.Entities.Message message)
{
    var dc = new Models.DataContext();
    dc.Messages.InsertOnSubmit(message);
    dc.SubmitChanges();

    return RedirectToAction("List", new { id = message.MessageId });
}

And here's how you would use the attribute, to redirect to a generic view, when an exception occurs:

[ErrorLogging("ControllerName", "ViewName")]
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create(Models.Entities.Message message)


This is a complete separation of logic. Nothing in the controller but the very basics.

roosteronacid
Your controller should be focussed on application flow, using this approach obscures the logical application flow by shifting the error portion of it up into attributes.Just because you can, doesn't mean you should.
Neal