I've got this route configuration:
routes.MapRoute(
"routeB",
"routeB/{action}/{id}",
new { controller = "Sample", action = "IndexB", id = UrlParameter.Optional });
routes.MapRoute(
"routeA",
"routeA/{action}/{id}",
new { controller = "Sample", action = "IndexA", id = UrlParameter.Optional });
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
And my Sample controller contains these Action methods:
public ActionResult IndexA(string id)
{
return View("Index");
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult IndexA()
{
return RedirectToAction("Confirmation");
}
public ActionResult Confirmation()
{
return View("Confirmation");
}
public ActionResult IndexB(string id)
{
return View("Index");
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult IndexB()
{
return RedirectToAction("Confirmation");
}
If I land on the localhost/RouteA page and make a POST (via a button) it redirects me to localhost/RouteB/Confirmation.
How can I get the page to redirect to the RouteA/Confirmation page?
Thanks.
There are 2 issues here. As others have pointed out, your HTTP POST needs to be corrected. Since you are sharing a single view for 2 different actions, the simplest way to do that is to set the actionName parameter to null. This tells MVC to use the action name from the current request.
#{ ViewBag.Title = "Index"; Layout = "~/Views/Shared/_Layout.cshtml"; }
<h2>Index</h2>
#using (Html.BeginForm(null, "Sample", new { id = "OrderForm" }))
{
#Html.AntiForgeryToken()
<button type="submit" id="orderFormBtn">Extend my discount.</button>
}
The second issue is that the RedirectToAction call is ambiguous between routeA and routeB when generating the URL. Since the first match always wins, the URL you are redirecting to is always the top one in your configuration.
You can fix this problem by using RedirectToRoute to specify the route name (in addition to your existing matching criteria) explicitly.
public class SampleController : Controller
{
public ActionResult IndexA(string id)
{
return View("Index");
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult IndexA()
{
return RedirectToRoute("routeA", new { action = "Confirmation" });
}
public ActionResult Confirmation()
{
return View("Confirmation");
}
public ActionResult IndexB(string id)
{
return View("Index");
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult IndexB()
{
// Note that changing this one to RedirectToRoute is not strictly
// necessary, but may be more clear to future analysis of the configuration.
return RedirectToRoute("routeB", new { action = "Confirmation" });
}
}
Your problem is quite simple.
Take a look to your controller, the methods IndexA and IndexB, returns the same view; (from your comments)
#{ ViewBag.Title = "Index"; Layout = "~/Views/Shared/_Layout.cshtml"; }
<h2>Index</h2>
using (Html.BeginForm("IndexB", "Sample", new { #id = "OrderForm" })) {
#Html.AntiForgeryToken()
<button type="submit" id="orderFormBtn">Extend my discount.</button>
}
When you click on submit, you always do a post to IndexB
There are a lot of ways to correct such a simple error, for example you simple could use 2 different views IndexA and IndexB and change
using (Html.BeginForm("IndexB", "Sample", new { #id = "OrderForm" })) {
to
using (Html.BeginForm("IndexA", "Sample", new { #id = "OrderForm" })) {
The problem is that both your routeA and routeB routes will work for creating a link for an IndexA action in SampleController. As a result, BeginForm will just short-circuit and pick the first route that will work, which may or may not be the "correct" one you're looking for. To differentiate the routes, generate them via the route name:
#using (Html.BeginRouteForm("routeA", new { #id = "OrderForm" })) {
However, that will only ever let you get the "default" routeA route, i.e. /routeA/. There's no way to specify a different action other than the default one for the route.
For more flexibility, you can employ attribute routing, which will allow you to give a custom route name to each action, which you can then use to get the URL for that action.
Short of that, you will need to differentiate the two routes in some way, so that it's not ambiguous which should be used when generating a URL. That's often difficult to do with standard routing, which is why attribute routing is the much better approach if you're going to diverge from simply using the default route for everything.
Alternatively, you can restructure your project to keep the same URL structure, but make it much easier to differentiate routes. By employing areas, you can then specify the area where the route should be created from. For example, assuming you had RouteA and RouteB areas, you could then do:
#using (Html.BeginForm("IndexB", "Sample", new { area = "RouteA", #id = "OrderForm" })) {
That would then mean having a SampleController in each area, but you can employ inheritance to reuse code.
Related
I'm having an odd issue with routing in a basic ASP/MVC project.
I have a bunch of nav items setup:
<li>Home</li>
<li>Fire</li>
<li>Law Enforcement</li>
<li>Forensics</li>
<li>Reconstruction</li>
They all work fine, except for the third one, labeled law-enforcement.
When I mouse over this item, the URL is: http://localhost:54003/home/law-enforcement
When I mouse over any other item, the URl is: http://localhost:54003/fire
My Controller setup is:
public ActionResult Index()
{
return View();
}
[Route("~/fire")]
public ActionResult Fire()
{
return View();
}
[Route("~/law-enforcement")]
public ActionResult Law()
{
return View();
}
[Route("~/forensics")]
public ActionResult Forensics()
{
return View();
}
[Route("~/reconstruction")]
public ActionResult Reconstruction()
{
return View();
}
And my route config is:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.LowercaseUrls = true;
routes.MapMvcAttributeRoutes();
routes.MapRoute("Default", "{controller}/{action}/{id}",
new {controller = "Home", action = "Index", id = UrlParameter.Optional}
);
}
When I go to the route the URL specifies, ASP responds with a 404 page not found (as it should). If I go to the route that I know it should be, such as localhost/law-enforcement then the correct View is rendered.
Any ideas why ASP is routing this one particular action incorrectly?
The Razor Url.Action(...) cannot refer to the route defined by the [RouteAttribute] on the controller action; instead it needs to refer to the action's name. So changing my Razor syntax to refer to #Url.Action("law", "home") instead of #Url.Action("law-enforcement", "home") solved the issue.
You can keep your url mapping in either of 2 ways as below:
One way is to decorate your actionresult with attribute as below:
// eg: /home/show-options
[Route("law-enforcement")] //Remove ~
public ActionResult Law()
{
return View();
}
As per docs
otherwise
Just add one more configuration in Route.config file
routes.MapRoute("SpecialRoute", "{controller}/{action}-{name}/{id}",
new {controller = "Home", action = "law-enforcement", id = UrlParameter.Optional}
);
Source
I have the following Controller
namespace ExampleProject.Areas.Plugins.Default.Admin.Controllers
{
public class HomeController : Controller
{
public ActionResult Index()
{
return View();
}
}
}
I have the following custom route in my PluginsAreaRegistration.cs
context.MapRoute(
"Plugins_Default_Admin",
"Plugins/Default/Admin/{controller}/{action}/{id}",
new { action = "Index", id = UrlParameter.Optional },
namespaces: new[]{
"ExampleProject.Areas.Plugins.Default.Admin.Controllers"
}
);
So when I call the url "/Plugins/Default/Admin/Home/Index", I will get to the ActionResult in the controller Above.
What I want achieve is to call the Index ActionResult using Html.Action in a different View
Example:
#Html.Action("Index", "Home", new {area = "Plugins" })
I'm not sure how I can point the the specific home controller that I want? I have multiple HomeControllers in the Plugins Area, all with different routes.
Error I'm getting is "Multiple types were found that match the controller named 'Home'."
I have the following route defined
routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new { controller = "Account", action = "Login", id = UrlParameter.Optional }
);
I am then trying to use Html.BeginForm as below
<% using (Html.BeginForm("Login", "Account", System.Web.Mvc.FormMethod.Post, new { #class = "login-form" }))
{ %>
But this renders me a form like below
<form class="login-form" action="/" method="post">
</form>
However if i change my defaults on me route to be something different like
routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new { controller = "Home", action = "Login", id = UrlParameter.Optional }
);
My form then renders correctly, for info i am using Html.BeginForm() in a partial view that is returned from the login method on my account controller.
public class AccountController : Controller
{
public ActionResult Login()
{
return View();
}
[HttpPost]
public ActionResult Login(LoginModel model)
{
if (ModelState.IsValid)
{
//TODO: Login user
}
return View(model);
}
}
The behavior that you are noticing is expected and is also correct. When generating links MVC goes through the list of routes in the route collection(top-down) and sees which routes can match based on the route data that you are providing in Html.BeginForm. For example, you can imagine a request coming in like POST / and in this case your Account controller's Login action would be called because of the presence of defaults.
Actually it is expected behaviour. Actually routing system is pretty clever and it knows the request which is coming that is for the default values.(In your case default controller is Account and default action is Login and in your begin form you are using the same controller and action).
So routing system will replace it
by '/'.
You can verify it by just adding one another controller let say Admin and the same View Login. And now just replace the controller by new controller like
<% using (Html.BeginForm("Login", "Admin", System.Web.Mvc.FormMethod.Post,
new { #class = "login-form" }))
Now you will have link like
<form class="login-form" action="/Admin" method="post"></form>
There will be no action, because routing system will find the action is default action.
Thanks
It is my understanding that child actions execute using the same routes as the parent actions but I am having difficulty getting this to work. I have an MVC5 application that incorporates several areas. In one area I want to display a partial with some data in every view that is separate from the controller model so I added a child action to accomplish this, but when I try to view any of the pages in that area I get an InvalidOperationException telling me that
No route in the route table matches the supplied values
The area controller looks like this (simplified for brevity):
[RouteArea("SomeArea")]
[RoutePrefix("Here")]
public class PageController : Controller
{
private readonly IRepository repository;
public PageController(IRepository Repository)
{
this.repository = repository;
}
[Route("Page")]
public ActionResult ShowPage(int pageNumber = 1)
{
var model = new PageViewModel(repository, pageNumber);
ViewBag.Title = "This is a Page";
return View("ShowPage", model);
}
[ChildActionOnly]
public PartialViewResult DataView()
{
var model = new DataViewModel(repository);
return PartialView("DataView", model);
}
}
The layout for the area:
#{
Layout = "~/Views/Shared/_Layout.cshtml";
}
#Scripts.Render("~/bundles/pages")
<body>
<div id="content">
<br />
#Html.Partial("~/Areas/Page/Views/Shared/Search.cshtml")
#RenderBody()
#Html.Action("DataView", "Page")
</div>
</body>
The route config:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapMvcAttributeRoutes();
AreaRegistration.RegisterAllAreas();
routes.MapRoute("Default",
"{controller}/{action}/{id}",
new {controller = "Home", action = "Index", id = UrlParameter.Optional}
);
}
I have tried catching unhandled routing exceptions as in this article by David Boike to no avail.
Any suggestions would be greatly appreciated at this point. Thanks
Answer, supplied by Kiran in the comment above, is to simply decorate the child action with the route value for that action. Thank you Kiran!
I spent an ungodly amount of time researching this, and getting frustrated while I was at it, for such a simple solution. The routing now works with the child action as below:
[ChildActionOnly]
[Route("DataView")]
public PartialViewResult DataView()
{
var model = new DataViewModel(repository);
return PartialView("DataView", model);
}
I want to redirect to an action in other Controller but it doesn't work
here's my code in ProductManagerController:
[HttpPost]
public ActionResult RedirectToImages(int id)
{
return RedirectToAction("Index","ProductImageManeger", new { id=id });
}
and this in my ProductImageManagerController:
[HttpGet]
public ViewResult Index(int id)
{
return View("Index",_db.ProductImages.Where(rs=>rs.ProductId == id).ToList());
}
It redirect to ProductImageManager/Index without parameter very well(no error) but with above code i get this:
The parameters dictionary contains a null entry for
parameter 'ID' of non-nullable type 'System.Int32' for method
'System.Web.Mvc.ViewResult Index(Int32)' in
'...Controllers.ProductImageManagerController'.
An optional parameter must be a reference type, a nullable type, or be
declared as an optional parameter. Parameter name: parameters
This error is very non-descriptive but the key here is that 'ID' is in uppercase. This indicates that the route has not been correctly set up. To let the application handle URLs with an id, you need to make sure that there's at least one route configured for it. You do this in the RouteConfig.cs located in the App_Start folder. The most common is to add the id as an optional parameter to the default route.
public static void RegisterRoutes(RouteCollection routes)
{
//adding the {id} and setting is as optional so that you do not need to use it for every action
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
Now you should be able to redirect to your controller the way you have set it up.
[HttpPost]
public ActionResult RedirectToImages(int id)
{
return RedirectToAction("Index","ProductImageManager", new { id });
//if the action is in the same controller, you can omit the controller:
//RedirectToAction("Index", new { id });
}
In one or two occassions way back I ran into some issues by normal redirect and had to resort to doing it by passing a RouteValueDictionary. More information on RedirectToAction with parameter
return RedirectToAction("Index", new RouteValueDictionary(
new { controller = "ProductImageManager", action = "Index", id = id } )
);
If you get a very similar error but in lowercase 'id', this is usually because the route expects an id parameter that has not been provided (calling a route without the id /ProductImageManager/Index). See this so question for more information.
This should work!
[HttpPost]
public ActionResult RedirectToImages(int id)
{
return RedirectToAction("Index", "ProductImageManeger", new { id = id });
}
[HttpGet]
public ViewResult Index(int id)
{
return View(_db.ProductImages.Where(rs => rs.ProductId == id).ToList());
}
Notice that you don't have to pass the name of view if you are returning the same view as implemented by the action.
Your view should inherit the model as this:
#model <Your class name>
You can then access your model in view as:
#Model.<property_name>
Try this,
return RedirectToAction("ActionEventName", "Controller", new { ID = model.ID, SiteID = model.SiteID });
Here i mention you are pass multiple values or model also.
That's why here i mention that.
return RedirectToAction("ProductImageManager","Index", new { id=id });
Here is an invalid parameters order, should be an action first
AND
ensure your routing table is correct