ViewBag passes values in GET parameters in MVC - asp.net-mvc

I am new to MVC.NET and I am stopped at some point while passing data from controller to view. I have two action, one is for GET and another is for POST. When I am setting ViewBag values in my POST method action, it redirects me to View but passes the values using GET in the URL hence ViewBag values are not accessible in view.
Here is snapshot of the same:
View:
<div>
<p>#ViewData["FileName"]</p>
<p>#ViewData["myName"]</p>
<p>#ViewBag.myAdd</p>
<p>#ViewBag.someData</p>
</div>
<div>
<form id="myForm" action="~/Test/Index">
<input type="text" name="myName"/>
<input type="text" name="myAdd" />
<input type="file" name="myFile"/>
<input type="submit"/>
</form>
</div>
CONTROLLER
public ActionResult Index()
{
return View();
}
[HttpPost]
public ActionResult Index(HttpPostedFileBase file, FormCollection data)
{
ViewBag.FileName = Convert.ToString(file.FileName);
ViewBag.myName = Convert.ToString(data["myName"]);
ViewBag.myAdd = Convert.ToString(data["myAdd"]);
ViewBag.someData = "someData";
return View();
}
On submit of form, it redirects me to http://localhost:65077/Test/Index?myName=mYname&myAdd=MyAdddress&myFile=432f7018-d505-4b0b-8cba-505d62b5472d.png
it would be great if someone can help and explain the same to me.
thanks in advance.

Per default form-data is appended to the URL when send back to the server(GET-method). You have to change this by useing the method attribute:
<form id="myForm" action="~/Test/Index" method="post">
<input type="text" name="myName"/>
<input type="text" name="myAdd" />
<input type="file" name="myFile"/>
<input type="submit"/>
</form>

Related

ASP.NET Core TagHelper input asp-for rendering value attribute sticks with original value in request model

I build up a simple web page with asp.net core 3.1 for reproducing problem. There are 2 action in same HomeController.cs.
one for rendering a html form, another for receiving form post data then rendering form again.
public class HomeController : Controller
{
// rendering form
[HttpGet]
public IActionResult Index()
{
var myForm = new MyForm();
myForm.Id = 100;
return View("index", myForm);
}
// after form post, rendering form with updated value.
[HttpPost]
public IActionResult Index(MyForm m)
{
m.Id = 200;
return View("index", m);
}
}
I use asp.net core tag helper in index.cshtml
#model MyForm
<div>
<form asp-controller="Home" asp-action="Index" method="post">
<span>#Model.Id</span>
<input type="text" asp-for="#Model.Id"/>
<button type="submit">Submit</button>
</form>
</div>
When I GET /index, everything is fine, both #Model.Id in span and input value are 100
Http Response for Get /index
<form method="post" action="/">
<span>100</span>
<input type="text" data-val="true"
data-val-required="The Id field is required."
id="Id" name="Id" value="100">
<button type="submit">Submit</button>
</form>
But When I post form, I expected both value in span and input should be 200, but it is 200 in span and 100 in form
Http Response for POST /index with 100 in input
<form method="post" action="/">
<span>200</span>
<input type="text" data-val="true"
data-val-required="The Id field is required."
id="Id" name="Id" value="100">
<button type="submit">Submit</button>
</form>
I guess "asp-for" choose to read data from original ModelState, which is built while modelbinding. Instead of read ViewModel passed for view Razor rendering.
Does Anyone know why tag helper asp-for behavior like this? How should I avoid this behavior and make asp-for use the Model I passed in View("index", m)
I guess "asp-for" choose to read data from original ModelState
Yes, this is by design. Default TagHelper display ModelState value not Model.
If you want asp-for display the updated model value, you could add ModelState.Clear(); in your post action.
public IActionResult Index(MyForm m)
{
ModelState.Clear();
m.Id = 200;
return View("index", m);
}

Retain query string param after submit

