How do I Redirect to another Action/Controller in MVC3 from a VOID method? - asp.net-mvc

I have a controller method that returns a void because it is building an Excel report for the user to download. The Excel 3rd party library we're using is writing to the response itself. The method looks something like this:
[HttpGet]
public void GetExcel(int id)
{
try
{
var report = _reportService.GetReport(id);
var table = _reportService.GetReportTable(id);
var excelReport = new ExcelReport(table, report.Name);
excelReport.DownloadReport(System.Web.HttpContext.Current.Response);
}
catch (Exception ex)
{
// This is wrong, of course, because I'm not returning an ActionResult
Response.RedirectToRoute("/Report/Error/", new { exceptionType = ex.GetType().Name });
}
}
There are several security checks in place that throw exceptions if the user doesn't meet certain credentials for fetching the report. I want to redirect to a different page and pass along some information about the exception, but I can't figure out how to do this in MVC3....
Any ideas?

You could use the following code
Response.Redirect(Url.Action("Error", "Report", new { exceptionType = ex.GetType().Name }));
But have you taken a look at the FilePathResult or FileStreamResult ?

Instead of letting the 3rd part library write to the response directly get the content use regular ActionResult and return File(...) for the actual file or RedirectToAction(...) (or RedirectToRoute(...)) on error. If your 3rd party library can only write to Response you may need to use some tricks to capture it's output.
[HttpGet]
public ActionResult GetExcel(int id)
{
try
{
var report = _reportService.GetReport(id);
var table = _reportService.GetReportTable(id);
var excelReport = new ExcelReport(table, report.Name);
var content = excelReport.MakeReport(System.Web.HttpContext.Current.Response);
return File(content, "application/xls", "something.xls");
}
catch (Exception ex)
{
RedirectToRoute("/Report/Error/", new { exceptionType = ex.GetType().Name });
}
}

You can return an EmptyActionResult:
[HttpGet]
public ActionResult GetExcel(int id)
{
try
{
var report = _reportService.GetReport(id);
var table = _reportService.GetReportTable(id);
var excelReport = new ExcelReport(table, report.Name);
excelReport.DownloadReport(System.Web.HttpContext.Current.Response);
return new EmptyResult();
}
catch (Exception ex)
{
return RedirectToAction("Error", "Report", rnew { exceptionType = ex.GetType().Name });
}
}
Not sure if it works, haven't tested it.

Another approach would be using an exception filter:
public class MyExceptionFilter : FilterAttribute, IExceptionFilter
{
public void OnException(ExceptionContext filterContext)
{
var routeValues = new RouteValueDictionary()
{
{ "controller", "Error" },
{ "action", "Report" }
};
filterContext.Result = new RedirectToRouteResult(routeValues);
filterContext.ExceptionHandled = true;
// Or I can skip the redirection and render a whole new view
//filterContext.Result = new ViewResult()
//{
// ViewName = "Error"
// //..
//};
}
}

Related

Return shared view from ViewComponent

