ASP.NET Core Boolean property binding on form POST does not work as expected - asp.net-core-mvc

I have got a bare-bones ASP.NET Core MVC application generated off of the MVC project template. The HomeController has been modified to expose two Index method overloads – one for GET and one for POST request, both of which render the same Index.cshtml view with a simple BooleanInputsViewModel:
public class BooleanInputsViewModel
{
public bool IsImportant { get; set; }
public bool IsActive { get; set; }
public List<string> Messages { get; } = new List<string>();
}
The Index.cshtml view looks like this:
#model AspNetCoreBooleanInputs.Models.BooleanInputsViewModel
<h2>
#nameof(this.Model.IsImportant) = #this.Model.IsImportant.ToString()
<br />
#nameof(this.Model.IsActive) = #this.Model.IsActive.ToString()
</h2>
<form class="form-horizontal" method="post">
<div class="col-md-12">
<input asp-for="IsImportant" />
<label asp-for="IsImportant">
</label>
<input asp-for="IsActive" />
<label asp-for="IsActive">
</label>
</div>
<div class="col-md-12">
<ul>
#foreach(string message in this.Model.Messages)
{
<li>#message</li>
}
</ul>
</div>
<div class="col-md-12">
<button type="submit">Submit</button>
</div>
</form>
Finally, the HomeController Index methods are implemented like this:
public IActionResult Index()
{
var model = new BooleanInputsViewModel();
model.Messages.Add($"GET values: {nameof(model.IsImportant)} = {model.IsImportant}, {nameof(model.IsActive)} = {model.IsActive}");
return View(model);
}
[HttpPost]
public IActionResult Index(BooleanInputsViewModel model)
{
model.Messages.Add($"POST values: {nameof(model.IsImportant)} = {model.IsImportant}, {nameof(model.IsActive)} = {model.IsActive}");
model.IsActive = !model.IsActive;
model.IsImportant = !model.IsImportant;
model.Messages.Add($"Negated POST values: {nameof(model.IsImportant)} = {model.IsImportant}, {nameof(model.IsActive)} = {model.IsActive}");
return this.View(model);
}
The POST handler negates the two model properties and passes the modified model back to the view. However, the negated values are not reflected in the rendered form as it always renders the originally POST-ed values. To me this looks like a bug. Do I miss something obvious?
The complete ASP.NET Core project is posted here - https://github.com/PaloMraz/AspNetCoreBooleanInputs.
Edit based on #Chris Platt's answer below:
Hi Chris, thank you for the prompt answer. I have verified that using the ModelState dictionary as you suggested works as expected, e.g.:
[HttpPost]
public IActionResult Index(BooleanInputsViewModel model)
{
model.Messages.Add($"POST values: {nameof(model.IsImportant)} = {model.IsImportant}, {nameof(model.IsActive)} = {model.IsActive}");
// This does NOT work:
//model.IsActive = !model.IsActive;
//model.IsImportant = !model.IsImportant;
// This works:
this.ModelState[nameof(model.IsActive)].RawValue = !model.IsActive;
this.ModelState[nameof(model.IsImportant)].RawValue = !model.IsImportant;
model.Messages.Add($"Negated POST values: {nameof(model.IsImportant)} = {model.IsImportant}, {nameof(model.IsActive)} = {model.IsActive}");
return this.View(model);
}
However, I still find it a very cumbersome behavior, because the model has already been bound once the Index method gets called. Why is the binding occurring again in the call to the View method, effectively ignoring the passed in model instance? This does not look right to me, sorry.
Besides, can you tell me please where did you get the information about the ModelState dictionary composition? The official documentation at https://learn.microsoft.com/en-us/aspnet/core/mvc/models/model-binding does not mention ViewData/ViewBag as sources; only form values, route values and query string...

The values your form fields are set to come from ModelState, which is composed of values from Request, ViewData/ViewBag, and finally Model. Importantly, Model is a last resort, so once you've done a post, the posted value (in Request) will be what the field is set to, regardless of any changes you make to Model.
I haven't tried doing this in ASP.NET Core, but you should be able to set ModelState["IsActive"].RawValue and ModelState["IsImportant"].RawValue instead. Assuming you can change the value in ModelState, then it will display as you want on your view.

Related

Asp.Net Core MVC pass string from Controller 1 to Controller 2 and use in View 2

