How to display exceptions in the same View? - asp.net-mvc

I search for a generic way to display thrown exceptions without redirecting to an error page but displaying it in the same view. I tried these below:
1) I firstly tried to handle them by adding a custom filter in global.asax and overriding public override void OnException(ExceptionContext filterContext) in my Attribute class but in that way, I couldn't fill filterContext.Result in the way I want since the old model of the view is not reachable so I could only redirect to an error page but that's not what I want.
2) Then I tried to catch the exceptions on my BaseController(All of my controllers inherits from it). I again override public override void OnException(ExceptionContext filterContext) in my controller and put exception details etc. in ViewBag and redirected the page to the same view by filterContext.HttpContext.Response.Redirect(filterContext.RequestContext.HttpContext.Request.Path ); but ViewBag contents are lost in the redirected page so I can't think any other way?
How can I achieve that? Code Sample that I wrote in my BaseController is below:
protected override void OnException(ExceptionContext filterContext) {
var controllerName = (string)filterContext.RouteData.Values["controller"];
var actionName = (string)filterContext.RouteData.Values["action"];
//filterContext.Result = new ViewResult
//{
// ViewName = actionName,
// ViewData = new ViewDataDictionary<??>(??),
// TempData = filterContext.Controller.TempData,
//};
filterContext.ExceptionHandled = true;
filterContext.HttpContext.Response.Clear();
filterContext.HttpContext.Response.TrySkipIisCustomErrors = true;
ModelState.AddModelError("Error", filterContext.Exception.Message);
ViewBag.das = "dasd";
filterContext.HttpContext.Response.Redirect(filterContext.RequestContext.HttpContext.Request.Path);
}

Maybe you could set a property in your BaseController class to have the name of the view that you want to use, setting that in whatever controller action handles the request. Then in OnException() you could have a method, that redirects to a controller action, that just returns a View that corresponds to the view name? Each controller action would have to set a default view name before it does anything else because only it knows what view it will call if any, and what view it likely was invoked by.
You'd need some sort of BaseController action that returns the new View.
The route(s) may or many not need configuration to have some sort of optional parameter(s) that you could set to be what error information you want to send to your view. For example, in the default route:
routes.MapRoute(RouteNames.Default,
"{controller}/{action}/{id}",
new {controller = "Home", action = "Index", id = "", errorInfo = UrlParameter.Optional}
BaseController:
protected ActionResult ErrorHandler()
{
ViewBag.das = (string)filterContext.RouteData.Values["errorInfo"];
return View(ViewName);
}
protected string ViewName { get; set; }
protected void GoToErrorView(ExceptionContext context, string exceptionData)
{
var actionName = "ErrorHandler";
var newVals = new RouteValueDictionary();
newVals.Add("errorInfo", exceptionData);
this.RedirectToAction(actionName, newVals);
}
In BaseController.OnException():
// ...
filterContext.HttpContext.Response.TrySkipIisCustomErrors = true;
ModelState.AddModelError("Error", filterContext.Exception.Message);
// anything else you need to do to prepare what you want to display
string exceptionData = SomeSortOfDataYouWantToPassIntoTheView;
this.GoToErrorView(filterContext, exceptionData);
}
In the specific controllers that inherit from BaseController that are returning an ActionResult specifically a ViewResult:
[HttpGet]
public ActionResult Index()
{
ViewName = <set whatever view name you want to here>
// code here, including preparing the Model
// ...
var model = new MyViewModel();
model.SomethingIWantToGiveTheView = someDataThatItNeeds;
// ...
return View(<model name>, model);
}

