Html.BeginForm rendering with "/" action - c#

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

Related

Adding a new page to project

I have an MVC project using C#.
I have been using only one view, Views/Home/Index.cshtml to do most of the stuff I need the app to do.
Today I was asked to add a new page, that will serve as an "Admin" type of page to allow some basic crud operations to a record.
I am having trouble understanding how to navigate to a page other than the Home/Index.cshtml, actualy I do not even navigate to that page in the browser, since that is the default routing, the url looks like: http://localhost:51225/Meetings/Agenda/ -- this is how I can see the Index.cshtml page.
So far what I have done, in the HomeController, I added this code below the Index View:
// GET: Home
public ActionResult Index()
{
return View();
}
//GET: Admin
public ActionResult Admin()
{
return View(); -- I right clicked, and added a new view named "Admin"
}
My folders now look:
Views
Home
Admin.cshtml
Index.cshtml
I have not changed the RouteConfig class:
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = "" } // Parameter defaults
);
}
}
I can still open my app, and see the Index.cshtml when I go to:
http://localhost:51225/Meetings/Agenda
But I do not know how to access the Admin.cshtml
So far I know it is not by simply adding Admin at the end
http://localhost:51225/Meetings/Agenda/Admin
Nor
http://localhost:51225/Meetings/Agenda/Home/Admin
Nor
http://localhost:51225/Meetings/Agenda/Home/Admin.cshtml
Is it possible to ask for help in trying to learn how and what needs to chane in my solution in order to navigate to a different page that Index in the Home folder?
Add Custom route to the RegisterRoutes method in Route confige before default route:
routes.MapRoute(
name: "AgendaRoute",
url: "Meetings/Agenda/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
Final RouteConfig:
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "AgendaRoute",
url: "Meetings/Agenda/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
}
Result :
http://localhost:51225/Meetings/Agenda => Index.html
http://localhost:51225/Meetings/Agenda/index => Index.html
http://localhost:51225/Meetings/Agenda/index/1 => Index.html
http://localhost:51225/Meetings/Agenda/admin => Admin.html
http://localhost:51225/Meetings/Agenda/admin/1 => Admin.html
the default routing convention of ASP.NET MVC is Controllername/Action/parameter.
Lets say for example you have a Route like Products/Create, ASP.NET will search for an Action named Create in ProductsController and the view will be Create.cshtml inside the Products directory.
I suggest you follow that convention and create an AdminController and put Index action on the controller. Which you can Access by localhost:51225/Admin/Index. For the views, the convention is it searches for a view with the same name as the action,, that is you create a folder named Admin and put Index.cshtml inside it
You can always access anything by /ControllerName/ViewName/ParametersIfNeeded. I suggest you to add a button on the navigation bar which will be visible only for admins, and so that only they can visit that page. In your case the admin View can be accessed with http://localhost:51225/Home/Admin.

MVC route not kept on RedirectToAction

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.

ASP.NET MVC redirect to action Redirect Loops

