Cleanly handling normal errors - asp.net-mvc

I use this pattern all over the place to grab data from the database and display a view:
public ActionResult Index(int? id)
{
RequestViewModel model;
model = this.ClientRepository.GetRequest(id);
return View("~/Views/Requests/Index.aspx", model);
}
If the repository returns null, which is the case if the record does not exist, then my page craps out and throws an error because the model is null.
I’d like to show a friendly “the requested record cannot be found” message instead of the yellow page of death or a generic “an error occurred” page.
What’s the recommended pattern to handle “normal” errors as opposed to unhandled exceptions?
Thanks,
Rick

You could write an action filter:
public class NullModelCheckerAttribute : ActionFilterAttribute
{
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
base.OnActionExecuted(filterContext);
var viewResult = filterContext.Result as ViewResultBase;
if (viewResult != null && viewResult.ViewData.Model == null)
{
// If the action selected a view to render and passed a null model
// render the NotFound.aspx view
var result = new ViewResult();
result.ViewName = "~/Views/Errors/NotFound.aspx";
filterContext.HttpContext.Response.StatusCode = 404;
filterContext.Result = result;
}
}
}
And then decorate your base controller (that all your controllers derive from) with this attribute:
[NullModelChecker]
public class BaseController: Controller
{ }
This way your current code stays untouched.
--
UPDATE:
In ASP.NET MVC 3 you could register your action filter globally without even decorating your base controller with it. Simply add the following to your Application_Start in Global.asax:
GlobalFilters.Filters.Add(new NullModelCheckerAttribute());

I'm not familiar with ASP.NET MVC. I'm familiar though with Spring MVC.
Why can't you just put a simple if-else condition? Like this one:
public ActionResult Index(int? id)
{
RequestViewModel model;
model = this.ClientRepository.GetRequest(id);
if (model == null) {
return View("~/Views/Requests/FriendlyError.aspx");
}
return View("~/Views/Requests/Index.aspx", model);
}

Related

Controller to Return an Error Message Instead of a View?

I'm fairly new to ASP.NET MVC and am not sure how best to handle the following situation.
A method in my controller needs to load some data based on an ID argument. Under normal circumstances, this ID argument will be set to a valid ID of an entity within my database. I construct some data and place it in ViewBag, which the view uses to render the page.
However, I would like some basic error handling just in case the ID argument is not valid. Although I could write a bunch of error handling code in the view, it would be much simpler not to display the view if there is a major misuse or malfunction of the site.
Is there a way the controller could simply return a "Item not found" string or something like that, and display that rather than the normal view? Or perhaps someone can suggest a better idea?
if (itemId == null)
{
return Content("Item not found");
}
Or if you want to return an HTTP 404 instead:
throw new HttpException(404, "Item Not Found");
In case fetching model from database (as described in Darin's answer) is somehow complicated and cannot be made generic, this is how I deal with resources not found.
Implement your base controller
public abstract class MyBaseController : Controller
{
[NonAction]
protected virtual void EnsureResourceFound(object resource)
{
if (resource == null)
{
HttpStatusCode statusCode = HttpStatusCode.NotFound;
throw new HttpException((int)statusCode, statusCode.ToString());
}
}
}
And in derived controllers - use that method
[HttpGet]
public virtual ActionResult Edit(int id)
{
SomeModel model = null;
EnsureResourceFound(model = _someModelService.Get(id));
return View(question);
}
And what will you do with resulting http exception, return custom view, log the error, depends on configured HandleErrorAttribute.
You can do it by simply throwing exception from the Controller.
You need to write following code and need to add ErrorController and it's respective view.
Global.asax
Exception exception = Server.GetLastError();
Response.Clear();
HttpException httpException = exception as HttpException;
//Add controller name
RouteData routeData = new RouteData();
routeData.Values.Add("controller", "Error");
//we will add controller's action name
routeData.Values.Add("action", "Index");
// Pass exception details to the target error View.
routeData.Values.Add("error", exception.Message);
// Clear the error on server.
Server.ClearError();
// Call target Controller and pass the routeData.
IController errorController = new ErrorController();
errorController.Execute(new RequestContext(new HttpContextWrapper(Context), routeData));
//and throw the exception from the Controller ny simply writing
throw new Exception()
ErrorController:
public class ErrorController : BaseController
{
#region Function Declaration
/// <summary>
/// Shows application error
/// </summary>
/// <param name="error">error description</param>
public ActionResult Index(string error)
{
ViewBag.Description = Resources.ErrorMessage.GeneralError;
if (string.IsNullOrEmpty(error))
{
ViewBag.DetailError = error;
}
else
{
ViewBag.DetailError = string.Empty;
}
return View("ErrorIndex");
}
#endregion
}
Another Approach :
If you want to write a view for a particular error than you have to write followig code. You have to just add DivByZero view.
[HandleError(View = "DivByZero", ExceptionType = typeof(System.DivideByZeroException))]
public ActionResult About()
{
List<Alok> alokList = new List<Alok>();
var al = from aa in alokList.Distinct()
select aa;
ViewData["Errorname"] = "Divide By Zero Exception";
ViewBag.ErrorName = "Divide By Zero Exception";
//throw new DivideByZeroException();
return View();
}
DivByZero View :
#model System.Web.Mvc.HandleErrorInfo
#{
ViewBag.Title = "Error";
Layout = "~/Views/Shared/_Layout.cshtml";
}
#*<h2>
#ViewData["Errorname"].ToString()
#ViewBag.ErrorName</h2>*#
<p>
Controller : #Model.ControllerName
</p>
<p>
Action : #Model.ActionName
</p>
<p>
Error Message : #Model.Exception
</p>
public ActionResult Foo(int id)
{
MyModel model = ...
if (model == null)
{
return HttpNotFound();
}
return View(model);
}
And since writing this same code over and over again in your actions could quickly become boring a better solution is to write a custom model binder which will fetch the model from the database and if not found will simply throw a new HttpException and set the status code to 404. Then your controller action will simply look like this:
public ActionResult Foo(MyModel model)
{
return View(model);
}
and the model binder itself:
public class MyModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var id = bindingContext.ValueProvider.GetValue("id");
if (id == null)
{
throw new HttpException(404, "Not found");
}
MyModel model = FetchTheModel(id.AttemptedValue);
if (model == null)
{
throw new HttpException(404, "Not found");
}
return model;
}
private MyModel FetchTheModel(string id)
{
throw new NotImplementedException();
}
}
This model binder could obviously be made more generic.

