Posting a Partial View Model to the Same MVC Page - c#

From the bare-bones MVC template, I have modified Manage.cshtml to include a partial, the _AccountSettingsPartial:
<section id="accountSettings">
#Html.Partial("_AccountSettingsPartial")
</section>
#if (ViewBag.HasLocalPassword)
{
#Html.Partial("_ChangePasswordPartial")
}
Manage.cshtml does not use #model anywhere, and it is loaded in AccountController.cs, in public ActionResult Manage(ManageMessageId? message).
_AccountSettingsPartial uses #model crapplication.Models.ManagePublicSettingsViewModel and _ChangePasswordPartial uses #model crapplication.Models.ManageUserViewModel.
Originally, AccountController.cs had just a post method for ManageUserViewModel:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Manage(ManageUserViewModel model)
I have tried to overload it for the other partial:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Manage(ManagePublicSettingsViewModel model)
But this does not work, and produces the following error on a post:
System.Reflection.AmbiguousMatchException: The current request for action 'Manage' on controller type 'AccountController' is ambiguous between the following action methods:
System.Threading.Tasks.Task`1[System.Web.Mvc.ActionResult] Manage(crapplication.Models.ManagePublicSettingsViewModel) on type crapplication.Controllers.AccountController
System.Threading.Tasks.Task`1[System.Web.Mvc.ActionResult] Manage(crapplication.Models.ManageUserViewModel) on type crapplication.Controllers.AccountController
How can I make my _AccountSettingsPartial page post back data from the user account management portal?

You need to have separate forms in your partials, in order to post to different actions. (Or you could change the action of a single form with JQuery, etc., but this will be easier.)
#using (Html.BeginForm("ManagePublicSettings", "Account", FormMethod.Post))
...
#using (Html.BeginForm("ManageUser", "Account", FormMethod.Post))
You then need to uniquely name the actions.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> ManagePublicSettings(ManagePublicSettingsViewModel model)
...
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> ManageUser(ManageUserViewModel model)

Related

Returning a method for a form

I have a form which after clicking Submit should call the AddProduct method
View
#using (Html.BeginForm("AddProduct", "Home", FormMethod.Post))
The problem is the controller. Has a specified Route.
In this situation, when I click Submit, AddProduct is not called, but Index
HomeController
[Route("{name?}/{adminCode?}")]
public IActionResult Index(string name, string adminCode)
[HttpPost]
public ActionResult AddProduct(string productName)
How to call the AddProduct method correctly?
Because of the optional route parameters and the lack of a HTTP Verb on the Index action you most are most likely encountering a route conflict.
First option would be to use the appropriate route attribute on the action.
[Route("[controller]")]
public HomeController {
[HttpGet("{name?}/{adminCode?}")]
public IActionResult Index(string name, string adminCode) {
//...
}
[HttpPost]
public ActionResult AddProduct(string productName) {
//...
}
}
That way when the post is made it will only consider actions that can handle POST requests.
Another option I would suggest is moving AddProduct to a separate controller (like ProductController).
HomeController
[Route("[controller]")]
public HomeController {
[HttpGet("{name?}/{adminCode?}")]
public IActionResult Index(string name, string adminCode) {
//...
}
}
ProductController
[Route("[controller]")]
ProductController {
[HttpPost]
public ActionResult AddProduct(string productName) {
//...
}
}
and updating the view accordingly
#using (Html.BeginForm("AddProduct", "Product", FormMethod.Post))
Apart from that, there is not enough details about what you are actually trying to achieve here, so I would also suggest adding more details about the main goals so that a more target answer can be provided.
Because of a wrong route config, controller can't see you that you have AddProduct action, and uses default Index action with AddProduct
as a name parameter. So try to add a route to AddProduct too
[Route("~/Home/AddProduct")]
public ActionResult AddProduct(string productName)
if the problem continues with another actions. Pls post your controller header and config routes code. Probably it needs to be fixed.

Ambiguous action methods with different HttpMethod