Cannot work out how to simply pass a string from Controller 1 to Controller 2 and use in View 2. Can someone provide a simple example for me?
You said to pass a string from one controller to another, however, the
code you provided in comment is passing an object of Complaints.
If you want to pass an object by TempData, you need to convert object to json first ,then put this json in the TempData, when you want to get the object, you need to get the TempData and deserialize the json to object.
public class Complaints
{
public int? UniqueId { get; set; }
}
Change your code as follow:
public async Task<IActionResult> CreateWorkTicket(int? id)
{
Complaints data = new Complaints() { UniqueId = id };
TempData["mydata"] = JsonConvert.SerializeObject(data);// convert object to json
return RedirectToAction("Create", "WorkTickets");
}
/WorkTickets/Create
public IActionResult Create()
{
Complaints data = JsonConvert.DeserializeObject<Complaints>(TempData["mydata"].ToString());//convert json to object
return View(data);
}
Create.cshtml:
#model Complaints
#{
ViewData["Title"] = "Create";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<h1>Create</h1>
<div class="form-group">
<label asp-for="UniqueId" class="control-label">ID</label>
<input asp-for="UniqueId" onclick="ZoomMap()" class="form-control" />
<span asp-validation-for="UniqueId" class="text-danger"></span>
</div>
Test result:
Update
To pass a string , you can use TempData directly, or pass from controller to view, you can refer to the following code:
public async Task<IActionResult> CreateWorkTicket(int? id)
{
TempData["MyString"] = "hello";
return RedirectToAction("Create", "WorkTickets");
}
public IActionResult Create()
{
var data = TempData["MyString"].ToString();
return View((object)data);
}
View:
#model string
#{
ViewData["Title"] = "Create";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<h1>Create</h1>
<div class="form-group">
<input class="form-control" value="#Model" />#*first way*#
<input class="form-control" value="#TempData["MyString"].ToString()" />#*second way*#
</div>
Here is the test result:
you just need to make the view a strongly type view and bind primitive values to input field like
#model Complaints
<div class="form-group"> <label asp-for="Id" class="control-label">ID</label> <input asp-for="Id" onclick="ZoomMap()" class="form-control" value=#Model.Value1 /> <span asp-validation-for="Id" class="text-danger"></span> </div>

How to bind a list of objects in ASP.NET Core MVC when the posted list is smaller than original?

Using "disabled" attribute on inputs on form does not post them, which is expected and wanted. However, if you prepare a form of 3 objects in a list, disable the first and third, and submit, the 2nd object appears in post header, but does not bind to the list correctly, because it has an index [1] instead of [0].
I understand how model binding works and why it does not bind the posted object that I want, but I don't know how else to describe the problem to get specific results that would lead me to my solution. Anything I search for leads to basic post and binding examples.
List inside the model I'm using:
public IList<_Result> Results { get; set; }
Class _Result has one of the properties:
public string Value { get; set; }
I fill up the list and use it in view like so:
#for (int i = 0; i < Model.Results.Count; i++)
{
...
<td>
<input asp-for="Results[i].Value" disabled />
</td>
...
}
I have checkboxes on form, which remove (with javascript) the "disabled" attribute from the inputs and thus allow them to be posted.
When I fill up the said list with 3 _Result objects, they are shown on form and all have the "disabled" attribute. If I remove the "disabled" attribute from the first two objects and click on submit button, I receive the Results list with first 2 _Result objects, which is as expected.
However, if I remove the "disabled" attribute only from the second _Result object (the first _Result object still has "disabled" attribute), the Results list comes back empty in my Controller method.
In my Form Data Header, I see this: "Results[1].Value: Value that I want posted", which means that post occurs, but list does not bind the object due to the index.
Any idea on how I can achieve that proper binding? Also, the reason I'm using "disabled" attribute is because I'm showing many results on a single page and want to only post those that are selected.
For getting selected items, you could try checkbox with View Model instead of using jquery to control the disable property.
Change ViewModel
public class ModelBindVM
{
public IList<_ResultVM> Results { get; set; }
}
public class _ResultVM
{
public bool IsSelected { get; set; }
public string Value { get; set; }
}
Controller
[HttpGet]
public IActionResult ModelBindTest()
{
ModelBindVM model = new ModelBindVM
{
Results = new List<_ResultVM>() {
new _ResultVM{ Value = "T1" },
new _ResultVM{ Value = "T2" },
new _ResultVM{ Value = "T3" }
}
};
return View(model);
}
[HttpPost]
public IActionResult ModelBindTest(ModelBindVM modelBind)
{
return View();
}
View
<div class="row">
<div class="col-md-4">
<form asp-action="ModelBindTest">
#for (int i = 0; i < Model.Results.Count; i++)
{
<input type="checkbox" asp-for="Results[i].IsSelected" />
<label asp-for="#Model.Results[i].IsSelected">#Model.Results[i].Value</label>
<input type="hidden" asp-for="#Model.Results[i].Value" />
}
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<input type="submit" value="Save" class="btn btn-primary" />
</div>
</form>
</div>
</div>