I have an Error-view which should be loaded if an Exception occurs. The Error view is located in:
Views/Shared/Error.cshtml (See attached picture).
In my Controller, my try-and catch looks like this:
public IActionResult Device(string id, bool like, int type)
{
try
{
//code
return View(viewModel);
}
catch (Exception exe)
{
return View("Error", exe);
}
}
This works and the correct Error View is displayed. However, I have a ViewComponent which should display the same Error-view.
I have tried the following:
1) Copy the Error-file and pasted it in the same folder as my ViewComponent (Right beneath the Default view). This does not give me an error, but the Default-view is the one being loaded.
2) I have returned the Error view from the shared-folder in the following way:
return View("../../../Shared/Error");
This as well does not give errors, but the Default view is the one being loaded.
Any ideas on how to solve this?
EDIT
So far I have created a new class
public class HandleExceptionAttribute : ExceptionFilterAttribute
{
public override void OnException(ExceptionContext context)
{
var result = new ViewResult { ViewName = "Error" };
var modelMetadata = new EmptyModelMetadataProvider();
result.ViewData = new ViewDataDictionary(
modelMetadata, context.ModelState);
result.ViewData.Add("HandleException",
context.Exception);
context.Result = result;
context.ExceptionHandled = true;
}
}
And in my Error View I added this:
#{
ViewData["Title"] = "Error";
Layout = "_LayoutCustomer";
Exception ex = ViewData["HandleException"] as Exception;
}
And lastly, I added [HandleException] on top of my Controller:
[HandleException]
public class CustomerController : Controller
{
//All the actions...
}
To simulate a new Exception, I use:
public IActionResult Device(string id, bool like, int type)
{
try
{
throw new Exception();
//code
return View(viewModel);
}
catch (Exception exe)
{
throw;
}
}
This seems to work in the Controller-actions. How can I simulate if it works within the ViewComponent? It gives me an error when I do the same try-catch method.
Step 1 : - Create a Custom ExpectionFilter Attribute
public class CustomExpectionFilter : IExceptionFilter
{
public void OnException(ExceptionContext filterContext)
{
filterContext.ExceptionHandled = true;
filterContext.Result = new ViewResult
{
ViewName = "~/Views/Shared/Error.cshtml"
};
}
}
Step 2 :- Register CustomExpectionFilter in FilterConfig
public class FilterConfig
{
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new CustomExpectionFilter());
}
}
Step 3 :- Change on
public IActionResult Device(string id, bool like, int type)
{
try
{
//code
return View(viewModel);
}
catch (Exception)
{
throw;
}
}
Now when ever error occur in application it will be calling CustomExpection Filter and this filter will handle error and display Error page.

Redirect to external url from OnActionExecuting?

I need to redirect to an external url (let's say "www.google.com") from OnActionExecuting method. Right now I'm using something like this:
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
if (!HttpContext.Current.User.Identity.IsAuthenticated)
{
var redirectUrl = "www.google.com";
try
{
var isAjaxRequest = filterContext.HttpContext.Request.IsAjaxRequest();
if (isAjaxRequest)
{
filterContext.HttpContext.Response.StatusCode = SessionController.CustomHttpRedirect;
filterContext.HttpContext.Response.StatusDescription = redirectUrl;
filterContext.Result = new JsonResult
{
Data = new { Redirect = redirectUrl },
JsonRequestBehavior = JsonRequestBehavior.AllowGet
};
}
else
{
filterContext.Result = new RedirectResult(redirectUrl, true);
}
return;
}
else
{
throw new LoggedOutException();
}
}
catch
{
throw new LoggedOutException();
}
}
}
The problem is that it's not redirecting me to "www.google.com" but it's redirecting to "http://localhost:1234/www.google.com" (I try it locally).
There is any way to solve this ?
Thanks
The problem was verry easy to solve:
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
if (!HttpContext.Current.User.Identity.IsAuthenticated)
{
var redirectUrl = "http://www.google.com";
try
{
var isAjaxRequest = filterContext.HttpContext.Request.IsAjaxRequest();
if (isAjaxRequest)
{
filterContext.HttpContext.Response.StatusCode = SessionController.CustomHttpRedirect;
filterContext.HttpContext.Response.StatusDescription = redirectUrl;
filterContext.Result = new JsonResult
{
Data = new { Redirect = redirectUrl },
JsonRequestBehavior = JsonRequestBehavior.AllowGet
};
}
else
{
filterContext.Result = new RedirectResult(redirectUrl, true);
}
return;
}
else
{
throw new LoggedOutException();
}
}
catch
{
throw new LoggedOutException();
}
}
}
All I had to do was that when I assigned the value to "redirectUrl", I had tu put http before wwww. This mus be put if you use a SSL conenction and you're trying to redirect from mvc to another domain.
Instead of using:
filterContext.Result = new RedirectResult("www.google.com", true);
Try the following:
filterContext.Result = new RedirectToRouteResult(new RouteValueDictionary(new { controller = "Home", action = "External" , ReturnURL = "www.google.com"}));
and in your (Home) controller create an action called (External) and from there redirect to your external url:
public class HomeController : Controller
{
[AllowAnonymous]
public ActionResult External(string ReturnURL){
return Redirect(ReturnURL);
}
}
You can't directly perform a server side redirect from an ajax response. You could, however, return a JsonResult with the new url and perform the redirect with javascript. see this answer

