Why does Html.BeginForm generate empty action? - asp.net-mvc-3

I have a controller in an area called Admin
public class SiteVisitController : Controller
{
public ViewResult ReadyForCompletion() { ... }
public ViewResult CompleteAndExport() { ... }
}
and a view (ReadyForCompletion.cshtml) that has posts back to a different controller action on the same class
#using (Html.BeginForm( "CompleteAndExport", "SiteVisit" ))
{
<input type="submit" value="Complete & Export" />
}
The generated HTML for this form has a blank action:
<form action="" method="post"> <input type="submit" value="Complete & Export" />
</form>
I want to know why this has a blank action? For more info, I also added in a
#Url.RouteUrl(new { controller = "ReadyForCompletion", action = "SiteVisit", area = "Admin" })
which also printed out an empty string. Also, if I use an empty Html.BeginForm() it generates the correct action.
Registered routes are
context.MapRoute(
"Admin_manyParams",
"Admin/{controller}/{action}/{id}/{actionId}",
new { action = "Index", id = UrlParameter.Optional, actionId = UrlParameter.Optional }
);

I believe your problem is caused by having consecutive optional parameters. I was not able to replicate your problem until I changed the route to contain two optional parameters.
See: This article which explains the problem

For those of you encountering this issue using ASP.NET Core the root cause is the same, though the solution is slightly different. I first saw this in Core using multiple default values when calling .MapRoutes(). E.g.
routes.MapRoute(
name: "default",
template: "{controller}/{action}/{id?}",
defaults: new { controller = "Foo", action = "Bar" }
);
The workaround is to place the default values into the string template:
routes.MapRoute(
name: "default",
template: "{controller=Foo}/{action=Bar}/{id?}"
);
YMMV.

Related

Asp.net MVC - Post data to a controller

I am working on a sample mvc project in which I am trying to post data to a controller. I have posted sample (see below), but if I add [HttpPost] to method I am getting '404' error.
View:
<% using (Html.BeginForm()) { %>
<%= Html.Telerik().NumericTextBox()
.Name("NumericTextBox")
.Spinners(false)
.EmptyMessage("ID")
%>
<input type="submit" value="Submit" />
<% } %>
Controller:
[HttpPost]
public ActionResult GetDetails(int id)
{
return View();
}
**I also tried,**
[HttpPost]
public ActionResult GetDetails(FormCollection collection)
{
return View();
}
Route:
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Customer", action = "GetDetails", id = UrlParameter.Optional } // Parameter defaults
);
You'll want the Name to match the parameter on the controller, so I believe it should be like this:
Html.Telerik().NumericTextBox()
.Name("id")
Notes:
Although you specified UrlParameter.Optional on the id route parameter, it's not truly optional unless you make it nullable (ie, int? id) in the controller action.
Normally you should use GET and not POST for HTTP requests that don't change anything on the server, which seems to be the case here.
You should use the second method, but instead of FormsCollection, use:
GetDetails(int NumericTextBox)
The parameter must be the same name as the input box.

Unexpected form url with mixed aspx & controller MVC .Net routes

