Frage

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(...);
}
War es hilfreich?

Lösung

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:

  • Explicitly contained within form fields (so HTML <input ...> tags whether visible or hidden), and
  • Is comprehensible by the MVC standard class DefaultModelBinder, 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.

Andere Tipps

It looks like you need to add form fields to bind to the Pack properties. Otherwise there won't be anything to post back to the controller. http is stateless so the original model object is not preserved, so it has to be reconstructed from form fields

The answer what you need is simple and you don't need to use hidden field or something for this.

public ActionResult TigerTrail()
{
    var vm = new List<TigerTrackingViewModel>();
    //other codes
    return View(vm) //Here is the important for you. Returning something inside View()is to show the Model and data.
}

Your other View which uses HttpPost is returning nothing inside View();

[HttpPost]
public ActionResult TigerTrail(List<TigerTrackingViewModel> youths)
{
    return View(); //Here is where you did not give any data/model to the View. So, if you don't return something inside View(), you can not present any data or can not use model in the View.
}

All you have to do is:

[HttpPost]
public ActionResult TigerTrail(List<TigerTrackingViewModel> youths)
{
    return View(youths); //It will work. 
}

Let me know if you need anything else.

[HttpPost]
public ActionResult TigerTrail(List<TigerTrackingViewModel> youths)
{
    return View();
}

I assume that your list is not null so you just forget to pass your model to the view. Like this:

return View(youths);

If you have to much things to write in your hiddenfield think about getting your model everytime from database and use only a hiddenfield id.

You can change some things:

1) In your ViewModel, it is good to initialize your complex properties in the constructor. It is specially true with collections. Somehow the modelbinder seems to not be able to do it. And this ends in a Null property. I think this is NOT your case, since your entire list of VM is null, but once you solve your problem, this issue could appear.

public class TigerTrackingViewModel
{
    public TigerTrackingViewModel(){
       this.TigerTrail = new TigerTrail(); //Add this
    }

    public Guid YouthGuid { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public TigerTrail TigerTrail { get; set; }
}

This should be done for all your complex properties.

2) When displaying a collection of items, you need to render it using an index, and to do that, change the foreach for a normal for:

@for (int i=0; i < Model.Count)
{
    @Html.HiddenFor(x => Model[i].TigerTrail)
    ...
    ...
}

In your case, given the lots of data to display, you should remove all the view code to handle the inner viewmodels... and try if you get the list of VMs posted. Once you have this, start adding the other properties.

Remember: only the information that is in inputs is posted, so... if you need something posted and is not, check if you have it at least as a hidden. The other thing to check, is if your index ([i]) is not correct.

Here you can see an excelent article about binding to lists: http://haacked.com/archive/2008/10/23/model-binding-to-a-list.aspx (it a bit old, but the basics are still applicable )

Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top