How to return different view, but presererve ViewModel in OnActionExecuting - asp.net-mvc

I'm trying to return a different view if a condition is met. I want to preserve the Model passed into the view from the action.
protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
var subAction = filterContext.RequestContext.RouteData.Values["subaction"].ToString();
var action = filterContext.RequestContext.RouteData.Values["action"].ToString();
if (!string.IsNullOrEmpty(subAction))
{
var view = (ViewResultBase) (filterContext.Result);
filterContext.Result = View(action + subAction, view.ViewData.Model);
}
base.OnActionExecuting(filterContext);
}
When doing it this way I get an "Object reference not set to an instance of an object.". Obviously, the Model haven't been set?
The reason I'm doing it this way, is that I want to keep the naming of the view as simple as possible. And my URL looks like this: /Global/Modules/Admin/Users/Create. That would return the view "UsersCreate". Which works. But the Model is either empty or null!
UPDATE
Actually. It just hit me. The behaviour is correct. Because I'm only returning a View. The action is never executed. The Users-action gets executed, but returns the view UsersCreate. How can I do a RedirectToAction kinda thing, without actually redirect.

OnActionExecuting is run before the action so no model exists yet. You may try with OnActionExecuted which is called once the action returns an ActionResult.

public override void OnActionExecuting(ActionExecutingContext filterContext)
{
...
if (needToRedirect)
{
...
filterContext.Result = new RedirectResult(url);
return;
}
...
}

Related

OnActionExecuting fires multiple times

I'm not sure if this is the correct way to go about the problem I need to solve... however in an OnActionExecuting action filter that I have created, I set a cookie with various values. One of these values is used to determine whether the user is visiting the website for the very first time. If they are a new visitor then I set the ViewBag with some data so that I can display this within my view.
The problem I have is that in some of my controller actions I perform a RedirectToAction. The result is OnActionExecuting is fired twice, once for the original action and then a second time when it fires the new action.
<HttpGet()>
Function Index(ByVal PageID As String) As ActionResult
Dim wo As WebPage = Nothing
Try
wp = WebPages.GetWebPage(PageID)
Catch sqlex As SqlException
Throw
Catch ex As Exception
Return RedirectToAction("Index", New With {.PageID = "Home"})
End If
End Try
Return View("WebPage", wp)
End Function
This is a typical example. I have a data driven website that gets a webpage from the database based on the PageID specified. If the page cannot be found in the database I redirect the user to the home page.
Is it possible to prevent the double firing in anyway or is there a better way to set a cookie? The action filter is used on multiple controllers.
Had the same issue. Resolved by overriding property AllowMultiple:
public override bool AllowMultiple { get { return false; } }
public override void OnActionExecuting(HttpActionContext actionContext)
{
//your logic here
base.OnActionExecuting(actionContext);
}
You can save some flag value into TempData collection of controller on first executing and if this value presented, skip filter logic:
if (filterContext.Controller.TempData["MyActionFilterAttribute_OnActionExecuting"] == null)
{
filterContext.Controller.TempData["MyActionFilterAttribute_OnActionExecuting"] = true;
}
You could return the actual action instead of redirecting to the new action. That way, you dont cause an http-request, thereby not triggering the onactionexecuting (i believe)
Old question, but I just dealt with this so I thought I'd throw in my answer. After some investigating I disovered this was only happening on endpoints that returned a view (i.e. return View()). The only endpoints that had multiple OnActionExecuting fired were HTML views that were composed of partial views (i.e. return PartialView(...)), so a single request was "executing" multiple times.
I was applying my ActionFilterAttribute globally to all endpoints, which was working correctly on all other endpoints except for the view endpoints I just described. The solution was to create an additional attribute applied conditionally to the partial view endpoints.
// Used specifically to ignore the GlobalFilterAttribute filter on an endpoint
public class IgnoreGlobalFilterAttribute : Attribute { }
public class GlobalFilterAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
// Does not apply to endpoints decorated with Ignore attribute
if (!filterContext.ActionDescriptor.GetCustomAttributes(typeof(IgnoreGlobalFilterAttribute), false).Any())
{
// ... attribute logic here
}
}
}
And then on my partial view endpoints
[HttpGet]
[AllowAnonymous]
[IgnoreGlobalFilter] //HERE this keeps the attribute from firing again
public ActionResult GetPartialView()
{
// partial view logic
return PartialView();
}

How to access data after redirect