I have two routes defined thus:
//Custom route for legacy admin page
routes.MapPageRoute(
"LocaliseRoute", // Route name
"Admin/Localise", // URL
"~/Views/Admin/Localise.aspx" // File
);
routes.MapRoute(
"Admin", // Route name
"Admin/{action}/{id}", // URL with parameters
new { controller = "Admin", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);
both the following GETs work fine:
http://pegfect.local/Admin/PegModelUpload
http://pegfect.local/Admin/Localise
However, the form action of the former is /Admin/Localise?action=UploadPegModel&controller=Admin
resulting in an expression of "WTF?!"
the code for the form is:
#using (Html.BeginForm("UploadPegModel", "Admin", FormMethod.Post, new { enctype = "multipart/form-data", onsubmit = "return validateForm();" }))
{
<input type='file' name='file' id='file' />
<input type="submit" value="submit" />
}
The answer is right here: http://bartwullems.blogspot.co.uk/2011/04/combining-aspnet-webforms-and-aspnet.html
Answers on a postcard as to why this would work...

Basic MVC routing query using default project template

I am in the process of learning MVC 3 using the basic project template coupled with several examples I have. Things are going well, but now I am trying to implement my controllers and I am having a couple of issues.
So far I have modified the _Layout.cshtml file to have a new link with a specified route defined:
<header>
<div id="title">
<h1>My MVC Application</h1>
</div>
<div id="logindisplay">
#Html.Partial("_LogOnPartial")
</div>
<nav>
<ul id="menu">
<li>#Html.ActionLink("Home", "Index", "Home")</li>
<li>#Html.RouteLink("Contracts", "Contract")</li>
<li>#Html.ActionLink("About", "About", "Home")</li>
</ul>
</nav>
</header>
and my global.asax.cs file is as follows:
routes.MapRoute(
"Contract",
"Contract",
new { controller = "Contract", action = "List", id = UrlParameter.Optional }
);
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);
This works fine as in it returns the expected action view from my Contract controller.
However I would like to modify this to accept an id into the List action. I know that I need to change the List method to accept a parameter, no problem there, but the issue it with the route and how to pass this paramter into the List method from the RouteLink in the _Layout.cshtml file. I have tried a few things, but this bit is really stumping me.
I intend to pass an id from the User that I logged in as through the AccountController, however I will ask another question about that to keep this more consise.
Thank you very much.
You don't actually need your Contract route, as your Default route will work for any controller and action that corresponds to the pattern controller/action/(optional id parameter here). See the comment in the template actually says Parameter defaults. This means, if there is no Controller, Action, or id passed in, it will default to those values. That's why you can just browse to the root of the website and the Home controller's Index action is the default call.
When using routes, you need to remember that the route parameter names need to match the parameter names in your actions.. for example, your Default route currently lets you do this:
[HttpGet]
public ActionResult MyAction(int id) {
}
But, if you changed your default route to be this:
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{myIDParameter}", // URL with parameters
new { controller = "Home", action = "Index", myIDParameter = UrlParameter.Optional } // Parameter defaults
);
..your Index action would no longer bind the integer parameter properly.. you would have to change the action to this:
[HttpGet]
public ActionResult MyAction(int myIDParameter) {
}
In answer to your question, it might make more sense to use an ActionLink, like the other two you already have:
#Html.ActionLink("Contracts", "Contract", "ActionMethodHere", new { id = UserIdHere }, null)
That assumes though, that you remove your Contract route and just use the default route.

How to display ValidationSummary using multiple forms using MVC