Changing the view in an ASP.NET MVC Filter

I want to redirect the user to a different view if they are using a mobile browser. I've decided I'd like to do this using MVC filters by applying it to actions which I want to have a mobile view.
I believe this redirect needs to happen in OnActionExecuted, however the filterContext does not contain information on the view - it does, however in OnResultExecuted, but by this time I believe it is too late to change the view.
How can I intercept the view name and change the ViewResult?
This is what I have in the result executed and what I'd like to have work in Action Executed.
public class MobilePageFilter : ActionFilterAttribute
{
public override void OnResultExecuted(ResultExecutedContext filterContext)
{
if(filterContext.Result is ViewResult)
{
if (isMobileSite(filterContext.HttpContext.Session[SetMobile.SESSION_USE_MOBILE]))
{
ViewResult viewResult = (ViewResult)filterContext.Result;
string viewName = viewResult.ViewName;
filterContext.Result = new ViewResult
{
ViewName = "Mobile/" + viewName,
ViewData = viewResult.ViewData,
TempData = viewResult.TempData
};
}
}
base.OnResultExecuted(filterContext);
}
}
I would recommend you the following blog post which explains a better alternative to achieve what you are asking for rather than using action filters.
This is what I ended up doing, and wrapped up into a reusable attribute and the great thing is it retains the original URL while redirecting (or applying whatever result you wish) based on your requirements:
public class AuthoriseSiteAccessAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
base.OnActionExecuting(filterContext);
// Perform your condition, or straight result assignment here.
// For me I had to test the existance of a cookie.
if (yourConditionHere)
filterContext.Result = new SiteAccessDeniedResult();
}
}
public class SiteAccessDeniedResult : ViewResult
{
public SiteAccessDeniedResult()
{
ViewName = "~/Views/SiteAccess/Login.cshtml";
}
}
Then just add the attribute [SiteAccessAuthorise] to your controllers you wish to apply the authorisation access to (in my case) or add it to a BaseController. Make sure though the action you are redirecting to's underlying controller does not have the attribute though, or you'll be caught in an endless loop!

How can I shared controller logic in ASP.NET MVC for 2 controllers, where they are overriden

