Simple problem, but I can't figure out what is missing. I have a simple ViewModel (it'll get bigger):
public class TigerTrackingViewModel
{
public TigerTrackingViewModel()
{
this.TigerTrail = new TigerTrail();
}
public Guid YouthGuid { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public TigerTrail TigerTrail { get; set; }
}
TigerTrail is a nested object. Here are all the properties and subproperties:
public class TigerTrail
{
public TigerTrail()
{
DoneDate = new DateTime(1950, 01, 01);
TigerTrailRequiredBadges = new Collection<TigerTrailRequiredBadge>();
TigerTrailElectivedBadges = new Collection<TigerTrailElectiveBadge>();
}
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<TigerTrailRequiredBadge> TigerTrailRequiredBadges { get; set; }
public virtual ICollection<TigerTrailElectiveBadge> TigerTrailElectivedBadges { get; set; }
//public virtual ICollection<Youth> Youth { get; set; }
public bool? Done { get; set; }
public DateTime? DoneDate { get; set; }
}
So it has TigerTrailRequiredBadges:
public class TigerTrailRequiredBadge
{
public TigerTrailRequiredBadge()
{
DoneDate = new DateTime(1950, 01, 01);
TigerTrailRequiredBadgeSubRequirements = new Collection<TigerTrailRequiredBadgeSubRequirement>();
}
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public virtual ICollection<TigerTrailRequiredBadgeSubRequirement> TigerTrailRequiredBadgeSubRequirements { get; set; }
public bool Done { get; set; }
public DateTime DoneDate { get; set; }
}
And in there is has TigerTrailRequiredBadgeSubRequirement(s):
public class TigerTrailRequiredBadgeSubRequirement
{
public TigerTrailRequiredBadgeSubRequirement()
{
DoneDate = new DateTime(1950, 01, 01);
}
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public string Name { get; set; }
public string ShortCode { get; set; }
public string Type { get; set; } //Family, Den, Go See It
public string Description { get; set; }
public bool Done { get; set; }
public DateTime DoneDate { get; set; }
}
Back in the TigerTrail.cs class, there was also the Elective Badge class:
public class TigerTrailElectiveBadge
{
public TigerTrailElectiveBadge()
{
DoneDate = new DateTime(1950, 01, 01);
}
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public int Number { get; set; }
public string Name { get; set; }
public string Requirement { get; set; }
public bool Done { get; set; }
public DateTime DoneDate { get; set; }
}
So there are ALL the properties that are going to be available via my ViewModel. There are for the most part all need unfortunately. It's big and ugly, but I gotta make it work.
In the Controller GET method:
public ActionResult TigerTrail()
{
var vm = new List<TigerTrackingViewModel>();
var pack = Ctx.CubPacks.FirstOrDefault(x => x.Id == PackId);
var permTrail = Ctx.TigerTrails.FirstOrDefault(x => x.Name.Contains("PERM"));
foreach (var youth in pack.Youths)
{
//if anyone does not have this trail set up, make a new one.
if (youth.TigerTrail == null)
{
youth.TigerTrail = new TigerTrail();
if (youth.TigerTrail.TigerTrailElectivedBadges == null)
{
youth.TigerTrail.TigerTrailElectivedBadges = new Collection<TigerTrailElectiveBadge>();
}
if (youth.TigerTrail.TigerTrailRequiredBadges == null)
{
youth.TigerTrail.TigerTrailRequiredBadges = new Collection<TigerTrailRequiredBadge>();
}
youth.TigerTrail = permTrail;
}
youth.TigerTrail.Name = youth.FirstName + " " + youth.LastName + " Tiger Trail";
vm.Add(new TigerTrackingViewModel
{
FirstName = youth.FirstName,
LastName = youth.LastName,
YouthGuid = youth.YouthGuid,
TigerTrail = youth.TigerTrail
});
}
return View(vm);
}
in the post method:
[HttpPost]
public ActionResult TigerTrail(List<TigerTrackingViewModel> youths)
{
return View();
}
The postback is coming back null every time. Here is the view:
@model List<eTrail.Cubs.ViewModels.TigerTrackingViewModel>
@{
ViewBag.Title = "Award Tracking";
Layout = "~/Areas/App/Views/Shared/_BackendDashboard.cshtml";
}
<h1>Award Tracking</h1>
<hr />
<div class="row">
<div class="span12">
@using (Html.BeginForm("TigerTrail", "Awards", FormMethod.Post, new { @class = "form-horizontal" }))
{
for (int i = 0; i < Model.Count; i++)
{
@Html.HiddenFor(x => Model[i].TigerTrail)
foreach (var item in Model[i].TigerTrail.TigerTrailElectivedBadges)
{
@Html.DisplayFor(x => item.Done)
@Html.DisplayFor(x => item.DoneDate)
@Html.DisplayFor(x => item.Id)
@Html.DisplayFor(x => item.Name)
@Html.DisplayFor(x => item.Number)
@Html.DisplayFor(x => item.Requirement)
}
}
<input type="submit" value="submit" />
}
</div>
</div>
I added all the HiddenFor fields throughout since it was suggested that they all need to be there in order for it to post back. Still no luck. If I view source on the page the id/name(s) are coming out like this:
<li>
<input id="elec_Done" name="elec.Done" type="checkbox" value="true"><input name="elec.Done" type="hidden" value="false">
<b>Pet Care</b>
Visit a veterinarian or animal groomer
<input id="elec_Number" name="elec.Number" type="hidden" value="43">
<input id="elec_DoneDate" name="elec.DoneDate" type="hidden" value="1/1/1950 12:00:00 AM">
</li>
What is getting lost in translation? How can I get a List back to the controller?
EDIT
Based off the two answer since the bounty I need to clarify this: In the httppost method, the List I am supposed to be recieving is coming in null.
[HttpPost]
public ActionResult TigerTrail(List<TigerTrackingViewModel> youths) //this is what is null on postback.
{
. . . Do work with youths . .
return RedirectToAction(...);
}
I've got this working successfully based on your code. The fix to your HttpGet method's view is of the form:
@using (Html.BeginForm("TigerTrail", "Awards", FormMethod.Post, new { @class = "form-horizontal" }))
{
for (int i = 0; i < Model.Count; i++)
{
@Html.EditorFor(x => x[i].FirstName)
for (int j = 0; j < Model[i].TigerTrail.TigerTrailElectivedBadges.Count; ++j)
{
@Html.EditorFor(x => x[i].TigerTrail.TigerTrailElectivedBadges[j].Name)
}
}
<input type="submit" value="submit" />
}
You can then take out a lot of the collection initialization you had added. (Incidentally there's a bug in there - the line youth.TigerTrail = permTrail;
replaces the preceeding empty but initialized object entirely.)
Short Explanation
The trick is to include all the fields you want using EditorFor
or HiddenFor
, as all others will be null / blank / default / empty. Collections do not need to be initialized for MVC model binding these days, they'll be created automatically if there's something to go in them (i.e. something using EditorFor
or HiddenFor
). DisplayFor
will not cause a value to make the round trip; and if no values of an object make the trip, the object won't be in the collection; and if no items in a collection make the round trip, the collection won't be there on postback either unless you force an empty collection.
The syntax is also critical: all the indexing (foo[i].bar[j].baz
) have to be inside the lambda you supply (i.e. on the right side of the =>
before the closing bracket). As in:
@Html.EditorFor(x => x[i].TigerTrail.TigerTrailElectivedBadges[j].Name)
// ^^^ ^^^
To get this to work, you'll have to change from ICollection<T>
and Collection<T>
to IList<T>
and List<T>
so you can index into the badges.
Longer Explanation
As other commenters mentioned, unless you write your own model binder, the only data that will make the round trip from your HttpGet method, via its view's form, through to the HttpPost method, is data which is:
<input ...>
tags whether visible or hidden), andDefaultModelBinder
, which means the input
's name
attribute has to be in a specific format.This means you have to stick with types the DefaultModelBinder
can work with, and be careful with EditorFor
, HiddenFor
and the like, especially for collections of collections.
So if you want any of your TigerTrailViewModel's TigerTrail members to make it across, you need to specify EditorFor
or HiddenFor
for each of those members. You do not need a hidden field for an object itself (e.g. @Html.HiddenFor(x => x.TigerTrail)
. You do need fields for those of its members you want to see on the other side. If you want the list of TigerTrails' elective badges to make it across, then for each field of the badges you want to see, you have to make sure those fields are EditorFor
or HiddenFor
as well using the above syntax.
As a footnote, TextBoxFor
or any other xxxxFor method will work fine too, provided the types match up.
Why do you have to put everything inside the lambda (right of the =>
)? That's down to MVC's use of expression trees, which are probably not worth worrying about unless you're deeply interested, but: the lambda you supply inside the call to EditorFor
or similar is turned into an expression tree rather than a delegate, and MVC parses this compiler-generated data structure at runtime to work out how to write out name
attributes for the inputs it creates, in a way which it can read back later.
If you use EditorFor
as in:
@Html.EditorFor(x => x[i].TigerTrail.TigerTrailElectivedBadges[j].Done)
Then MVC will create inputs a la:
<input class="check-box" name="[6].TigerTrail.TigerTrailElectivedBadges[28].Done" type="checkbox" value="true" />
And that name syntax is the one required by MVC's DefaultModelBinder
.
Incidentally you can manually generate the input tags with those name formats, rather than using EditorFor
etc., if you really want to. This is useful for applications where the user is adding items to a collection, if you have JavaScript which adds the relevant form fields on the fly as they do so. Provided the name format is correct, MVC will find them on postback.
Suggestions (FWIW, which may not be much)
MVC isn't like Web Forms, so there's no view state automatically saving every single item on the page for your use later. This is a good thing as it speeds your application, leads to faster load times and reduces unnecessary internet traffic. It's generally good practice to only include data which the user might change, or which you need to have to just make sense of other values the user has supplied (for example, unique record IDs which will help you find something in the database that the user is working on).
If you want to display or use other data on post back, in general it's better to look it up again (based on IDs etc. which either the user has supplied or which you included in hidden fields) than transmit it on the round trip unncessarily.
So if your nested collections don't make it back on the round trip, it shouldn't matter unless the user can edit that data; as you can pull it out of the database again if you really need it.
For some applications (not the most common with MVC apps), it's certainly conceivable you might want to transmit the entire state of the object (for example, if you've no ability to change a multi-user-unfriendly schema for a database table which the multiple users might be editing, but you want to prevent them overwriting each others' work by saving all values, checking for differences and rejecting edits if they clash). In that sort of case you would have to transmit all the data on the round trip, and you'd need to do that with HiddenFor
and correct syntax, or another mechanism of your own devising.