MVC3 Postback doesn't have modified data

So I have the following code:
#model Project.Models.ViewModels.SomeViewModel
#using (Html.BeginForm("SomeAction", "SomeController", new { id = Model.Id}))
{
for(int i = 0; i < Model.SomeCollection.Count(); i++)
{
#Html.HiddenFor(x => Model.SomeCollection.ElementAt(i).Id)
<div class="grid_6">
#Html.TextAreaFor(x => Model.SomeCollection.ElementAt(i).Text, new { #style = "height:150px", #class = "grid_6 input" })
</div>
}
<div class="grid_6 alpha omega">
<input type="submit" value="Next" class="grid_6 alpha omega button drop_4 gravity_5" />
</div>
}
On the Controller Side I have the following:
[HttpPost]
public ActionResult SomeAction(int id, SomeViewModel model)
{
return PartialView("_SomeOtherView", new SomeOtherViewModel(id));
}
My View Model is set up like this:
public class SomeViewModel
{
public SomeViewModel()
{
}
public IEnumerable<ItemViewModel> SomeCollection { get; set; }
}
public class ItemViewModel{
public ItemViewModel(){}
public int Id {get;set;}
public string Text{get;set;}
}
The SomeCollection is always empty when SomeAction if performed. What do I have to do in order to show the updated values by users. Text Property and Id field.
Use an EditorTemplate
Create an EditorTemplate folder under your Views/YourcontrollerName and create a view with name ItemViewModel.cshtml
And Have this code in that file
#model Project.Models.ViewModels.ItemViewModel
<p>
#Html.EditorFor(x => x.Text)
#Html.HiddenFor(x=>x.Id)
</p>
Now from your Main view, call it like this
#model Project.Models.ViewModels.SomeViewModel
#using (Html.BeginForm("SomeAction", "Home", new { id = Model.Id}))
{
#Html.EditorFor(s=>s.SomeCollection)
<div class="grid_6 alpha omega">
<input type="submit" value="Next" class="grid_6 alpha omega button drop_4 gravity_5" />
</div>
}
Now in your HTTPPOST method will be filled with values.
I am not sure what you want to do with the values( returning the partial view ?) So not making any comments about that.
I am not sure you have posted all the code.
Your action method does not do anything, since it returns a partial view (for some reason from a post call, not an ajax request) using a new model object.
Your effectively passing a model back to the action and then discarding it, and returning a new model object. This is the reason your collection is always empty, its never set anywhere.
Well, for one thing, why do you have both the model AND id, a property of model, sent back to the controller? Doesn't that seem a bit redundant? Also, you're using a javascript for loop in the view. It'd be much easier to just use #foreach.
Anyway, your problem is that when you tell an action to accept a model, it looks in the post for values with keys matching the names of each of the properties of the model. So, lets say we have following model:
public class Employee
{
public string Name;
public int ID;
public string Position;
}
and if I'm passing it back like this:
#using(Html.BeginForm("SomeAction", "SomeController"))
{
<input type="text" name = "name" [...] /> //in your case HtmlHelper is doing this for you, but same thing
<input type="number" name = "id" [...] />
<input type="submit" name = "position" [...] />
}
To pass this model back to a controller, I'd have to do this:
Accepting a Model
//MVC matches attribute names to form values
public ActionResult SomethingPosted(Employee emp)
{
//
}
Accepting a collection of values
//MVC matches parameter names to form values
public ActionResult SomethingPosted(string name, int id, string postion)
{
//
}
or this:
Accepting a FormCollection
//same thing as first one, but without a strongly-typed model
public ActionResult SomethingPosted(FormCollection empValues)
{
//
}
So, here's a better version of your code.
Your new view
#model Project.Models.ViewModels.SomeViewModel
#{
using (Html.BeginForm("SomeAction", "SomeController", new { id = Model.Id}))
{
foreach(var item in Model)
{
#Html.HiddenFor(item.Id)
<div class="grid_6">
#Html.TextAreaFor(item.Text, new { #style = "height:150px", #class = "grid_6 input" })
</div>
}
<div class="grid_6 alpha omega">
<input type="submit" value="Next" class="grid_6 alpha omega button drop_4 gravity_5" />
</div>
}
}
Your new action
[HttpPost]
public ActionResult SomeAction(int Id, string Text)
{
//do stuff with id and text
return PartialView("_SomeOtherView", new SomeOtherViewModel(id));
}
or
[HttpPost]
public ActionResult SomeAction(IEnumerable<ItemViewModel> SomeCollection) //can't use someviewmodel, because it doesn't (directly) *have* members called "Id" and "Text"
{
//do stuff with id and text
return PartialView("_SomeOtherView", new SomeOtherViewModel(id));
}