I am trying to implement user-friendly URLS, while keeping the existing routes, and was able to do so using the ActionName tag on top of my controller (Can you overload controller methods in ASP.NET MVC?)
I have 2 controllers:
ActionName("UserFriendlyProjectIndex")]
public ActionResult Index(string projectName) { ... }
public ActionResult Index(long id) { ... }
Basically, what I am trying to do is I store the user-friendly URL in the database for each project.
If the user enters the URL /Project/TopSecretProject/, the action UserFriendlyProjectIndex gets called. I do a database lookup and if everything checks out, I want to apply the exact same logic that is used in the Index action.
I am basically trying to avoid writing duplicate code. I know I can separate the common logic into another method, but I wanted to see if there is a built-in way of doing this in ASP.NET MVC.
Any suggestions?
I tried the following and I go the View could not be found error message:
[ActionName("UserFriendlyProjectIndex")]
public ActionResult Index(string projectName)
{
var filteredProjectName = projectName.EscapeString().Trim();
if (string.IsNullOrEmpty(filteredProjectName))
return RedirectToAction("PageNotFound", "Error");
using (var db = new PIMPEntities())
{
var project = db.Project.Where(p => p.UserFriendlyUrl == filteredProjectName).FirstOrDefault();
if (project == null)
return RedirectToAction("PageNotFound", "Error");
return View(Index(project.ProjectId));
}
}
Here's the error message:
The view 'UserFriendlyProjectIndex' or its master could not be found. The following locations were searched:
~/Views/Project/UserFriendlyProjectIndex.aspx
~/Views/Project/UserFriendlyProjectIndex.ascx
~/Views/Shared/UserFriendlyProjectIndex.aspx
~/Views/Shared/UserFriendlyProjectIndex.ascx
Project\UserFriendlyProjectIndex.spark
Shared\UserFriendlyProjectIndex.spark
I am using the SparkViewEngine as the view engine and LINQ-to-Entities, if that helps.
thank you!
Just as an addition this this, it might pay to optimize it to only hit the database once for the project...
ActionName("UserFriendlyProjectIndex")]
public ActionResult Index(string projectName)
{
//...
//var project = ...;
return IndexView(project);
}
public ActionResult Index(long id)
{
//...
//var project = ...;
return IndexView(project);
}
private ViewResult IndexView(Project project)
{
//...
return View("Index", project);
}
Sorry, it looks like I am answering my own question!
I returned the call to Index controller inside my "wrapper" controller and then I specified the view name in the Index controller.
ActionName("UserFriendlyProjectIndex")]
public ActionResult Index(string projectName)
{
//...
//var project = ...;
return Index(project.ProjectId);
}
public ActionResult Index(long id)
{
//...
return View("Index", project);
}

Return View from ActionFilter

I have an ActionFilter that checks if a parameter in the URL is valid.
If it is not valid I have to render a View. I dont want to redirect, because I still need the ActionExecutingContext.
Can that be done?
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
Guid processIdentifier = (Guid)filterContext.RouteData.Values["processIdentifier"];
//if processIdentifier not found render a view with message and some other objects in ViewData
filterContext.Controller.ViewData.ModelState.AddModelError("WrongProcessIdentifier", "The process-id you supplied is not valid");
base.OnActionExecuting(filterContext);
}
HandleErrorAttribute had what I was looking for.
filterContext.Result = new ViewResult
{
ViewName = "MessagePage",
ViewData = filterContext.Controller.ViewData,
TempData = filterContext.Controller.TempData
};
Yes. Look at the source for HandleErrorAttribute.
Try this
[HandleError]
public ActionResult MyAction (int id)
{
// ...
}
And put the view you want rendered in to ~/Views/Shared/Error.ascx.

ASP.NET MVC: Not executing actionfilters on redirect and throw HttpException

I've created an OnActionExecuted filter to populate some viewmodel attributes with data from db (I'm not using ViewData["somekey"], preferring many ViewModels descending from a common ancestor).
public class BaseController : Controller
{
protected DataClassesDataContext context = new DataClassesDataContext();
protected override void OnActionExecuted(ActionExecutedContext filterContext)
{
ViewModel model = (ViewModel) ViewData.Model;
model.IsUserAuthenticated = filterContext.HttpContext.User.Identity.IsAuthenticated;
if (model.IsUserAuthenticated)
{
model.UserName = filterContext.HttpContext.User.Identity.Name;
}
model.CommonAttribute = from c in context.Something select new SomethingElse() {...};
}
}
The problem is that when an action results in a redirect or a 404 error, OnActionExecuted tries to access ViewModel, which has not been initialized. Also, it's completely useless to fill those values, as they will not be used, since another action is going to be called.
How can I avoid filling viewodel on redirect?
A trivial solution would be to not fill in the model when it doesn't exist:
ViewModel model = ViewData.Model as ViewModel;
if (model != null)
{
model.IsUserAuthenticated = filterContext.HttpContext.User.Identity.IsAuthenticated;
if (model.IsUserAuthenticated)
{
model.UserName = filterContext.HttpContext.User.Identity.Name;
}
model.CommonAttribute = from c in context.Something select new SomethingElse() {...};
}

Resources