I found the solution a while ago and add the solution so that it may help the others. I use TempData and _Layout to display errors:
public class ErrorHandlerAttribute : HandleErrorAttribute
{
private ILog _logger;
public ErrorHandlerAttribute()
{
_logger = Log4NetManager.GetLogger("MyLogger");
}
public override void OnException(ExceptionContext filterContext)
{
if (filterContext.ExceptionHandled)
{
return;
}
if (!ExceptionType.IsInstanceOfType(filterContext.Exception))
{
return;
}
// if the request is AJAX return JSON else view.
if (filterContext.HttpContext.Request.Headers["X-Requested-With"] == "XMLHttpRequest")
{
filterContext.Result = new JsonResult
{
JsonRequestBehavior = JsonRequestBehavior.AllowGet,
Data = new
{
error = true,
message = filterContext.Exception.Message
}
};
filterContext.HttpContext.Response.StatusCode = 500;
}
// log the error using log4net.
_logger.Error(filterContext.Exception.Message, filterContext.Exception);
filterContext.ExceptionHandled = true;
filterContext.HttpContext.Response.Clear();
filterContext.HttpContext.Response.TrySkipIisCustomErrors = true;
if (filterContext.HttpContext.Request.Headers["X-Requested-With"] != "XMLHttpRequest")
{
if (filterContext.Controller.TempData["AppError"] != null)
{
//If there is a loop it will break here.
filterContext.Controller.TempData["AppError"] = filterContext.Exception.Message;
filterContext.HttpContext.Response.Redirect("/");
}
else
{
int httpCode = new HttpException(null, filterContext.Exception).GetHttpCode();
switch (httpCode)
{
case 401:
filterContext.Controller.TempData["AppError"] = "Not Authorized";
filterContext.HttpContext.Response.Redirect("/");
break;
case 404:
filterContext.Controller.TempData["AppError"] = "Not Found";
filterContext.HttpContext.Response.Redirect("/");
break;
default:
filterContext.Controller.TempData["AppError"] = filterContext.Exception.Message;
//Redirect to the same page again(If error occurs again, it will break above)
filterContext.HttpContext.Response.Redirect(filterContext.RequestContext.HttpContext.Request.RawUrl);
break;
}
}
}
}
}
And in Global.asax:
protected void Application_Error(object sender, EventArgs e)
{
var httpContext = ((MvcApplication)sender).Context;
var ex = Server.GetLastError();
httpContext.ClearError();
httpContext.Response.Clear();
httpContext.Response.StatusCode = ex is HttpException ? ((HttpException)ex).GetHttpCode() : 500;
httpContext.Response.TrySkipIisCustomErrors = true;
var routeData = new RouteData();
routeData.Values["controller"] = "ControllerName";
routeData.Values["action"] = "ActionName";
routeData.Values["error"] = "404"; //Handle this url paramater in your action
((IController)new AccountController()).Execute(new RequestContext(new HttpContextWrapper(httpContext), routeData));
}

Related

Redirecting from exception handling filter

I am using the filter from the solution posted to Clean way to catch all errors thrown in an MVC 3.0 application?, and I was wondering how I could redirect to a view (of a simple error page that resides in /Views/Account/ErrorPage, as part of the AccountController), in that filter? It is reproduced below:
public class HandleExceptionsAttribute : HandleErrorAttribute
{
public override void OnException(ExceptionContext filterContext)
{
var expception = filterContext.Exception;
// Go to generic error page:
}
}
Thank you.
Set the filterContext.Result
filterContext.Result = new ViewResult
{
ViewName = "<view name>",
ViewData = new ViewDataDictionary<HandleErrorInfo>(model),
TempData = filterContext.Controller.TempData
};
Here's the full method that captures the call that errors
public override void OnException(ExceptionContext filterContext)
{
base.OnException(filterContext);
var controllerName = (string) filterContext.RouteData.Values["controller"];
var actionName = (string) filterContext.RouteData.Values["action"];
var model = new HandleErrorInfo(filterContext.Exception, controllerName, actionName);
filterContext.Result = new ViewResult
{
ViewName = "<view name>",
ViewData = new ViewDataDictionary<HandleErrorInfo>(model),
TempData = filterContext.Controller.TempData
};
filterContext.ExceptionHandled = true;
filterContext.HttpContext.Response.Clear();
filterContext.HttpContext.Response.StatusCode = 500;
filterContext.HttpContext.Response.TrySkipIisCustomErrors = true;
}

MVC requests for /null returning 404 errors

