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

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>

Related

.Net Core Razor Pages - Refresh fields after post -unobtrusive ajax

I have created a .Net Core Razor Pages Application. There are two input fields and a submit button in a razor page. When I click on the button, the numbers in the input fields needs to be incremented. There is a message ‘Hello World’ which is assigned in the OnGet() method.
To keep the message, I used unobtrusive ajax. In this case, the message will remain there but the numbers will not increment. Is there any way to refresh the numbers without writing code in ajax call back method to assign values individually to each element?
Ultimately, my aim is to post a portion of a page and refresh the bind data in the fields on post back without assigning values to the controls individually in ajax call back. Code sample is given below
Note:Need to do this without the whole page relaod.
Index.cshtml
#page
#model IndexModel
#{
ViewData["Title"] = "Home page";
}
<h1>#Model.Message</h1>
<form method="post" data-ajax="true" data-ajax-method="post" >
<div>
<input type="text" asp-for="Num1" />
<input type="text" asp-for="Num2" />
<input type="submit" value="Submit" />
</div>
</form>
Index.cshtml.cs
public class IndexModel : PageModel
{
[BindProperty]
public int Num1 { get; set; } = 0;
[BindProperty]
public int Num2 { get; set; } = 0;
public string Message { get; set; }
public void OnGet()
{
Message = "Hello World";
GetNumbers();
}
void GetNumbers()
{
Num1 += 1;
Num2 += 5;
}
public IActionResult OnPost()
{
GetNumbers();
return Page();
}
}
ModelState.Remove("Nmu1");
ModelState.Remove("Nmu2");
Similarly to ASP.NET WebForms, ASP.NET Core form state is stored in ModelState. After posting, the form will be loaded with the the binding values, then updated with ModelState. So there is a need to clear the values within ModelState, otherwise the values will be overwritten.

ASP.NET Core Boolean property binding on form POST does not work as expected

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.

How do I presist data from the view during EDIT GET to EDIT POST?

I have a dilemma that I hope someone can help me with. During the Edit Get stage of loading and updating a page before it is loaded, I store the checkboxes that are checked like this:
foreach (var course in courses.Select((x, i) => new { Data = x, Index = i }))
{
#: <td>
<br /><br /><br />
<input type="checkbox" id="checkbox" name="selectedCourses" value="#course.Data.CourseID"
#(Html.Raw(course.Data.Assigned ? "checked=\"checked\"" : "")) />
</td>
bool theNewString = course.Data.Assigned;
String a = theNewString.ToString();
assignedCourses.Add(a);
}
At the top of the view I defined the list to store the data like this so that I can be sent in the BeginForm:
#{ List<String> assignedCourses = new List<String>(); }
Then I try to send the list to the Edit POST like this:
#using (Html.BeginForm(new { assigned = assignedCourses }))
In my controller signature looks like this:
[HttpPost]
public ActionResult Edit(int id, List<String> assigned)
The list is loaded at the end of the get stage but the data in the list does not go through to Edit POST. My question is how do I get the list that I created at the end of the edit get stage to persist so that I can use it in my post?
Thanks for any ideas.
I'll try make this as simple as possible. So your Course model is something like this...
public class Course
{
public int Id { get; set; }
public string Name { get; set; }
public bool Assigned { get; set; }
}
...and you want to display some saved values on screen to edit. So starting at the controller, I've dummied some data and set one of the Assigned booleans to true so it is checked when the page loads.
public Controller Courses
{
public ActionResult Edit(int id)
{
var courses = new Course[] {
new Course() { Id = 1, Name = "maths", Assigned = true },
new Course() { Id = 2, Name = "english", Assigned = false },
new Course() { Id = 3, Name = "science", Assigned = false }
};
return View(courses);
}
Now, your page should expect a collection of these courses, so at the top of your page define the type that the View is expecting...
cshtml
#model IEnumerable<ExampleProject.Domain.Course>
...which you can then enumerate through in your View and create your checkboxes.
#using (Html.BeginForm("Edit", "Courses", FormMethod.Post))
{
#foreach(var item in Model)
{
<label for="#item.Id">#item.Name</label>
<input type="checkbox" name="courses" id="#item.Id" value="#item.Id" checked="#item.Assigned" />
}
<button type="submit">Save changes</button>
}
This will render the following html:
<label for="1">maths</label>
<input type="checkbox" name="courses" id="1" value="1" checked="checked">
<label for="2">english</label>
<input type="checkbox" name="courses" id="2" value="2">
<label for="3">science</label>
<input type="checkbox" name="courses" id="3" value="3">
Now you can check what you like and submit your form. In true http fashion your controller action will receive the values were checked on the View:
[HttpPost]
public ActionResult Courses(int id, int[] courses)
{
// courses will contain the values of the checkboxes that were checked
return RedirectToAction("Index"); // etc
}
Hopefully this helps. You can be a bit smarter and use the Html helpers for some more complicated binding (eg. Html.CheckBox, Html.CheckBoxFor) but this should get you going and it's clear to see what's going on.

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));
}