Again there are multiple articles which says how to access data after redirect. but doesn't serves my purpose.
I am having errorcontroller which is having index action method and error index view.
If there is any error in the application it will caught in Application_Error event.
inside Application_Error event I had logged the error and redirected to Error Index page like this -
protected new void Application_Error(object sender, EventArgs e)
{
Exception error = Server.GetLastError();
log.error(error.Message);
HttpContext.Current.Response.Redirect("~/Error/Index");
}
Now in the error index view, I would like to display the error message. What should I do in Application_Error event which can be access by Error Index view?
Updated : I don't want to use Session as session object may not be available in Application_Error event. this is dependent on when the error occurred.
Approach - 1
As per my knowledge you can use TempData to store the posted data. It is like a DataReader Class, once read, Data will be lost. So that stored data in TempData will become null.
var Value = TempData["keyName"] //Once read, data will be lost
So to persist the data even after the data is read you can Alive it like below
var Value = TempData["keyName"];
TempData.Keep(); //Data will not be lost for all Keys
TempData.Keep("keyName"); //Data will not be lost for this Key
TempData works in new Tabs/Windows also, like Session variable does.
You could use Session Variable also, Only major problem is that Session Variable are very heavy comparing with TempData. Finally you are able to keep the data across Controllers/Area also.
Approach - 2
This works for me. This is very easy and no need to consider any change in Web.Config or Register the Action Filter in Global.asax file.
ok. So, First I am creating a simple Action Filter. This will handle Ajax and Non Ajax requests.
public class MyCustomErrorAttribute : HandleErrorAttribute
{
public override void OnException(ExceptionContext filterContext)
{
filterContext.ExceptionHandled = true;
var debugModeMsg = filterContext.HttpContext.IsDebuggingEnabled
? filterContext.Exception.Message +
"\n" +
filterContext.Exception.StackTrace
: "Your error message";
//This is the case when you need to handle Ajax requests
if (filterContext.HttpContext.Request.IsAjaxRequest())
{
filterContext.Result = new JsonResult
{
JsonRequestBehavior = JsonRequestBehavior.AllowGet,
Data = new
{
error = true,
message = debugModeMsg
}
};
}
//This is the case when you handle Non Ajax request
else
{
var routeData = new RouteData();
routeData.Values["controller"] = "Error";
routeData.Values["action"] = "Error";
routeData.DataTokens["area"] = "app";
routeData.Values["exception"] = debugModeMsg;
IController errorsController = new ErrorController();
var exception = HttpContext.Current.Server.GetLastError();
var httpException = exception as HttpException;
if (httpException != null)
{
Response.StatusCode = httpException.GetHttpCode();
switch (System.Web.HttpContext.Current.Response.StatusCode)
{
case 504:
routeData.Values["action"] = "Http404";
break;
}
}
var rc = new RequestContext
(
new HttpContextWrapper(HttpContext.Current),
routeData
);
errorsController.Execute(rc);
}
base.OnException(filterContext);
}
}
Now you can implement this Action Filter on Controller as well as on the Action only.Example:
I am going little off topic. I thought this is bit important to explain.
If you pay attention to the above highlighted part. I have specified the order of the Action Filter. This basically describes the order of execution of Action Filter. This is a situation when you have multiple Action Filters implemented over Controller/Action Method
This picture just indicates that let's say you have two Action Filters. OnActionExecution will start to execute on Priority and OnActionExecuted will start from bottom to Top. That means in case of OnActionExecuted Action Filter having highest order will execute first and in case of OnActionExecuting Action Filter having lowest order will execute first. Example below.
public class Filter1 : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
//Execution will start here - 1
base.OnActionExecuting(filterContext);
}
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
//Execution will move here - 5
base.OnActionExecuted(filterContext);
}
}
public class Filter2 : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
//Execution will move here - 2
base.OnActionExecuting(filterContext);
}
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
//Execution will move here - 4
base.OnActionExecuted(filterContext);
}
}
[HandleError]
public class HomeController : Controller
{
[Filter1(Order = 1)]
[Filter2(Order = 2)]
public ActionResult Index()
{
//Execution will move here - 3
ViewData["Message"] = "Welcome to ASP.NET MVC!";
return View();
}
}
You may already aware that there are different types of filters within MVC framework. They are listed below.
Authorization filters
Action filters
Response/Result filters
Exception filters
Within each filter, you can specify the Order property. This basically describes the order of execution of the Action Filters.
Use TempData for getting value.
Some feature about TempData
TempData is a dictionary object that is derived from TempDataDictionary class and stored in short lives session.
TempData is used to pass data from current request to subsequent request means incase of redirection.
It’s life is very short and lies only till the target view is fully loaded.
It’s required typecasting for complex data type and check for null values to avoid error.
It is used to store only one time messages like error messages, validation messages.

How to use HandleError with model state errors