I have an MVC project that appears to work great, unless you look at the error log. After every page is returned successfully, there is an attempt to load site.example.com/null and I have no idea why. Here's a snippet from Fiddler:
It has no effect to the user, but it is annoying. Here's a sample of the code that's being called:
public class GuidanceController : Controller
{
public ActionResult Index()
{
return View();
}
}
I have nothing unusual in the view and I haven't had to change the RouteConfig. I do have a custom authorization class, like so:
namespace MyProj.Filters {
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true)]
public class MyProjAuthorizeAttribute : AuthorizeAttribute
{
public AccessLvl[] AccessLvls;
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
AuthorizationClient authClient = new AuthorizationClient();
var authorized = base.AuthorizeCore(httpContext);
if (!authorized)
return false;
int intId = 0;
string requestId = httpContext.Request.Path.Split('/').Last(); //get ID sent with new page
string referrerId = httpContext.Request.UrlReferrer?.AbsolutePath.Split('/').Last(); //get ID sent with old page
if (int.TryParse(requestId, out intId) != true) //prefer new ID, if available
int.TryParse(referrerId, out intId); //else just use old id
List<AccessLvl> userAccessLevels = authClient.GetAccessLevels((intId == 0) ? null : intId.ToString());
foreach (AccessLvl level in AccessLvls)
{
if (userAccessLevels.Contains(level))
{
return true;
}
}
return false;
}
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
filterContext.Result = new RedirectToRouteResult(
new RouteValueDictionary(
new
{
controller = "Error",
action = "Unauthorized",
urlReferrer = filterContext.RequestContext.HttpContext.Request.Url
})
);
}
}
}
What am I overlooking? Any ideas are greatly appreciated.

Automatic C# MVC Error Handling

I've got this code.
public ActionResult Index()
{
ReceiptModel model = new ReceiptModel();
try
{
model = new ReceiptModel(context);
}
catch (BussinessException bex)
{
ModelState.AddModelError("Index", bex.MessageToDisplay);
return View("Index");
}
return View(model);
}
BussinesException ir returned from database and then displayed for user. I have to put on every controller method try-catch statement, which is a bit tedious. Is there any easier way how to handle these exceptions?
P.S. All other exceptions are handled with HandleExceptionAttribute
UPDATE:
I used Floradu88 approach. So Now i have something like this.
public sealed class HandleBussinessExceptionAttribute : HandleErrorAttribute, IExceptionFilter
{
public override void OnException(ExceptionContext filterContext)
{
filterContext.Controller.TempData["UnhandledException"] = filterContext.Exception;
filterContext.ExceptionHandled = true;
((Controller)filterContext.Controller).ModelState.AddModelError(
((BussinessException)filterContext.Exception).Code.ToString(),
((BussinessException)filterContext.Exception).MessageToDisplay
);
filterContext.Result = new ViewResult
{
ViewName = this.View,
TempData = filterContext.Controller.TempData,
ViewData = filterContext.Controller.ViewData,
};
}
}
and on Controller action i put
[HandleBussinessExceptionAttribute(Order = 2, ExceptionType = typeof(BussinessException), View = "Login")]
i also tried in exception handler:
filterContext.Result = new RedirectToRouteResult(new RouteValueDictionary(filterContext.RouteData));
and then handle error in action with ModelState.IsValid but values to action comes null. So for now i use first approach. When i have a bit more time i'll try to fix second approach.
Please read the documentation on this part:
http://msdn.microsoft.com/en-us/library/gg416513%28v=vs.98%29.aspx
http://www.asp.net/web-api/overview/web-api-routing-and-actions/exception-handling
http://www.asp.net/mvc/tutorials/older-versions/controllers-and-routing/understanding-action-filters-cs
Too much content to be posted here:
public class NotImplExceptionFilterAttribute : ExceptionFilterAttribute
{
public override void OnException(HttpActionExecutedContext context)
{
if (context.Exception is NotImplementedException)
{
context.Response = new HttpResponseMessage(HttpStatusCode.NotImplemented);
}
}
}
And your controller:
public class ProductsController : ApiController
{
[NotImplExceptionFilter]
public Contact GetContact(int id)
{
throw new NotImplementedException("This method is not implemented");
}
}
According to this post, Create a custom binder and put the model in TempData:
public class AppBinder : DefaultModelBinder
{
protected override void OnModelUpdated(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
base.OnModelUpdated(controllerContext, bindingContext);
controllerContext.Controller.TempData["model"] = bindingContext.Model;
}
}
Register it in global.asax:
ModelBinders.Binders.DefaultBinder = new AppBinder();
Create a BaseController and override the OnException:
protected override void OnException(ExceptionContext filterContext)
{
filterContext.ExceptionHandled = true;
this.ModelState.AddModelError(string.Empty, filterContext.Exception.Message);
filterContext.Result = new ViewResult
{
ViewName = filterContext.RouteData.Values["action"].ToString(),
TempData = filterContext.Controller.TempData,
ViewData = filterContext.Controller.ViewData
};
base.OnException(filterContext);
}
All Actions int The Controllers which inherited from this base controller will show their unhandled exceptions their own view in validation summery (remember to have
#Html.ValidationSummary(true)
in page). it works for me, hope works for you too!