Losing data in models and collections inside the ViewModel on postback

I am using a viewmodel that contains a few other models(entities) for each partial view.
I am rendering a partial view by passing the entity which is inside the ViewModel. My partial view has a few fields and some buttons. On click of button (which is inside my partial view) the form is being posted back with the data in a sub entity, whereas my viewmodel is always posted back as null...
I need the data to be present in my viewmodel on post back.
All views are strongly typed:
Code:
public class OrdersVM
{
public FiltersVM filterCriteria { get; set; }
public IEnumerable<MeterInventory> meters { get; set; }
public string assignTo { get; set; }
public IEnumerable<SelectListItem> AssigneeOptions { get; set; }
}
public partial class Meters
{
public int MTRNO { get; set; }
public string LOCName { get; set; }
}
public class FiltersVM
{
public string Center { get; set; }
public DateTime? DueDate { get; set; }
}
View Code
#model OrdersVM
#{
ViewBag.Title = "Orders";
}
#using (Html.BeginForm())
{
<div>
#Html.Partial("~/Views/Base/Filters.cshtml", Model.filterCriteria)
</div>
#foreach (var item in Model.meters)
{
<table>
<tr>
<td>
#Html.Encode(item.LOCNAME)
</td>
</tr>
</table>
}
}
Controller code
[HttpPost]
public ActionResult Index(OrdersVM orders, FiltersVM filters)
{
//orders is null
//filters has values
}
Thanks Olivehour. I am using the partial view "Filters.cshtml". and am rendering the same.
Below is the code for partial view :
#model ViewModels.FiltersVM <fieldset>
<legend>Order Assignment</legend>
<table id="tbl1" class="tableforcontrols">
<tr>
<td>
<div class="editor-label">
#Html.LabelFor(model => model.LDC)
</div>
</td>
<td>
<div class="editor-field">
<input type="submit" value="Search" id="btnSearch" name="button" />
</div>
</td>
<td>
<div class="editor-field">
<input type="submit" class="cancel" value="Reset" id="btnReset" name="button" />
</div>
</td>
</tr>
</table> </fieldset>
I tried with single argument "OrdersVM" (parent view model) but no luck.
[HttpPost]
public ActionResult Index(OrdersVM orders)
but if I pass the parent viewmodel to the partial view it was holding the data in OrdersVM.filterCriteria but not for properties (IEnumerable meters, string assignTo and Enumerable AssigneeOptions)
#Html.Partial("~/Views/Base/Filters.cshtml", Model)
I am new to MVC. Please let me know if any one finds the solution.
Thanks in advance.
It looks like you have a couple of problems here. One probable reason why the orders arg is null in your action method is because it doesn't look like you are rendering any input elements. You just have #Html.Encode(item.LOCNAME).
In order for the default model binder to construct an instance of OrdersVM and pass it to the action method, it needs to have input from the HTTP POST. You need something more like #Html.TextBoxFor(m => item.LOCNAME).
The second problem I think is that you have 2 arguments in the action method. Since the OrdersVM already has a FiltersVM property, you should just be able to have a single OrdersVM argument to the action method. During the HTTP POST, you can just access FiltersVM properties from OrdersVM.filterCriteria. This will lead to your 3rd challenge, though, since the meters property on OrdersVM is an IEnumerable collection.
To solve this, first have a couple reads of this article about model binding collections. It's old, but it still applies in MVC3. Read it and really wrap your head around it.
If you don't like using integers to index your collection fields, there is an HTML helper written by Steve Sanderson that allows you to index collection inputs using GUID's. We use this all the time, but it can be tricky -- mainly, you should always put the collection item in a partial view. For now, you might just be better off using integer-based indexing as outlined in the Haacked article.
It sounds like you are comming from Webforms. To transition to MVC you need to remove the thought of PostBack. This is concept that doesn't really exist on the web but Webforms faked it for us.
In MVC you usually start with a GET request like /edit/{someId}. From here you load the data for the viewmodel from the database and render the view. Now let's say that all data in the viewmodel is editable so each property have it's own input field. The user edits some data and saves the form. This issues a POST to the server.
Assume we have this POST method
[HttpPost]
public ActionResult Edit(MyViewModel model)
In this case you have all the data you need modelbinded because all data existed in the form.
You could do this and get the same view rendered because all data was databinded.
[HttpPost]
public ActionResult Edit(MyViewModel model){
return View(model);
}
Now let's pretend you have a dropdown in your form. Then you would have these two properties in your viewmodel.
public int CarId { get; set; }
public IEnumerable<SelectListItem> CarOptions {get; set; }
When you post the form this time the CarId will be populated in the ViewModel but not CarOptions because they are not a part of the form data. What you do if you would want to return the same view again is to reload the missing parts.
[HttpPost]
public ActionResult Edit(MyViewModel model){
model.CarOptions = LoadCarOptions();
return View(model);
}
It's certainly possible to modelbind that too if you put it in a hidden field. But it's easier and probably more effective to reload it from server/database again. This is the normal approach taken when working with MVC.