I want to use a custom action filter to handle specific exceptions from my service classes to populate the model state and then return the view.
For example, take my previous code:
public ActionResult SomeAction(SomeViewModel model)
{
try
{
_someService.SomeMethod(model);
}
catch (ServiceException ex)
{
ModelState.AddModelError(ex.Key, ex.ErrorMessage);
}
return View();
}
Basically, it would call a service, and if a ServiceException was thrown, it would know that there was an issue w/ the model data, and add the error to the ModelState, then just return the view. But I noticed some very un-DRY-like patterns, because I had this same try/catch code in every action method.
So, to DRY it up a bit, I basically created a new HandleServiceError action filter:
public class HandleServiceErrorAttribute : HandleErrorAttribute
{
public override void OnException(ExceptionContext context)
{
((Controller)context.Controller)
.ModelState
.AddModelError(
((ServiceException)context.Exception).Key,
((ServiceException)context.Exception).ErrorMessage
);
context.ExceptionHandled = true;
}
}
Then simplified my action methods like so:
public ActionResult SomeAction(SomeViewModel model)
{
_someService.SomeMethod(model);
return View();
}
Problem is, once the action filter handles the error, it doesn't return to my action method. I sort of understand, under the hood, why this is happening. But I would still like to figure out a way to do what I'm trying to do.
Is this possible?
Thanks in advance.
UPDATE:
I tried the suggestions from the article Darin provided in his answer, but ran into issues trying to use constructor injection with the controller's model state.
For example, if you look at their Controllers\ProductController.cs code, they have the controller's empty constructor using a service locator to create the service, passing in the controller's ModelState at that point:
public ProductController()
{
_service = new ProductService(new ModelStateWrapper(this.ModelState),
new ProductRepository());
}
But if you look at the injected constructor, it assumes the ModelState will be injected into the constructor for the service:
public ProductController(IProductService service)
{
_service = service;
}
I don't know how to get CI to work with the current controller's ModelState. If I could figure this out, then this approach may work.
You could still return the corresponding view:
context.Result = new ViewResult
{
ViewName = context.RouteData.GetRequiredString("action")
};
You may also take a look at the following article for an alternative about how to perform validation at the service layer.

repeat common error info logic in ActionFilterAttribute

I'm implementing a web API using the REST for ASP.NET MVC framework (MVC 2). I want to encapsulate this code, ideally in an ActionFilterAttribute (?), so that I can decorate specific actions that always perform this same logic:
if (!ModelState.IsValid) {
return View(
new GenericResultModel(){ HasError=True, ErrorMessage="Model is invalid."});
}
I really don't want to have to copy and paste this boilerplate code into every controller action where I need to do this.
In this web API scenario, I need to be able to do something like this so that the caller can get a result back in JSON or POX form and see that there is an error. In an ASPX view, obviously I wouldn't need something like this as the validation controls would take care of notifying the user of a problem. But I do not have an ASPX view - I am only returning JSON or POX data serialized from my model.
I've started with this code in an ActionFilter but am not sure what to do next (or if it is even the right starting point):
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
bool result = filterContext.Controller.ViewData.ModelState.IsValid;
if (!result)
{
GenericResultModel m = new GenericResultModel() { HasError = true };
// return View(m)
// ?????
}
base.OnActionExecuting(filterContext);
}
How do I accomplish this?
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
// Notice that the controller action hasn't been called yet, so
// don't expect ModelState.IsValid=false here if you have
// ModelState.AddModelError inside your controller action
// (you shouldn't be doing validation in your controller action anyway)
bool result = filterContext.Controller.ViewData.ModelState.IsValid;
if (!result)
{
// the model that resulted from model binding is not valid
// => prepare a ViewResult using the model to return
var result = new ViewResult();
result.ViewData.Model = new GenericResultModel() { HasError = true };
filterContext.Result = result;
}
else
{
// call the action method only if the model is valid after binding
base.OnActionExecuting(filterContext);
}
}

Inject referrer action via action filter?

Is there a way to inject the referrer action from an action filter?
Lets say I have a view that comes from action X. In dies view I call action Y and I want to redirect again to action X. (There are multiple X actions that call action Y). I thought that it could be nice if I had a parameter call referrerAction and an action filter that filled it with the previous action. Is it possible?
Thanks.
Here's how I do:
public class ReturnPointAttribute : Attribute
{
}
public class BaseController: Controller
{
private string returnPointUrl = null;
protected override void OnActionExecuted(ActionExecutedContext filterContext)
{
base.OnActionExecuted(filterContext);
if (filterContext.ActionDescriptor.IsDefined(typeof(ReturnPointAttribute), true))
returnPointUrl = filterContext.HttpContext.Request.Url.ToString();
}
public ActionResult RedirectOrReturn<T>(Expression<Action<T>> action) where T : BaseController
{
return returnPointUrl.IsNullOrEmpty()
? MyControllerExtensions.RedirectToAction(this, action)
: (ActionResult)Redirect(returnPointUrl);
}
}
Now, you mark you X actions with [ReturnPoint] and call RedirectOrReturn() if you want to return back.
I do not use UrlReferrer because it can be wrong and I have no control over its value. With ReturnPoint, you can also have groups, e.g. [ReturnPoint("Orders")] and RedirectOrReturn("Orders").
Of course, you can have more automatic behaviour in OnActionExecuted - e.g. it can check if returned result is Redirect, and automatically go to ReturnPoint if it has value. Or you can control this with [ReturnPoint(Automatic=true)], and so on.

Resources