HandleErrorAttribute not working

I have started an MVC 3 template project in VS10 and modified global.asax.cs as such:
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new HandleErrorAttribute { ExceptionType = typeof(DivideByZeroException), View = "DivideByZeroException", Order = 1 });
filters.Add(new HandleErrorAttribute { View = "AllOtherExceptions", Order = 2 });
}
To web.config I added:
<customErrors mode="On">
Then created the corresponding views and finally added a DivideByZero-throw to one of the actions.
The result: The view AllOtherExceptions is rendered.
Much though I hate to disagree with anything Darin says, he is wrong on this one.
There is no problem with setting the properties (that's the way you are supposed to do it).
The only reason your original code didn't work as expected is because you have the Order set wrong.
See MSDN:
The OnActionExecuting(ActionExecutingContext),
OnResultExecuting(ResultExecutingContext), and
OnAuthorization(AuthorizationContext) filters run in forward order.
The OnActionExecuted(ActionExecutedContext),
OnResultExecuting(ResultExecutingContext), and
OnException(ExceptionContext) filters run in reverse order.
So your generic AllOtherExceptions filter needs to be the lowest Order number, not the highest.
Hopefully that helps for next time.
You shouldn't set properties when registering a global action filter. You could write a custom handle error filter:
public class MyHandleErrorAttribute : FilterAttribute, IExceptionFilter
{
public void OnException(ExceptionContext filterContext)
{
if (!filterContext.IsChildAction && (!filterContext.ExceptionHandled && filterContext.HttpContext.IsCustomErrorEnabled))
{
Exception innerException = filterContext.Exception;
if ((new HttpException(null, innerException).GetHttpCode() == 500))
{
var viewName = "AllOtherExceptions";
if (typeof(DivideByZeroException).IsInstanceOfType(innerException))
{
viewName = "DivideByZeroException";
}
string controllerName = (string)filterContext.RouteData.Values["controller"];
string actionName = (string)filterContext.RouteData.Values["action"];
HandleErrorInfo model = new HandleErrorInfo(filterContext.Exception, controllerName, actionName);
ViewResult result = new ViewResult
{
ViewName = viewName,
ViewData = new ViewDataDictionary<HandleErrorInfo>(model),
TempData = filterContext.Controller.TempData
};
filterContext.Result = result;
filterContext.ExceptionHandled = true;
filterContext.HttpContext.Response.Clear();
filterContext.HttpContext.Response.StatusCode = 500;
filterContext.HttpContext.Response.TrySkipIisCustomErrors = true;
}
}
}
}
and then register it:
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new MyHandleErrorAttribute());
}
Do check fear's answer below. It's certainly the simpler, if it works.
Since it came after a few weeks, this is how my filter finally spelled out, using Darins response and incorporating Elmah-reporting, with code from this topic.
I still don't know why you can't set properties on a global action filter.
public class MyHandleErrorAttribute : FilterAttribute, IExceptionFilter
{
public void OnException(ExceptionContext filterContext)
{
if (!filterContext.IsChildAction &&
(!filterContext.ExceptionHandled && filterContext.HttpContext.IsCustomErrorEnabled))
{
var innerException = filterContext.Exception;
if ((new HttpException(null, innerException).GetHttpCode() == 500))
{
var viewName = "GeneralError";
if (typeof (HttpAntiForgeryException).IsInstanceOfType(innerException))
viewName = "SecurityError";
var controllerName = (string) filterContext.RouteData.Values["controller"];
var actionName = (string) filterContext.RouteData.Values["action"];
var model = new HandleErrorInfo(filterContext.Exception, controllerName, actionName);
var result = new ViewResult
{
ViewName = viewName,
ViewData = new ViewDataDictionary<HandleErrorInfo>(model),
TempData = filterContext.Controller.TempData
};
filterContext.Result = result;
filterContext.ExceptionHandled = true;
filterContext.HttpContext.Response.Clear();
filterContext.HttpContext.Response.StatusCode = 500;
filterContext.HttpContext.Response.TrySkipIisCustomErrors = true;
//From here on down, this is all code for Elmah-reporting.
var version = Assembly.GetExecutingAssembly().GetName().Version;
filterContext.Controller.ViewData["Version"] = version.ToString();
var e = filterContext.Exception;
if (!filterContext.ExceptionHandled // if unhandled, will be logged anyhow
|| RaiseErrorSignal(e) // prefer signaling, if possible
|| IsFiltered(filterContext)) // filtered?
return;
LogException(e);
}
}
}
private static bool RaiseErrorSignal(Exception e)
{
HttpContext context = HttpContext.Current;
if (context == null)
return false;
var signal = ErrorSignal.FromContext(context);
if (signal == null)
return false;
signal.Raise(e, context);
return true;
}
private static bool IsFiltered(ExceptionContext context)
{
var config = context.HttpContext.GetSection("elmah/errorFilter")
as ErrorFilterConfiguration;
if (config == null)
return false;
var testContext = new ErrorFilterModule.AssertionHelperContext(
context.Exception, HttpContext.Current);
return config.Assertion.Test(testContext);
}
private static void LogException(Exception e)
{
HttpContext context = HttpContext.Current;
ErrorLog.GetDefault(context).Log(new Error(e, context));
}
}