MVC3 Modelbinder EF4 ICollection property [duplicate]

I'm working on my first ASP.NET MVC 3 application and I've got a View that looks like this:
#model IceCream.ViewModels.Note.NotesViewModel
#using (Html.BeginForm())
{
#Html.ValidationSummary(true)
#Html.TextBoxFor(m => m.Name)
foreach (var item in Model.Notes)
{
#Html.EditorFor(m => item);
}
<input type="submit" value="Submit"/>
}
And I have an EditorTemplate that looks like this:
#model IceCream.ViewModels.Note.NoteViewModel
<div>
#Html.HiddenFor(m => m.NoteID)
#Html.TextBoxFor(m => m.NoteText)
#Html.CheckBoxFor(m => m.IsChecked)
</div>
NotesViewModel looks like so:
public class NotesViewModel
{
public string Name { get; set; }
public IEnumerable<NoteViewModel> Notes { get; set; }
}
NoteViewModel looks like this:
public class NoteViewModel
{
public int NoteID { get; set; }
public System.DateTime Timestamp { get; set; }
public string NoteText { get; set; }
public bool IsChecked { get; set; }
}
The NotesViewModel is populated just fine when it is passed to the view. However when the submit button is clicked, the controller action handling the post has only the value for the Name property of the viewmodel. The Notes property - the list of notes that have been checked/unchecked by the user - is null. I've got a disconnect between the populating of those TextBoxFor and CheckBoxFor elements when the view is displayed and the ViewModel being sent back. Guidance on this?
SOLUTION
Thanks go to Mystere Man for setting me straight on this. As I understand it, essentially by changing my loop to
#Html.EditorFor(m => m.Notes)
changes the underlying HTML, which I understand provides for the proper model binding on the post. Looking at the resulting HTML, I see that I get the following generated for one of the Notes:
<div>
<input id="Notes_0__NoteId" type="hidden" value="1" name="Notes[0].NoteId">
<input id="Notes_0__NoteText" type="text" value="Texture of dessert was good." name="Notes[0].NoteText">
<input id="Notes_0__IsChecked" type="checkbox" value="true" name="Notes[0].IsChecked>
</div>
Which is different than this HTML generated by my original code:
<div>
<input id="item_NoteId" type="hidden" value="1" name="item.NoteId>
<input id="item_NoteText" type="text" value="Texture of dessert was good." name="item.NoteText" >
<input id="item_IsChecked" type="checkbox" value="true" name="item.IsChecked">
</div>
By looping through the Notes, the generated HTML essentially loses any references to the viewmodel's Notes property and while the HTML gets populated correctly, the setting of the checkbox values has no way to communicate their values back to the viewmodel, which I guess is the point of the model binding.
So I learned something, which is good.
You're a smart guy, so look at your view. Then, consider how the HTML gets generated. Then, consider how on postback the Model Binder is supposed to know to re-populate Notes based on the generated HTML.
I think you'll find that your HTML doesn't have enough information in it for the Model Binder to figure it out.
Consider this:
#EditorFor(m => Model.Notes)
Rather than the for loop where you are basically hiding the context from the EditorFor function.
And for those that just want the answer as a for loop:
#for (int x = 0; x < Model.Notes.Count(); x++) {
#Html.HiddenFor(m => m.Notes[x].NoteId)
#Html.EditorFor(m => m.Notes[x].NoteText)
#Html.EditorFor(m => m.Notes[x].IsChecked)
}

Resources