I have looked at many of the solutions on offer on this site and others for my problem but none of them seem to work perfectly for my solution.
On my Layout page I have a login area on the top of the screen. This is always present unless a user is logged in. This login form has a ValidationSummary on it and every time I post back using another form on the site the validation for this login form is being triggered.
I'm almost certain that this is down to how I call this login page from my Layout page. It is not a partial view that is in a Shared folder, it is in an Area in my project. On my Layout page I call the login form like this:
#Html.Action("LogOn", "Account", new { area = "Store" })
The logon page contains the following:
#model Project.ViewModels.LogOn_ViewModel
#{
Layout = null;
}
#using (Ajax.BeginForm("LogOn", "Account", new { #area = "Store" }, new AjaxOptions { HttpMethod = "Post", UpdateTargetId = "LoginContainer", LoadingElementId = "actionLoaderImage" }, new { id="LogonForm" }))
{
#Html.AntiForgeryToken()
<div class="loginArea">
<div class="notRegistered">
<h4>Not registered yet?</h4>
Register now<br/>
#Html.ActionLink("Register", "Index", "SignUp", new { area = "Store" }, new { #class = "greenButton" })
</div> <!-- /notRegistered -->
<div class="loginForm">
<div class="formFields">
<label class="inline">#Html.LabelFor(m => m.UserName)</label>
#Html.TextBoxFor(m => m.UserName)
<label class="inline">#Html.LabelFor(m => m.Password)</label>
#Html.PasswordFor(m => m.Password)
<input type="submit" name="LogIn" value="Log in" class="blueButton" />
<img id="actionLoaderImage" src="#Url.Content("~/Content/web/images/loader.gif")" alt="icon" style="margin-right:15px; display:none;" />
#Html.ValidationSummary()
</div>
</div> <!-- /loginForm -->
</div> <!-- /loginArea -->
}
The login controller is standard stuff:
// GET: /Account/Logon
public ActionResult LogOn()
{
// if logged in show logged in view
if (User.Identity.IsAuthenticated)
return View("LoggedIn");
return View();
}
// POST: /Account/Logon
[HttpPost]
public ActionResult LogOn(LogOn_ViewModel model)
{
if (ModelState.IsValid)
{
if (SecurityService.Login(model.UserName, model.Password, model.RememberMe))
{
return View("LoggedIn");
}
else
{
ModelState.AddModelError("", "The user name or password provided is incorrect.");
}
}
return PartialView(model);
}
I suspect that what is happening here is that Html.Action is 'posting' the login form if a post is occurring elsewhere on the page. This makes sense as the layout page itself would be posted as part of a form post action.
I tried implementing the custom Validator examples from some other SO questions and blogs (http://blogs.imeta.co.uk/MBest/archive/2010/01/19/833.aspx) but I found that using those examples would not display the validation summary with client side validation which is not much use to me.
The solution I am looking for would need to allow both client and server side validations to appear for the correct form. Does anyone have an example of something like this using MVC? I'd like to avoid manually looking after the client side validation using jquery if possible and just to use the framework to handle the work for me.
Thanks for your suggestions,
Rich
After playing around with this issue for longer than I care to admit, the answer as always was simple once you know how!
The solution to this issue for anyone else who comes up against it is to rename your action so your POST action is a different name to your GET action. This way, when the Layout page is posted back and triggers the PartialView to also be posted back there is only a GET action for your PartialView so the ValidationSummary will not be triggered.
Based on my example above I have altered the form to postback to another action for my login partial view and this has solved this issue for me.
In my Layout page the link to the partial in the store area remains the same:
#Html.Action("LogOn", "Account", new { area = "Store" })
The logon form action is then altered to point to a new action - not called the same name as the GET action:
#using (Ajax.BeginForm("ConfirmLogon", "Account", new { #area = "Store" }, new AjaxOptions { HttpMethod = "Post", UpdateTargetId = "LoginContainer", LoadingElementId = "actionLoaderImage" }, new { id="LogonForm" }))
{
....
}
Then I just renamed my controller action so that it has a different action name for the POST action so that when the layout page is called using a post and the partial is loaded up with a POST action that it doesn't call my logon POST action:
// POST: /Account/ConfirmLogon
[HttpPost]
public ActionResult ConfirmLogon(LogOn_ViewModel model)
{
if (ModelState.IsValid)
{
if (SecurityService.Login(model.UserName, model.Password, model.RememberMe))
{
return PartialView("LoggedIn");
}
else
{
ModelState.AddModelError("", "The user name or password provided is incorrect.");
}
}
return PartialView("Logon",model);
}
Hopefully this will help some others out with this issue. Seems so obvious now but drove me nutty :D

ASP.NET MVC 3 Controller route - make everything under home controller appear under the domain

Currently everything under homecontroller appears in the URL as
example.com/Home/{Action}
Is there a way we can keep all other routing the way it is but ONLY special case home controller so everything under home comes under the domain.
like
example.com/about
example.com/contact
example.com/error
instead of creating new controller classes for each of them.
EDIT:
The other URL's like
example.com/user/details/123
example.com/user/edit/123
Which are in the userController should work the same as they are now
I think the best way is:
routes.MapRoute("home", "home", new { controller = "Home", action = "Index" });
routes.MapRoute("about", "about", new { controller = "Home", action = "About" });
routes.MapRoute("contact", "contact", new { controller = "Home", action = "Contact" });
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);
and when you want to create a link, use:
#Html.RouteLink("Home", "home", new{/* route values */}, new {/* html attribues */})
OR:
#Html.RouteLink("Home", "home")
instead of:
#Html.ActionLink("Home", "Index", "Home", new{/* route values */}, new {/* html attribues */})
this works for me, and should work for you too.
UPDATE:
you can create a symbol like # (or - or anything else), before the action part in url, to make the url unique, such as:
routes.MapRoute(
"test", // route name
"#{action}", // url and parameters
new {controller = "MyHome", action = "Home"} // parameter defaults
);
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);
in this way, your urls are different from the Default map-route and you can create urls like:
site.com/#Home
site.com/#About
site.com/#Contact
but the first, in my idea, is better and I always use that.
Using the Attribute Routing of MVC5, I did similar to Javad_Amiry answer, by placing a route for each action in HomeController:
public class HomeController : Controller
{
[Route("about")]
public ActionResult About()
{
return View();
}
I think this is more maintainable than placing every action in the global RouteConfig.cs file. Better still to combine Attribute Routing with convention-based routing, so new actions added to controller will work by default without a Route attribute (eg: /Home/action) but can be improved by adding the Route attribute (eg: /action).
You could simply modify the default route and remove the controller bit from the url and specify that it will always be Home in the default values:
routes.MapRoute(
"Default",
"{action}/{id}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
Obviously you realize that this limits your application to a single controller which is HomeController as now you no longer have any possibility to set it in your url. Stuffing all the actions in a single controller is a bad practice IMHO and violates a couple of principles like RESTful routing and SRP.
ASP.NET MVC root url’s with generic routing

Resources