On my website I use MVC Authentication, if a user wants to reset his password, he clicks on 'Forgot Password' then he gets by email a link with a unique code to reset his password.
My issue is if for some reason the reset fails (for example- password and password confirmation don't match etc) then it returns to the View without the unique code that was generated in the link that the user got by email and the reset password won't work afterwards (code query string param)
How can I keep the query string unique code when the reset fails and return it to the View?
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> ResetPassword(ResetPasswordViewModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
var user = await UserManager.FindByNameAsync(model.Email);
if (user != null)
{
var result = await UserManager.ResetPasswordAsync(user.Id, model.Code, model.Password);
if (result.Succeeded)
{
return RedirectToAction("ResetPasswordConfirmation", "Account");
}
}
ViewBag.Error = "Password reset failed";
return View();
}
And this is the form:
<form id="reset-form" method="post" action="Account/ResetPassword">
#Html.AntiForgeryToken()
<div class="header">
<h2>Reset Password</h2>
#if (!String.IsNullOrEmpty(ViewBag.Error))
{
<div class="invalid">
#ViewBag.Error
</div>
}
</div>
<div class="inputField">
<input type="email" id="email" name="email" placeholder="USERNAME" />
</div>
<div class="inputField">
<input type="password" id="password" name="password" placeholder="PASSWORD" />
</div>
<div class="inputField">
<input type="password" id="confirm-password" name="confirmPassword" placeholder="CONFIRM PASSWORD" />
<input type="submit"/>
</div>
</form>
I need it to return to the View like this:
~/account/resetpassword?code=xyz
The URL will remain the same as what you post when the view is returned unless you do a redirect (not recommended).
However, the issue isn't that you need the URL to change after the action method runs. You need to change the URL that is posted to the action method to include the query string so it is there when the action method returns the view. This can be done by simply changing the URL of the form.
Note that you should never hard code the URL in MVC views. Instead, you should always use controller, action, and other route values to resolve the URL using the UrlHelper (and related extension methods such as ActionLink). The routing infrastructure should be the only place where URLs exist so they can be changed in one place.
In this case the solution is to use Html.BeginForm to build up the form tag based on routing.
#using (Html.BeginForm("ResetPassword", "Account", new { code = "xyz" }, FormMethod.Post))
{
#Html.AntiForgeryToken()
<div class="header">
<h2>Reset Password</h2>
#if (!String.IsNullOrEmpty(ViewBag.Error))
{
<div class="invalid">
#ViewBag.Error
</div>
}
</div>
<div class="inputField">
<input type="email" id="email" name="email" placeholder="USERNAME" />
</div>
<div class="inputField">
<input type="password" id="password" name="password" placeholder="PASSWORD" />
</div>
<div class="inputField">
<input type="password" id="confirm-password" name="confirmPassword" placeholder="CONFIRM PASSWORD" />
<input type="submit"/>
</div>
}
Of course, you shouldn't hard code "xyz" here either, it should be passed to the view from the HttpGet ResetPassword method either in a Model or in ViewBag.
Note that if the url parameter of the route definition (typically in RouteConfig.cs) does not contain a parameter for code, it will automatically be added to the URL as a query string parameter.
I'm using Thymeleaf and had a similar issue. Here's how I solved it using the thymeleaf functionality:
HTML:
<form class="forms-sample" action="/login"
!!! th:action="#{/reset_password(token=${token})}" !!!
th:object="${passwordDto}" method="post">
**INSERT CODE HERE**
</form>
MVC Controller:
if(bindingResult.hasErrors()) {
model.addAttribute("token", tokenStr); !!!!!!!!
return "create_password";
}
try {
PasswordResetToken token = passwordResetTokenService.getByToken(tokenStr);
User user = token.getUser();
user = userMapper.setNewPassword(user, passwordDto);
userService.update(user);
passwordResetTokenService.delete(token);
return "home";
}catch (PasswordNotMatchException e) {
bindingResult.rejectValue("confirmNewPassword", "password_error",
"Confirm password did not match.");
model.addAttribute("token", tokenStr); !!!!!!!!
return "create_password";
}
catch (EntityNotFoundException e) {
return "create_password";
}
What I do is I put the token in the Model and if the post method encounters an error I make sure to add it again, before returning the view for creating a password.
The important thing to note is, as NightOwl mentioned, that the url stays the same, only the Model changes. Thats why we need to readd the token as a model attribute.

Return not only one, but many data

I've got this nice list in my View, and it has many values, so how can I like merge all the values and return it to my Controller?
<ul>
<li>Alex</li>
<li>Bobby</li>
<li>Camilla</li>
<li>Denise</li>
<li>Elise</li>
<li>Francis</li>
</ul>
Edit
My View is like, I have a textbox to enter a name, then I click the "add" button, and it's added in the list by jQuery.
So when the add button is clicked, I'd like to send all names to the Controller
You need to put those itens inside a form on your view
#using (Html.BeginForm())
{
<ul>
<li>Alex<input type="hidden" name="names" value="Alex"></li>
<li>Bobby<input type="hidden" name="names" value="Bobby"></li>
<li>Camilla<input type="hidden" name="names" value="Camilla"></li>
<li>Denise<input type="hidden" name="names" value="Denise"></li>
<li>Elise<input type="hidden" name="names" value="Elise"></li>
<li>Francis<input type="hidden" name="names" value="Francis"></li>
</ul>
<button type="submit">GOO</button>
}
then on your controller
[HttpPost]
public ActionResult Index(List<string> names)
{
return View();
}

Accepting params or raw data in controller?

I was wondering if it would be possible having a "params" argument in a controller function, or something similar which would allow me to process X amount of entries in my form.
For instance, I have a form which has X amount of "name" elements, which are auto-generated through jQuery. An example of these name elements could be the following:
<input type="text" name="studentName1"></input>
<input type="text" name="studentName2"></input>
<input type="text" name="studentName3"></input>
Now, there's a different amount of student names every time, so this makes it quite complex for me to handle the form data in my controller. I had something like the following 2 examples in mind, but of course they wouldn't work in reality.
[HttpPost]
public ActionResult PostStudentNames(params string[] studentNames)
Or:
[HttpPost]
public ActionResult PostStudentNames(string[] formValues)
Can I achieve something similar to that?
I just want to chime in with a different approach you can use for this. If it's more convenient, you can model bind directly to collections of primitive or complex types. Here's 2 examples:
index.cshtml:
#using (Html.BeginForm("ListStrings", "Home"))
{
<p>Bind a collection of strings:</p>
<input type="text" name="[0]" value="The quick" /><br />
<input type="text" name="[1]" value="brown fox" /><br />
<input type="text" name="[2]" value="jumped over" /><br />
<input type="text" name="[3]" value="the donkey" /><br />
<input type="submit" value="List" />
}
#using (Html.BeginForm("ListComplexModel", "Home"))
{
<p>Bind a collection of complex models:</p>
<input type="text" name="[0].Id" value="1" /><br />
<input type="text" name="[0].Name" value="Bob" /><br />
<input type="text" name="[1].Id" value="2" /><br />
<input type="text" name="[1].Name" value="Jane" /><br />
<input type="submit" value="List" />
}
Student.cs:
public class Student
{
public int Id { get; set; }
public string Name { get; set; }
}
HomeController.cs:
public class HomeController : Controller
{
public ActionResult Index()
{
return View();
}
public ActionResult ListStrings(List<string> items)
{
return View(items);
}
public ActionResult ListComplexModel(List<Student> items)
{
return View(items);
}
}
ListStrings.cshtml:
#foreach (var item in Model)
{
<p>#item</p>
}
ListComplexModel.cshtml:
#foreach (var item in Model)
{
<p>#item.Id. #item.Name</p>
}
The first form simply binds a list of strings. The second, binds the form data to a List<Student>. By using this approach, you can let the default model binder do some of the tedious work for you.
Updated for comment
Yes you can do that too:
Form:
#using (Html.BeginForm("ListComplexModel", "Home"))
{
<p>Bind a collection of complex models:</p>
<input type="text" name="[0].Id" value="1" /><br />
<input type="text" name="[0].Name" value="Bob" /><br />
<input type="text" name="[1].Id" value="2" /><br />
<input type="text" name="[1].Name" value="Jane" /><br />
<input type="text" name="ClassId" value="13" /><br />
<input type="submit" value="List" />
}
Controller action:
public ActionResult ListComplexModel(List<Student> items, int ClassId)
{
// do stuff
}
Mathias,
This works perfectly well without recourse to the params object. your form controls:
<input type="text" name="studentName" />
<input type="text" name="studentName" />
<input type="text" name="studentName" />
<input type="text" name="professorName" />
You would use the FormCollection object, which will contain all your form elements as either comma separated lists (if a control array) or as single properties. In the above example, this is what we'd get:
[HttpPost]
public ActionResult PostStudentNames(FormCollection formValues)
{
// basic check for rogue commas inside input controls
// would need far more sophistication in a #real# app :)
var valueStudents = formValues["studentName"].Split(',')
.Where(x => x.Length > 0).ToArray();
var valueProfessor = formValues["professorName"];
// other stuff
}
etc... At least, this is my recollection of this from a recent project. :)
<input type="text" name="studentName[0]"></input>
<input type="text" name="studentName[1]"></input>
<input type="text" name="studentName[2]"></input>
public ActionResult PostStudentNames(string[] studentName)
{
}

