Get route data from within IResultFilter.OnResultExecuted - asp.net-mvc

When a view has been rendered in memory and before it is sent as a response to the client, I would like to intercept the call, check which view is being rendered, what was the action and controller, and do some house-keeping.
Therefore, I am implementing a ResultFilter and overriding the OnResultExecuted method.
Within this method, how do I get the route data to figure out which view, action, controller were called?
Update
My profuse apologies. I just looked up ResultExecutedContext in reflector and it showed me only an Exception, Cancelled and ActionResult property. It didn't show me any RouteData. When I fired up the IDE, it did show me the route data. I feel like a dick for asking this question.

You could get it from the filterContext's RouteData property:
public override void OnResultExecuted(ResultExecutedContext filterContext)
{
RouteData rd = filterContext.RouteData;
// read from the current request RouteData the information
// you were looking for. For example to get the current controller
// and action:
string currentController = rd.GetRequiredString("controller");
string currentAction = rd.GetRequiredString("action");
}
As far as which view was rendered is concerned you could retrieve this information from the Result property:
var viewResult = filterContext.Result as ViewResultBase;
if (viewResult != null)
{
// the controller action returned a view result (either a ViewResult or PartialViewResult)
// so we could retrieve the view name here:
string viewName = viewResult.ViewName;
}
If on the other hand the controller action returned a JsonResult you could also extract retrieve it:
var jsonResult = filterContext.Result as JsonResult;
and so on...

ResultExecutedContext has a RouteData property which should give you what you need

Related

How to set viewbag in AuthorizedAttribute?

I use MVC 4 and have moved some logic into an authorize filter. I am trying redirect to an error page based on not being authorized. I would like to set the last page route and a few other properties to catch the error.
Below is my override
// handle unauthorized
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
filterContext.Controller.ViewBag.LastRoute = filterContext.RouteData;
filterContext.Controller.ViewBag.Issue = "put...val here";
var routeValues = new RouteValueDictionary(new
{
controller = "Error",
action = "Oops"
});
filterContext.Result = new RedirectToRouteResult(routeValues);
}
controller
[AllowAnonymous]
public ActionResult Oops()
{
var m = new Core.Models.ErrorModel();
var v = ViewBag.Issue; // == null
return View("~/Views/Error/Oops.cshtml", m);
}
I tried how to set values to viewbag in actionfilterattribute asp mvc 5 for action filters and it works
Any help would be appreciated.
EDIT:
Sorry when I get to the controller the value for:
ViewBag.Issue = null.
I'm not sure how to set the property and have it hold value.
RedirectToRouteResult is going to send a redirect response to the browser and browser will issue a brand new GET request to the url specified. ViewBag data do not survive between 2 http requests.
You may use TempData which will keep the data between 2 seperate http requests. You can set your TempData value in one action method and any action method that is called after this can get values from the TempData object and then use it. TempData use Session behind the scene to store data. The value of TempData persists until it is read or until the session times out. This is ideal for scenarios such as redirection because the values in TempData are available beyond a single request.
So in your action filter you can set the TempData dictionary instead of ViewBag.
filterContext.Controller.TempData["Issue"] = "Robots are laughing non stop";
var routeValues = new RouteValueDictionary(new
{
controller = "Home",
action = "Oops"
});
filterContext.Result = new RedirectToRouteResult(routeValues);
Now in your Oops action method, you may read the TempData value you set
public ActionResult Oops()
{
var issueDetails = TempData["Issue"];
// TO DO : Do something useful with issueDetails :)
return View();
}
Keep in mind that TempData values won't be available after you read it. So if you want to read it in your view again, set it again or better use a view model and set the already read value as the property value of your view model.

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.

post object to MVC controller using HttpWebRequest, WebClient etc

I have this api controller action that takes an object "ContentList" as parameter.
[HttpPost]
public List<string> SendList(string param, ContentList list)
{
List<string> testReturn = new List<string> { "test1", "test2", "test3", "test4" };
return testReturn ;
}
What I have tried so far is to call a controller action like this:
Uri _uri = new Uri("http://localhost:xxxxx/api/FakeTest/SendList?param=test");
var serializer = new JavaScriptSerializer();
string requestData = serializer.Serialize(new
{
list = ContentList,
});
using (var client = new WebClient())
{
client.Headers[HttpRequestHeader.ContentType] = "application/json";
var result = client.UploadData(_uri, Encoding.UTF8.GetBytes(requestData));
var tempString = Encoding.UTF8.GetString(result);
}
In this example, tempString = ["test1","test2","test3","test4"] as reurned by the controller action..
In the controller action, I can access the properties of the passed in ContentList, and return their values (changing the actions return value accordingly ofcourse).
However, in the controller action, I need to send off the ContentList object for further processing, and this seems to fail. I get a 500 internal server error, and I can't put a breakpoint in the controller to follow the values passed in. The debugger never hits it...
I expect this has something to do with sending json to the controller action.
Anyway, it seems that the ContentList is rejected by the code it is sent to from the controller action, so I figure I need to do some sort of de-serializing, right?
Bottomline, the question is, what is the correct way to call a controller action from code, pass in a C# object, and make it usable from the controller action?
If you are using MVC 3 your controller should be able to reveive and parse json data in a direct way. If you are using MVC 2 you'll need to register a new factory on your application to take care of json parsing on the controller
protected void Application_Start()
{
RegisterRoutes(RouteTable.Routes);
ValueProviderFactories.Factories.Add(new JsonValueProviderFactory());
}
More info on the subject here:
http://haacked.com/archive/2010/04/15/sending-json-to-an-asp-net-mvc-action-method-argument.aspx