I am learning asp.net MVC and I was trying Redirect to action and I tried the following code and I keep getting Redirect Loop Error.
This is the Controller class in which I am getting an error
public class CuisineController : Controller
{
// GET: Cuisine
public ActionResult Search(string name)
{
return RedirectToAction("About","Cuisine");
}
public ActionResult Index()
{
return Content("This is Index");
}
public ActionResult About()
{
return Content("this is About");
}
}
I have also created a route of my own other than the Default route
routes.MapRoute(name: "Cuisine",
url: "cuisine/{name}",
defaults: new { controller = "Cuisine", action = "Search", name = UrlParameter.Optional });
When I try to access the Cuisine controller it gives me a redirect loop error.
Thank you
In your routeConfig, you have a route defined for "cuisine/{name}" and it will be sent to the Search action method.
In your Search action method, You are redirecting to the About, Which is cuisine/About. This is matching with the route you defined cuisine/{name} so it will send the request to Search action again. The same process will keep running. That is why you are getting the redirect loop
You should either delete this specific route you defined or rename the url pattern for cusine search to prevent the redirect loop.
routes.MapRoute(name: "Cuisine",
url: "searchcuisine/{name}",
defaults: new { controller = "Cuisine", action = "Search",
name = UrlParameter.Optional });
This should be pretty obvious:
routes.MapRoute(name: "Cuisine",
url: "cuisine/{name}",
defaults: new { controller = "Cuisine", action = "Search", name = UrlParameter.Optional })
Says all urls that start with cuisine/ use the Search method on the CuisineController.
/Cuisine/About starts with that url, so it willl always use the Search method.
It looks like you want to make {name} part of URL. You can use attribute routing instead of changing default routing, it does not have such global "destructive" effects (explained in other answers) at least, and looks it's what you are actually after :)
[Route("Search/{name}")]
public ActionResult Search(string name)
{
return RedirectToAction("About", "Home");
}
See more about attribute routing here (for this to work, don't forget to add routes.MapMvcAttributeRoutes() if it is not there yet).
You need to restore controller action pattern. Just change url: "cuisine/{name}" to url: "cuisine/{action}/{name}"

Routing for Particular Action Adds 'Home' In Front of Action

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

ASP MVC4 Finding appropriate method in controllers with the same name in different areas

I have seen some similar questions but i found that i have a diferent problem.
I have a MVC4 project with controllers in the base project and 3 areas. Each area contains a controller named AdminController and a method List.
My problem is when i use a link to go to method List always go through the same controller (e.g. AdminController in Users area) but the view returned is correct.
The structure of the project is like this:
Mimbre.Administration (web mvc4 project)
Areas
Contents
Controllers
AdminController
Models
Views
Admin
List.chtml
-ContentsAreaRegistration.cs
Logs
Controllers
AdminController
Models
Views
Admin
List.chtml
LogsAreaRegistration.cs
Users
Controllers
AdminController
Models
Views
Admin
List.chtml
UsersAreaRegistration.cs
Controllers (base project)
Views (base project)
Classes for area registration contains RegisterArea method like this (with apropiate name and namespace according to the current area):
public override void RegisterArea(AreaRegistrationContext context) {
context.MapRoute(
"Contents_default",
"Contents/{controller}/{action}/{id}",
new { action = "Index", id = UrlParameter.Optional },
new string[] { "Mimbre.Administration.Areas.Contents.Controllers" }
);
}
Global.asax contains method for register routes for base project
public static void RegisterRoutes(RouteCollection routes) {
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
AreaRegistration.RegisterAllAreas();
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
namespaces: new string[]{"Mimbre.Administration.Controllers"}
);
}
In index.chtml (in base project) I created links to List method of each area:
<a href="#Url.Action("List", "Admin", new { area = "Contents"})" >Contents list</a>
<a href="#Url.Action("List", "Admin", new { area = "Logs"})" >Logs list</a>
<a href="#Url.Action("List", "Admin", new { area = "Users"})" >Users list</a>
In generated html these links appear like this:
Contents list
Logs list
Users list
I think these links are correct, the problem is that any link clicked always takes me through method List in AdminController of Users area, method List in AdminController of another areas are never picked.
This problem could be solved by renaming the name of the controller, unique name for each area, but i really need keep the same controller name in all areas, any idea???
Thans in advance!!!
Finally I could simulate your problem. The only way that I could have the same output as you (doens't matter what area you're trying to access, but reaching same users controller and responding with right view) is when you register the Area with the wrong controllers namespaces.
/Admin/Home/List route:
namespace SandboxMvcApplication.Areas.Admin.Controllers
{
public class HomeController : Controller
{
public ActionResult List()
{
ViewBag.Message = "You've acessed Admin/List";
return View();
}
}
}
/Admin/Home/List view:
#{
ViewBag.Title = "List";
}
<h1>Admin</h1>
<h2>#ViewBag.Message</h2>
Admin Area Registration:
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
"Admin_default",
"Admin/{controller}/{action}/{id}",
new { action = "Index", id = UrlParameter.Optional },
new string[] { "SandboxMvcApplication.Areas.Admin.Controllers" }
);
}
/User/Home/List route:
public class HomeController : Controller
{
public ActionResult List()
{
ViewBag.Message = "You've accessed User/List";
return View();
}
}
/User/Home/List view:
#{
ViewBag.Title = "List";
}
<h1>User</h1>
<h2>#ViewBag.Message</h2>
User Area registration:
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
"User_default",
"User/{controller}/{action}/{id}",
new { action = "Index", id = UrlParameter.Optional },
new string[] { "SandboxMvcApplication.Areas.Admin.Controllers" }
);
}
With this configuration, my outputs are:
/Admin/Home/List
Admin
You've acessed Admin/List
/User/Home/List
User
You've acessed Admin/List
Note that the first word works fine (insuring that the right view was rendered but with wrong data comming from list action).
If I correct the user controller namespace in UserAreaRegistration ("SandboxMvcApplication.Areas.User.Controllers"), everything works fine!
Check it out if this could help you or try to explicitly put your namespaces to the right controller. You could also check if the Admins controller are with the right namespace too.
You need to append one more setting to your route registration. This will prevent the DefaultControllerFactory from searching for controller in unintended areas.
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
namespaces: new string[]{"Mimbre.Administration.Controllers"}
).DataTokens["UseNamespaceFallback"] = false;

Categories

Resources