PartialViewResult is returning view with master page

I wrote my own HandleError attribute.
When an error occurs during an ajax request I want to return a partialview and when the request is non ajax a view with master page should be returned.
So far I wrote this
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
public class IcpHandleErrorAttribute : FilterAttribute, IExceptionFilter
{
private readonly Type _exceptionType = typeof(Exception);
public IcpHandleErrorAttribute()
{}
public void OnException(ExceptionContext filterContext)
{
if (filterContext == null)throw new ArgumentNullException("filterContext");
if (filterContext.IsChildAction)return;
if (filterContext.ExceptionHandled || !filterContext.HttpContext.IsCustomErrorEnabled)return;
Exception exception = filterContext.Exception;
if (new HttpException(null, exception).GetHttpCode() != 500)return;
if (!_exceptionType.IsInstanceOfType(exception))return;
var controllerName = (string)filterContext.RouteData.Values["controller"];
var actionName = (string)filterContext.RouteData.Values["action"];
var model = new HandleErrorInfo(filterContext.Exception, controllerName, actionName);
ViewResultBase result;
if (filterContext.HttpContext.Request.IsAjaxRequest())
{
result = new PartialViewResult { ViewName = "ErrorAjax" };
}
else
{
result = new ViewResult{ViewName = "Error"};
}
result.ViewData = new ViewDataDictionary<HandleErrorInfo>(model);
result.TempData = filterContext.Controller.TempData;
filterContext.ExceptionHandled = true;
filterContext.HttpContext.Response.Clear();
filterContext.HttpContext.Response.StatusCode = 500;
filterContext.HttpContext.Response.TrySkipIisCustomErrors = true;
}
}
But the masterpage is returned even when the PartialViewResult is assigned to the result.
Why is this happening ?
The ErrorAjax partial view
#model HandleErrorInfo
#{
Layout = null;
}
<div class="error">#Model.Exception.Message</div>
The action is called through jquery's post.
The action method is a test case
[HttpPost]
public ActionResult Create(ProjectCreateCommand command)
{
throw new NotImplementedException("ajax");
return Post(command);
}
The attribte registration in global.asax 's Application_Start() method.
GlobalFilters.Filters.Add(new IcpHandleErrorAttribute());
RegisterGlobalFilters(GlobalFilters.Filters);
You don't seem to be doing anything useful with your result local variable like for example assigning it to the filter context:
filterContext.Result = result;
If this not Ajax request you need redirect to some Action. And in this action return View()

Resources