How to get subcollections on viewmodel on postback in MVC3? - c#

Phil Haack has an article that describes how to set things up so the default model binder will bind to a collection on a post back:
http://haacked.com/archive/2008/10/23/model-binding-to-a-list.aspx
The problem I am having is I am not just trying to send a collection back to the Controller action, but a ViewModel with a collection.
I have a class that basically looks like this:
public class MyViewModel
{
public int IncidentNumber { get; set; }
public string StoreId { get; set; }
public string RepId { get; set; }
public string OrderStatus { get; set; }
public CustomerViewModel Customer { get; set;
//... other properties and SelectLists for binding
public IEnumerable<OrderItemViewModel> OrderItemViewModels { get; set; }
I can actually get the CustomerViewModel data back on a postback, but the list of OrderItemViewModels is empty. How do I get those back? Phil's article isn't helping there.

I had a similar problem here MVC binding to model with list property ignores other properties which was solved by using the following code within the view
<div class="editor-field">
#for (int i = 0; i < Model.MyCollection.Count(); i++)
{
#Html.HiddenFor(m => m.MyCollection[i].Id)
#Html.HiddenFor(m => m.MyCollection[i].ParentId)
#Html.HiddenFor(m => m.MyCollection[i].Name)
#Html.TextBoxFor(m => m.MyCollection[i].Value)
}
</div>

Use an editor template:
#model MyViewModel
#using (Html.BeginForm())
{
... some input fields
#Html.EditorFor(x => x.OrderItemViewModels)
<input type="submit" value="OK" />
}
and then inside the corresponding editor template which will automatically be rendered for each element of the OrderItemViewModels collection (~/Views/Shared/EditorTemplates/OrderItemViewModels.cshtml):
#model OrderItemViewModels
<div>
#Html.LabelFor(x => x.Prop1)
#Html.EditorForFor(x => x.Prop1)
</div>
<div>
#Html.LabelFor(x => x.Prop2)
#Html.EditorForFor(x => x.Prop2)
</div>
...

Related

ASP NET MVC 4 collection is null on post

I read most of Google :-), but I can't proceed. The collection on my object is and stays null on post, whatever I do.
My Model:
public class ArticleViewModel
{
public Guid EventId { get; set; }
public IList<ArticleItemViewModel> ArtikelListe { get; set; }
public decimal GesamtpreisNetto { get; set; }
public decimal MwSt { get; set; }
}
and
public class ArticleItemViewModel
{
public Guid EventId { get; set; }
public Guid Id { get; set; }
public string Artikelname { get; set; }
public string Artikelname_EN { get; set; }
public string Information { get; set; }
public string Information_EN { get; set; }
public decimal Preis { get; set; }
public bool MitAnzahl { get; set; }
public bool IstKategorie { get; set; }
public int Anzahl { get; set; }
public bool Checkbox { get; set; }
public int Reihenfolge { get; set; }
}
My View:
#using (Html.BeginForm("Next", "Article", FormMethod.Post))
{
#Html.HiddenFor(x => x.EventId)
<input type="hidden" name="ArtikelListe" />
for (var i = 0; i < Model.ArtikelListe.Count; i++)
// foreach (EventManager.ViewModels.ArticleItemViewModel artikelItem in Model.ArtikelListe)
{
<div>
<div>
#if (Model.ArtikelListe[i].IstKategorie)
{
#Html.LabelFor(x => x.ArtikelListe[i].Artikelname)<br />
#Html.LabelFor(x => x.ArtikelListe[i].Information)
}
else
{
if (Model.ArtikelListe[i].MitAnzahl)
{
#Html.TextBoxFor(x => x.ArtikelListe[i].Anzahl, new { #class = "field text fn" })
}
else
{
#Html.LabelFor(x => x.ArtikelListe[i].Anzahl)
}
#Html.LabelFor(x => x.ArtikelListe[i].Artikelname)<br />
#Html.LabelFor(x => x.ArtikelListe[i].Information)
}
</div>
</div>
}
On post, I get my Viewmodel back and it has a Collection of ArtikelListe with 15 items (thats correct), but these are all null!
In my HTTP Header I get the following post data:
EventId:824e7f3c-7190-4ebb-aa60-51b57c977b1e
ArtikelListe:
ArtikelListe[1].Anzahl:0
ArtikelListe[2].Anzahl:1
ArtikelListe[3].Anzahl:0
submitButton:Nächste
I wonder why just partial data is sent withon the http post and why all my list items are null. I tried to render by for and foreach. same result.
Any ideas? I'm helpless.
Collection indexers must start at zero and be consecutive (unless you include an Index property).Because of your if statements, you are not necessarily generating a control for the property Anzahl. Looking at your header information, you do not have a value for ArtikelListe[0].Anzahl which means that the first item must have either IstKategorie=true or MitAnzahl=false. You can correct this by adding a hidden input so a value posts back
#if (Model.ArtikelListe[i].IstKategorie)
{
#Html.LabelFor(x => x.ArtikelListe[i].Artikelname)<br />
#Html.LabelFor(x => x.ArtikelListe[i].Information)
#Html.HiddenFor(x => x.ArtikelListe[i].Anzahl) // add this
}
else
{
if (Model.ArtikelListe[i].MitAnzahl)
{
#Html.TextBoxFor(x => x.ArtikelListe[i].Anzahl, new { #class = "field text fn" })
}
else
{
#Html.LabelFor(x => x.ArtikelListe[i].Anzahl)
#Html.HiddenFor(x => x.ArtikelListe[i].Anzahl) // add this
}
#Html.LabelFor(x => x.ArtikelListe[i].Artikelname)<br />
#Html.LabelFor(x => x.ArtikelListe[i].Information)
}
Alternatively you can add an Index property which the DefaultModelBinder uses to match up collection items that are non-consecutive
#if (Model.ArtikelListe[i].IstKategorie)
{
#Html.LabelFor(x => x.ArtikelListe[i].Artikelname)<br />
#Html.LabelFor(x => x.ArtikelListe[i].Information)
}
else
{
if (Model.ArtikelListe[i].MitAnzahl)
{
#Html.TextBoxFor(x => x.ArtikelListe[i].Anzahl, new { #class = "field text fn" })
<input type="hidden" name="x.ArtikelListe.Index" value="#i" /> // add this manually
}
else
{
#Html.LabelFor(x => x.ArtikelListe[i].Anzahl)
}
#Html.LabelFor(x => x.ArtikelListe[i].Artikelname)<br />
#Html.LabelFor(x => x.ArtikelListe[i].Information)
}
With the first option, it will post back all items. In the second case it will post back only items that meet the if conditions.
Note as Sergey noted, you need to also remove <input type="hidden" name="ArtikelListe" />
The issue in this line:
<input type="hidden" name="ArtikelListe" />
When POST request is sent back:
ArtikelListe:
ArtikelListe[1].Anzahl:0
ArtikelListe[2].Anzahl:1
ArtikelListe[3].Anzahl:0
Then ArtikelListe overrides value for the list and that's why it's always null.
So you just need to rename your hidden field to some another name to not conflict with existing names.
Here is working example based on your MVC code in DotNetFiddle - https://dotnetfiddle.net/BCXduq
You can click RUN, then enter some values in two input fields in the right bottom box, and click Save button. And then it will display model that server received in POST as JSON text.
Thank you guys.
The problem was, that I did the testing with the script from the Fiddle. In this code
Model.ArtikelListe[i].MitAnzahl
was always true.
In case it is not true, the value "Anzahl" was not bound to a control containing a value (but just a label).
#Html.LabelFor(x => x.ArtikelListe[i].Anzahl)
As soon as I inserted a hidden field within that scope and bound the "Anzahl" value to it, the post returned with all the data I expected.
Thanks anyway. I learned a lot!

How to bind a model in mvc.net where the model contains a list of dropdowns?

If I have a ViewModel with the following structure:
public class FormViewModel
{
public string Name { get; set; }
public List<OptionGroup> Options { get; set; }
public string Comments { get; set; }
}
public class OptionGroup
{
public string OptionType;
public IEnumerable<SelectListItem> Options { get; set; }
public string SelectedOption;
}
and I want to post to a Controller Method with a signature like:
[HttpPost]
public ActionResult PostForm(FormViewModel model)
{
// do stuff
}
How do I bind the SelectLists in the razor such that the selected values are properly bound when sending back to the server?
My first instinct was to just try:
foreach (OptionGroup optionGroup in Model.Options)
{
<div class="form-group">
<label>#optionGroup.OptionType</label>
#Html.DropDownListFor(m => optionGroup.SelectedOption, optionGroup.Options, optionGroup.OptionType, new { #class = "form-control" })
</div>
}
But that results in not getting any options returns to the server at all.
Then I found this article by Scott Hanselman:
http://www.hanselman.com/blog/ASPNETWireFormatForModelBindingToArraysListsCollectionsDictionaries.aspx
and tried using a for loop instead:
for (int i = 0; i < Model.Options.Count; i++)
{
<div class="form-group">
<label>#Model.Options[i].OptionType</label>
#Html.DropDownListFor(m => m.Options[i].SelectedOption, Model.Options[i].Options, Model.Options[i].OptionType, new { #class = "form-control", name = string.Format("model.Options[{0}].SelectedOption", i) })
</div>
}
This binds the options list, but fails to populate the selected values. (i.e. I get a list of Options on the server, but all the properties of the options are Null)
All the examples I can find are using a simple enumerable as the whole view model - how do you bind to a list of dropdowns when they are only part of the complete ViewModel?
Any pointers would be great,
Cheers!
EDIT
Related: How Do I Model Bind A List Of 'List<SelectItem>' Using MVC.Net which is sending a key value also
and taking into account Stephen Muecke's answer I have tried:
for (int i = 0; i < Model.Options.Count; i++)
{
<div class="form-group">
<label>#Model.Options[i].OptionType</label>
#Html.HiddenFor(m => Model.Options[i].OptionType)
#Html.DropDownListFor(m => m.Options[i].SelectedOption, Model.Options[i].Options, Model.Options[i].OptionType, new { #class = "form-control" })
</div>
}
but still no luck :(
Remove the following from the method
.. name = string.Format("model.Options[{0}].SelectedOption", i) ..
#Html.DropDownList() method will correctly name the select for you, which will be
<select name="Options[0].SelectedOption" ...>
<select name="Options[1].SelectedOption" ...>
// etc
but you are perpending "model." to it so the names will not match up on postback
Your properties for OptionGroup are also missing accessors
public class OptionGroup
{
public string OptionType { get; set; } // add get/set
public IEnumerable<SelectListItem> Options { get; set; }
public string SelectedOption { get; set; } // add get/set
}

View not passing Ienumerable of Model

Edit: Removed Partial View to make things simpler. Now I just need to find out why The View isn't Posting the Values
ViewModelProspectUsers
public class ViewModelProspectUsers
{
public int Id { get; set; }
public string User { get; set; }
public IEnumerable<ViewModelProspectSelect> Prospects { get; set; }
}
ViewModelProspectSelect
public class ViewModelProspectSelect
{
public int ProspectID { get; set; }
public string Name { get; set; }
public bool IsSelected { get; set; }
}
View
#model OG.ModelView.ViewModelProspectUsers
#using (Html.BeginForm())
{
#Html.HiddenFor(model => model.Id)
<h5>Please Select Prospects you wish to assign to this User.</h5>
-----HERE is where the partial used to be, these values aren't being posted to the [Post] Method------
-----------------------------------------However they are populating just fine----------------------------------------
#foreach (var item in Model.Prospects)
{
#Html.HiddenFor(x => item.ProspectID)
#Html.DisplayFor(x => item.Name)
#Html.EditorFor(x => item.IsSelected)
}
#*#Html.Partial("_ShowProspectCheckedForUser", Model.Prospects)*#
#*#Html.Partial("_ShowProspectCheckedForuser", new OG.ModelView.ViewModelProspectSelect())*#
<input type="submit" value="Save changes" />
#Html.ActionLink("Cancel", "Index")
}
Post
[HttpPost]
public ActionResult UsersInProspect(ViewModelProspectUsers viewModel)
If i were to look at viewModel.Prospects(m=>m.isSelected) //<- this value is Null shouldn't be
My viewmodel Variableis showing Data but not for the Ienumerable.
When dealing with list-type objects, you must reference them with array notation to have the field names generated in a way that the modelbinder can parse them back, i.e:
for (var i = 0; i < Model.Count(); i++)
{
#Html.LabelFor(m => Model[i].SomeProperty)
#Html.EditorFor(m => Model[i].SomeProperty)
}
In your scenario, you'd be better served by using a view model to contain your list and adding a Selected property to the items so that you can track which ones were or were not selected.
public class ViewModelProspects
{
public List<ViewModelProspectSelect> Prospects { get; set; }
}
public class ViewModelProspectSelect
{
// Whatever else you have
public bool Selected { get; set; }
}
Then, in your view:
#model ViewModelProspects
#using (Html.BeginForm())
{
for (var i = 0; i < Model.Prospects.Count(); i++)
{
<label>
#Html.HiddenFor(m => Model.Prospects[i].Id)
#Html.CheckboxFor(m => Model.Prospects[i].Selected, true)
#Model.Prospects[i].Name
</label>
}
}
And finally, change your action method signature:
[HttpPost]
public ActionResult UsersInProspect(ViewModelProspects model)
Then, you can easily get the list of selected ids inside the action with:
var selectedIds = model.Prospects.Where(m => m.Selected).Select(m => m.Id)

MVC editorfor not working for collection

I have a model class called Events and a model class called EventRange:
public class Event
{
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public List<EventRange> RangesCollection { get; set; }
}
public class EventRange
{
public int Id { get; set; }
public string RangeName { get; set; }
public string RangeDescription { get; set; }
public int Capacitiy { get; set; }
}
As you can see the Event class contains a List for as many EventRanges as the user should be able to add a lot of EventRanges.
I have created a view called events, which dynamicly appends a partial view for the evenrange. The user can in example press the Add Event Range button 5 times if he want's 5 EventRanges saved.
The Event view:
#using (Ajax.BeginForm("CreateEvent", "Events", new AjaxOptions { HttpMethod = "POST" }, new { #class = "mainForm" }))
{
#*Event data:*#
#Html.LabelFor(m => m.Name)#Html.TextBoxFor(m => m.Name)
#Html.LabelFor(m => m.Description)#Html.TextBoxFor(m => m.Description)
#*EventRange data:*#
<div id="EventRangediv">
#Html.EditorFor(m => m.RangesCollection)
</div>
}
The partial view for the eventrange is saved under "~/views/events/EditorTemplates/EventRange.cshtml"
EventRange.cshtml:
#model fanaticksMain.Models.EventRange
#Html.HiddenFor(m => m.Id)
#Html.DisplayFor(m => m.RangeName)
#Html.LabelFor(m => m.RangeName)#Html.TextBoxFor(m => m.RangeName)
#Html.LabelFor(m => m.RangeDescription)#Html.TextBoxFor(m => m.RangeDescription)
However, loading the events view doesn't load any element from the partialview, the labels and textboxes for the name and description for the event work. But nothing is shown for the eventrange partial view.
Anyone have anyone idea what I'm doing wrong? Also, does anyone have any suggestion if this is the right way to bind a collection for posting a form?
EventRange.cshtml has the model specified as EventRange. m.RangesCollection is a collection of EventRange objects.
I don't see a model that is defined as accepting a collection of EventRange.
try place a for loop in your code to iterate each EventRange and display the EventRange.cshtml editor for each one:
Instead of:
#Html.EditorFor(m => m.RangesCollection)
Try:
#for (int i = 0; i < m.RangeCollection.Count; i++)
{
#Html.EditorFor(m => m.RangeCollection[i])
}
Try placing the "EventRange.cshtml" in the “~/Views/Shared/EditorTemplates/” folder.

.net mvc4 HttpPost null value

I have a problem while passing an object with HttpPost...
Once the form is submitted, the model is set "null" on the controller side, and I don't know where is the issue..
Here is my controller :
public ActionResult AddUser(int id = 0)
{
Group group = db.Groups.Find(id);
List<User> finalList = db.Users.ToList() ;
return View(new AddUserTemplate()
{
group = group,
users = finalList
});
//Everything is fine here, the object is greatly submitted to the view
}
[HttpPost]
public ActionResult AddUser(AddUserTemplate addusertemplate)
{
//Everytime we get in, "addusertemplate" is NULL
if (ModelState.IsValid)
{
//the model is null
}
return View(addusertemplate);
}
Here is AddUserTemplate.cs :
public class AddUserTemplate
{
public Group group { get; set; }
public User selectedUser { get; set; }
public ICollection<User> users { get; set; }
}
Here is the form which return a null value to the controller (note that the dropdown list is greatly populated with the good values) :
#using (Html.BeginForm()) {
<fieldset>
<legend>Add an user</legend>
#Html.HiddenFor(model => model.group)
#Html.HiddenFor(model => model.users)
<div class="editor-field">
//Here, we select an user from Model.users list
#Html.DropDownListFor(model => model.selectedUser, new SelectList(Model.users))
</div>
<p>
<input type="submit" value="Add" />
</p>
</fieldset>
}
Thanks a lot for your help
I tried your code and in my case the addusertemplate model was not null, but its properties were all null.
That's because of a few model binding issues: Html.HiddenFor and Html.DropDownListFor do not work with complex types (such as Group or User) (at least that's how it is by default).
Also, Html.HiddenFor cannot handle collections.
Here's how to solve these issues:
instead of #Html.HiddenFor(model => model.group) there should be one #Html.HiddenFor for each property of the group that you need bound
instead of #Html.HiddenFor(model => model.users) you need to iterate through the list of users and for each object add #Html.HiddenFor for each property of the user that you need bound
instead of #Html.DropDownListFor(model => model.selectedUser [...], create a property like int SelectedUserId {get;set;} and use that in the DropDownList (as it cannot handle complex types).
Here's the code that works:
1. The User and Group classes, as I imagined them to be:
public class User
{
public int Id { get; set; }
public string Name { get; set; }
}
public class Group
{
public int Id { get; set; }
public string Name { get; set; }
}
2. The adjusted AddUserTemplate class:
public class AddUserTemplate
{
public Group Group { get; set; }
public IList<User> Users { get; set; }
public int SelectedUserId { get; set; }
public User SelectedUser
{
get { return Users.Single(u => u.Id == SelectedUserId); }
}
}
The adjustments:
Users was changed from ICollection to IList, because we'll need to access elements by their indexes (see the view code)
added SelectedUserId property, that will be used in the DropDownList
the SelectedUser is not a readonly property, that returns the currently selected User.
3. The adjusted code for the view:
#using (Html.BeginForm())
{
<fieldset>
<legend>Add an user</legend>
#*Hidden elements for the group object*#
#Html.HiddenFor(model => model.Group.Id)
#Html.HiddenFor(model => model.Group.Name)
#*Hidden elements for each user object in the users IList*#
#for (var i = 0; i < Model.Users.Count; i++)
{
#Html.HiddenFor(m => m.Users[i].Id)
#Html.HiddenFor(m => m.Users[i].Name)
}
<div class="editor-field">
#*Here, we select an user from Model.users list*#
#Html.DropDownListFor(model => model.SelectedUserId, new SelectList(Model.Users, "Id", "Name"))
</div>
<p>
<input type="submit" value="Add" />
</p>
</fieldset>
}
Another option that does not require a bunch of hidden fields is to simply specify that you want the model passed to the controller. I think this is much cleaner.
#using(Html. BeginForm("action","controller", Model, FormMethod.Post)){
...
}

Categories