How to return different view, but presererve ViewModel in OnActionExecuting

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

Unit testing my controller method results in an empty ViewName?

I'm doing some simple MS unit tests on my standard, nothing special controller.
When I check the ViewName proprty, from the returned ViewResult object, it's "" (empty).
I'm under the impression that the ViewName is implied by the name of the View (as suggested by this MS article on ASP.NET MVC controller testing).
BTW, when I test the ViewData, it's all there and correct.
Here's the code I have...
public ActionResult Index(int? page, string tag)
{
if (page == null || page <= 0)
{
page = 1;
}
var viewData = new IndexViewData
{
... my property setters, etc ...
};
return View(viewData);
}
[TestMethod]
public void Index_Action_Should_Return_Index_View_For_Default_HomePage()
{
// Arrange.
var controller = PostController; // Wrapper, cause I use D.I.
// Act.
ViewResult viewResult = controller.Index(null, null) as ViewResult;
// Assert.
Assert.IsNotNull(viewResult);
Assert.AreEqual("Index", viewResult.ViewName); // This is false/fails.
var indexViewData = viewResult.ViewData.Model as IndexViewData;
Assert.IsNotNull(indexViewData); // This is true.
}
The ViewName is only present when you set it in the ViewResult. If your View name matches your controller name, then I would check to ensure that the ViewName is null or empty as that would be (IMO) the correct behavior since you wouldn't want to set a name on the view. I only check that the ViewName is set when I intend that the View to be returned does not match the action -- say, when returning the "Error" view, for example.
EDIT: The following is the source for ExecuteResult in ViewResultBase.cs (from RC1, I don't have the source for RTW on my Macintosh). As you can see it checks to see if the ViewName has been set directly and if not, it pulls it from the action in the controller context's route data. This only happens in ExecuteResult, which is invoked AFTER your controller's action has completed.
public override void ExecuteResult(ControllerContext context) {
if (context == null) {
throw new ArgumentNullException("context");
}
if (String.IsNullOrEmpty(ViewName)) {
ViewName = context.RouteData.GetRequiredString("action");
}
ViewEngineResult result = null;
if (View == null) {
result = FindView(context);
View = result.View;
}
ViewContext viewContext = new ViewContext(context, View, ViewData, TempData);
View.Render(viewContext, context.HttpContext.Response.Output);
if (result != null) {
result.ViewEngine.ReleaseView(context, View);
}
}
I personally found the testing facilities provided by MVC2 to be somewhat clumsy. I'm guessing there is something better already extant, but I ended up creating a simple class to test actions. I modeled the interface (the implementation is another story) on a class provided by the excellent open source Java MVC framework Stripes called MockRoundTrip.
Here is the method used to get the action destination page when testing actions, called getTripDestination(). It returns the correct result irrespective of whether the viewname is explicitly set or not
//Get the destination page of the request, using Runtime execution pattern of MVC, namely
//if no ViewName is explicitly set in controller, ViewResult will have an empty ViewName
//Instead, current action name will be used in its place
public string getTripDestination()
{
RouteData routeData = getRouteData();
ViewResult viewResult = (result is ViewResult) ? (ViewResult)result : null;
string tripDestination = (viewResult != null) ? viewResult.ViewName : "";
return (tripDestination != "") ? tripDestination : (String)routeData.Values["action"];
}
private RouteData getRouteData()
{
HttpContextBase context = controller.ControllerContext.RequestContext.HttpContext;
return RouteTable.Routes.GetRouteData(context);
}
The viewname is set automatically by the framework. But when we unit test, we short-circuit the framework and there is nothing left to set the name.
So our actions need to set the viewname explicitly when we unit test. We could also check for null or empty if we really, really want to lean on the convention.
The documentation for Controller.View() states:
This method overload of the View class returns a ViewResult object
that has an empty ViewName property. If you are writing unit tests for
controller actions, take into account the empty ViewName property for
unit tests that do not take a string view name.
At run time, if the ViewName property is empty, the current action
name is used in place of the ViewName property.
So when expecting a view with the same name as the current action we can just test that it's an empty string.
Alternatively, the Controller.View(String) method will set the ViewName.

Resources