ASP.NET MVC Web API : Posting a list of objects

I'm trying to post a list of objects from my winforms application to my asp.net mvc 4 website. I've tested posting one object, and it works, but does not work for the list. It returns a 500 (Internal Server Error). Here is my code:
ASP.NET MVC Web API
public class PostTraceController : ApiController
{
public HttpResponseMessage Post(List<WebTrace> list)
{
try
{
// Some code
return Request.CreateResponse(HttpStatusCode.Created);
}
catch (Exception ex)
{
HttpContext.Current.Trace.Write("exception", ex.Message);
return Request.CreateErrorResponse(HttpStatusCode.ServiceUnavailable, ex);
}
}
public HttpResponseMessage Post(WebTrace item)
{
try
{
// Some code
return Request.CreateResponse(HttpStatusCode.Created);
}
catch (Exception ex)
{
HttpContext.Current.Trace.Write("exception", ex.Message);
return Request.CreateErrorResponse(HttpStatusCode.ServiceUnavailable, ex);
}
}
}
Win forms application
public class BaseSender
{
public BaseSender()
{
Client = new HttpClient
{
BaseAddress = new Uri(#"http://localhost/mywebsite/")
};
Client.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/json"));
}
public string UserCode { get; set; }
protected readonly HttpClient Client;
public HttpResponseMessage PostAsJsonAsync(string requestUri, object value)
{
var response = Client.PostAsJsonAsync(requestUri, value).Result;
response.EnsureSuccessStatusCode();
return response;
}
}
public class WebTraceSender : BaseSender
{
private const string requestUri = "api/posttrace";
public bool Post(List<ArchiveCptTrace> list)
{
try
{
var listWebTrace = new List<WebTrace>();
foreach (var item in list)
{
listWebTrace.Add(new WebTrace
{
DateStart = item.DatePreparation,
DateEnd = item.DateCloture,
UserStart = item.UserPreparation.UserName,
UserEnd = item.UserCloture.UserName,
AmountStart = item.MontantPreparation,
AmountEnd = item.MontantCloture,
TheoricAmountEnd = item.MontantTheorique,
Difference = item.Ecart,
UserCode = UserCode
});
}
var responce = PostAsJsonAsync(requestUri, listWebTrace);
return responce.IsSuccessStatusCode;
}
catch (Exception e)
{
// TODO : Trace the exception
return false;
}
}
}
EDIT :
I've found out the scenario of the error, which is having two methods in my api controller, even thought they have different signature. If I comment one method, the post work fine (item or a list). Any ideas ?
The methods may have different signatures, but Web API can't tell the difference between them without inspecting the body, which it won't do for performance reasons.
You could do two things - either create a new class which just holds a list of WebTrace objects, and put that in a different API controller, or you could map a custom route to one of your existing methods. You could do that with ActionName attribute, however, I would probably take the first approach.

How to display exceptions in the same View?

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

Return special JsonResult in case of Exception occurs

public JsonResult Menu() { // Exception }
I need application not to redirect user to the 404 page, but return special JSON result like { "result":1 }.
I wonder, is there any another solution, not try-catching.
You can implement your own FilterAttribute similar to the HandleErrorAttribute.
The HandleErrorAttribute normally does a redirect when an error occurs, but you could implement a similar attribute that returns a JsonResult. Something like the following will do:
public class CustomHandleErrorAttribute : HandleErrorAttribute
{
public override void OnException(ExceptionContext filterContext)
{
if (filterContext == null)
{
throw new ArgumentNullException("filterContext");
}
filterContext.Result = new JsonResult
{
Data = new { result = 1 },
JsonRequestBehavior = JsonRequestBehavior.AllowGet
};
filterContext.ExceptionHandled = true;
}
}
And then
[CustomHandleError]
public JsonResult Menu()
{
throw new Exception();
}
I would recommend that you download the MVC source code from CodePlex and inspect the current implementation of HandleErrorAttribute. It is a lot more subtle than my crude implementation above and you may want some of its functionality.

Resources