object gets passed to action as string

I have a form that has a hidden field wich stores a object. This object is a RoutesValues (I want to store a reference because when I process the form I want to redirect to a route). The action that processes the form is:
public ActionResult Añadir(string userName, string codigoArticulo, string resultAction, string resultController, object resultRouteValues, int cantidad)
{
processForm(codigoArticulo, cantidad);
if (!ModelState.IsValid)
TempData["Error"] = #ErrorStrings.CantidadMayorQue0;
if (!string.IsNullOrWhiteSpace(resultAction) && !string.IsNullOrWhiteSpace(resultController))
return RedirectToAction(resultAction, resultController, resultRouteValues);
return RedirectToAction("Index", "Busqueda", new {Area = ""});
}
and my form is:
#using (Html.BeginForm("Añadir", "Carrito", FormMethod.Get, new { #class = "afegidorCarrito" }))
{
<fieldset>
<input type="hidden" name="codigoArticulo" value="#Model.CodiArticle" />
<input type="hidden" name="resultController" value="#Model.Controller" />
<input type="hidden" name="resultAction" value="#Model.Action" />
<input type="hidden" name="resultRouteValues" value="#Model.RouteValues" />
<input type="text" name="cantidad" value="1" class="anadirCantidad" />
<input type="submit" />
</fieldset>
}
the problem I have is that resultRouteValues gets passed as a string instead of an object. Is there any way to fix this?
Thanks.
No, there is no easy way if RouteValues is a complex object. You will have to serialize the object into some text representation into this hidden field and then deserialize it back in your controller action. You may take a look at MvcContrib's Html.Serialize helper.

Resources