I'm experiencing a weird behaviour, at least to me. I written two methods within a controller with apparently different signatures:
[Route("~/Wallets/{walletId}/Transactions/Add")]
public async Task<ActionResult> Add(long walletId)
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Add(AddTransactionViewModel model)
The thing is every time I try to call the POST method using Ajax.BeginForm the GET method (the first) gets called.
#using (Ajax.BeginForm("Add", "Transactions",
new AjaxOptions() { HttpMethod = "POST" })
{
...
}
Now, why this is happening? Of course if I change the name of the GET method say to AddTransaction the code works, but I want to understand why it doesn't as it is.
This is because BeginForm uses GetVirtualPath internally to get the url from the route table. The first link is added to the route table in your example.
Simply editing the POST method with the following should do the trick:
[HttpPost]
[ValidateAntiForgeryToken]
[Route("Add")]
public async Task<ActionResult> Add(AddTransactionViewModel model)

Using Tuple for passing multiple models

I have a view includes login and registration form.
LoginAndRegister.cshtml file:
#model Tuple<Models.LoginViewModel, Models.RegisterViewModel>
#using (Html.BeginForm("Login", "Account", FormMethod.Post))
{
#Html.AntiForgeryToken()
#Html.ValidationSummary(false)
// Form Login here
}
#using (Html.BeginForm("Register", "Account", FormMethod.Post))
{
#Html.AntiForgeryToken()
#Html.ValidationSummary(false)
// Form Register here
}
AccountController file:
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Register([Bind(Prefix = "Item2")] RegisterViewModel model)
{
if (ModelState.IsValid)
{
// enter code here
}
// If we got this far, something failed, redisplay form
var tupleModel = new Tuple<LoginViewModel, RegisterViewModel>(null, model);
return View("LoginAndRegister", tupleModel);
}
}
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login([Bind(Prefix = "Item1")] LoginViewModel model, string returnUrl)
{
var tupleModel = new Tuple<LoginViewModel, RegisterViewModel>(model, null);
if (!ModelState.IsValid)
{
return View("LoginAndRegister", tupleModel);
}
}
I have 2 question, if you guys don't mind could you help me?
When I pass model (just one item of tuple) from controller to View, i have to change it to Tuple<> type and pass 1 value is null. Does this way is correct? It's working for me but I afraid that my way isn't correct.
And then, when model is invalid (example: values's input in to Login form is invalid), error messages will bind into #Html.ValidationSummary(false). But it's showed in 2 places (register and login form). How to resolve this issue?
https://gyazo.com/e9146059a6a098ee787565222d8dc744
Thanks for kind helping
Login and register are two different models. You can get around using Tuples in asp.net with the html helpers. Using a tuple just makes things messy.
What you probably want is something like this:
Register.cshtml file:
#model Models.RegisterViewModel
#using (Html.BeginForm("Register", "Account", FormMethod.Post))
{
#Html.AntiForgeryToken()
#Html.ValidationSummary(false)
// Form Register here
}
<div>Or if you already have an account then login:</div>
#Html.RenderAction("Login")
Controller:
public ActionResult Login()
{
if (Request.IsAjaxRequest())
{
return PartialView();
}
}
This will render the login view in the register view, you can also do this the other way around. Although I'd personally just offer a link to the user to redirect them to login page rather than using renderaction.

Only POSTs with both ActionLink and submit button; redirects otherwise

I'm trying to setup a simple form submission in MVC5, but I'm finding that my method doesn't get fired unless I have both an ActionLink and submit button.
I have a simple model:
public class LoginModel
{
public string username { get; set; }
}
Then I have two methods in my controller, one for when a form submission is available and one when not:
[HttpGet]
public ActionResult Login()
{
return View();
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Login(LoginModel myModel)
{
var username = myModel.username;
// do something with username
return View();
}
Finally, my View creates a POSTing form:
#using (Html.BeginForm("Login", "Home", FormMethod.Post))
{
#Html.AntiForgeryToken()
#Html.TextBox("username", string.Empty)
#Html.ActionLink("Enter", "Login")
<input type="submit" name="submit" value="Enter" />
}
I don't really care whether I use an ActionLink or whether I have a submit button (MSDN implies it should be the latter), but if I have only one of them, my [HttpPost] method is not called, and my page is redirected with the username in the query string:
/Home/Login?ReturnUrl=%2F%3Fusername%3DmyUsernameHere
If I have both on the page, I can click the ActionLink and I see that the appropriate method is called with myModel.username containing the value I provided. The submit button, however, will still redirect.
I want this form method to be POST, not GET (which it is in the generated HTML), and for failures to not contain the key as a GET param. What's the problem with having only one of these form submission mechanisms? Why do they not trigger the POST as expected? Is there something more I need to do to 'register' my model with the view, even though it is submitted properly in my workaround scenario?
Edit -- My configured routes are typically as follows:
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "Default",
url: "{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
routes.RouteExistingFiles = true;
So normally, I'm going to /Login instead of /Home/Login.
Also, I do have some authentication setup, with my HomeController decorated with [Authorize] and my Login methods both with [AllowAnonymous]. When I remove all annotations, I still find that my [HttpPost] is not called, and username shows up as a GET parameter instead of being POSTed.
I believe that the application doesn't understand that you're trying to make a model with just username. So, you are sending a string username and attempting to place it into a model. I'm not entirely sure about that. Could you try binding your form to your model? Here's an example:
View:
#model YourApplication.Models.LoginModel
#using (Html.BeginForm("Login", "Home"))
{
#Html.AntiForgeryToken()
#Html.TextBoxFor(model => model.username, string.Empty)
<input type="submit" value="Enter" />
}
Controller:
[HttpGet]
public ActionResult Login()
{
return View();
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Login([Bind(Include="username")] LoginModel myModel)
{
var username = myModel.username;
// do something with username
return View("Congrats");
}
Here's an alternate option. If you wanted to do something else, maybe try accepting the string "username" and creating your model after? Example:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Login(string username)
{
LoginModel myModel = new LoginModel();
myModel.username = username;
// do something with username
return View("Congrats");
}
Either way, you should be using the submit button.

Can I use an AntiForgeryToken without AcceptVerbs tag?

I would like to use the AntiForgeryToken function but the AcceptVerbs post does not apply. I am getting the anti forgery error. Is there a way to do this without the post method?
public ActionResult Page1(string data)
{ //code with view that includes link to Edit }
public ActionResult Page2(string data)
{ //code with view that includes link to Edit }
public ActionResult Edit(string pageName)
{ //execution then redirect to Page1/Page2 }
The anti forgery token works by a cookie and a hidden input field in the form. They both hold the same encrypted value. When the controller handles an action decorated with [ValidateAntiForgeryToken] it checks if the values in the cookie and the hidden input field match. If they don't - you get a nice exception.
You can use code like this
View:
<% using (var form = Html.BeginForm("DoSomething", "Default")) { %>
<%:Html.ValidationMessageFor(x => x) %>
<%:Html.AntiForgeryToken() %>
<%:Html.Hidden("a", 200) %>
<input type="submit" value="Go"/>
<%}%>
Controller:
public class DefaultController : Controller
{
public ActionResult Index()
{
return View();
}
[ValidateAntiForgeryToken]
public ActionResult DoSomething(int a)
{
return View("Index");
}
}
But then the form generated gets an method="post" attribute. On the controller side you don't need to specify [AcceptVerbs(HttpVerbs.Post)]. So the answer to your question is that you can use AntiForgeryToken without the AcceptVerbs attribute. You just need to use the POST method in the form.
To continue with the sample, if you specify [AcceptVerbs(HttpVerbs.Get)] on the action and Html.BeginForm("DoSomething", "Default", FormMethod.Get), the example won't work, because the GET request does not contain the cookie only the hidden input value gets encoded in the query string.

Categories