how can i transfer information of one view to another?

i have designed a view in asp .net mvc3 off course registration form. This is very simple form having name ,father name , qualification and a submit button , after pressing submit button i want to display information by using another view. please suggest me how can i send information from one view to another view.
my controller class is :
namespace RegistrationForm.Controllers
{
public class HomeController : Controller
{
public ActionResult Index()
{
// ViewBag.Message = "Welcome to ASP.NET MVC!";
//return View();
return RedirectToAction("registrationView");
}
public ActionResult About()
{
return View();
}
public ActionResult registrationView()
{
return View();
}
}
}
my view is :
#{
Layout = null;
}
registrationView
Enter Name
</td>
<tr>
<td>
Enter Father Name
</td>
<td>
<input type="text" name="fname" id="fname" />
</td>
<tr>
<td>
Enter Qualification
</td>
<td>
<input type="text" name="qly" id="qly" />
</td>
</tr>
</table>
<input type="submit" value="submit" />
</div>
well, we faced this problem before, and the best way to get this to work was to define a model that this page will work with, then use this model object when posting back, or redirecting to another view.
for your case, you can simply define this model in your Models folder
ex: RegistrationModel.cs file, and define your required properties inside.
after doing so, you will need to do 2 more steps:
1- in your GET action method, create a new RegistrationModel object, and provide it to your view, so instead of:
return View();
you will need something like:
var registrationModel = new registrationModel();
return View(registrationModel);
2- Use this model as a parameter in your POST Action method, something like
[HttpPost]
public ActionResult registrationView(RegistrationModel model)
{
// your code goes here
}
but don't forget to modify the current view to make use of the provided model. a time-saver way would be to create a new dummy View, and use the pre-defined template "Create" to generate your View, MVC will generate the properties with everything hooked up. then copy the generated code into your desired view, and omit any unneeded code.
this is a Pseudo reply. if you need more code, let me know
<% using Html.Form("<ActionName>") { %>
// utilize this HtmlHelper action to redirect this form to a different Action other than controller that called it.
<% } %>
use ViewData to store the value.
just remember that it will only last per one trip so if you try to call it again, the value would have been cleared.
namespace RegistrationForm.Controllers { public class HomeController : Controller { public ActionResult Index() { // ViewBag.Message = "Welcome to ASP.NET MVC!";
ViewData["myData"] = "hello world";
//return View();
return RedirectToAction("registrationView");
}
public ActionResult About()
{
return View();
}
public ActionResult registrationView()
{
// get back my data
string data = ViewData["myData"] != null ? ViewData["myData"].ToString() : "";
return View();
}
}
And you can actually usethe ViewData value on the html/aspx/ascx after redirect to the registrationView.
For example on the registrationView.aspx:
<div id="myDiv">
my data was: <%= ViewData["myData"] %>
</div>
You could simply in you method parameter list declare the parameters with the name of the controls. For example:
The control here has an id "qly"
<input type="text" name="qly" id="qly" />
Define your method parameter list as following:
public ActionResult YourMethod(string qly)
{
//simply pass your qly to another view using ViewData, TempData, or ViewBag, and use it in the desired view
}
You should use TempData which was made exactly for it, to persist values between actions.
This example is from MSDN (link above):
public ActionResult InsertCustomer(string firstName, string lastName)
{
// Check for input errors.
if (String.IsNullOrEmpty(firstName) ||
String.IsNullOrEmpty(lastName))
{
InsertError error = new InsertError();
error.ErrorMessage = "Both names are required.";
error.OriginalFirstName = firstName;
error.OriginalLastName = lastName;
TempData["error"] = error; // sending data to the other action
return RedirectToAction("NewCustomer");
}
// No errors
// ...
return View();
}
And to send data to the view you can use the model or the ViewBag.

Resources