2

I'm having trouble binding data to a collection item's collection (I'm also having trouble wording my problem correctly). Let's just make thing easier on everyone by using an example with psudo models.

Lets say I have the following example models:

public class Month()
{
    public int ID { get; set; }
    public string Name { get; set; }
    public virtual ICollection<Week> Weeks { get; set; }
}

public class Week()
{
    public int ID { get; set; }
    public int MonthID { get; set; }
    public String Name { get; set; }
    public virtual ICollection<Day> Days { get; set; }
}

public class Day()
{
    public int ID { get; set; }
    public String Name { get; set; }
}

...and an example viewmodel:

public class EditMonthViewModel()
{
    public Month Month { get; set; }
    public List<Week> Weeks { get; set; }
    public List<Day> AllDays { get; set; }
}

The purpose of the Edit Action/View is to enable users to edit a month, the weeks assigned to the month, and add and remove days from weeks of a certain month. A view might help.

@model myProject.ViewModels.EditMonthViewModel

//...
@using (Html.BeginForm())
{
    //Edit Month Stuff...

    @for(int i = 0; i < Model.Weeks.Count(); i++)
    {
        <h2>@Model.Weeks[i].Name</h2>
        @Html.EditorFor(model => Model.Weeks[i].Name)

        //loop through all possible days
        //Select only days that are assigned to Week[i] 
        @for(int d = 0; d < Model.AllDays.Count(); d ++)
        {
            //This is the focus of this question.  
            //How do you bind the data here?
            <input type="checkbox"
                   name="I have no idea" 
                   @Html.Raw(Model.Weeks[i].Days.Contains(Model.AllDays[d]) ? "checked" : "") />
        }
    }
}

Controller Action methods

public ActionResult Edit(int id)
{
    var viewModel = new EditMonthViewModel();
    viewModel.Month = db.Months.Find(id);
    viewModel.Weeks = db.Weeks.Where(w => w.MonthID == id).ToList();

    viewModel.AllDays = db.Days.ToList();
}

[HttpPost]
public ActionResult Edit(EditMonthViewModel viewModel)
{
    var monthToUpdate = db.Months.Find(viewModel.Month.ID);
    //...

    if(viewModel.Weeks != null)
    {
        foreach (var week in viewModel.Weeks)
        {
            var weekToUpdate = monthToUpdate.Weeks.Single(w => w.ID == week.ID);
            //...

            /*So, I have a collection of weeks that I can grab, 
              but how do I know what was selected? My viewModel only has a 
              list of AllDays, not the days selected for Week[i]
            */
        }
}

How can I ensure that when I submit the form the selected days will bind to the week?

John Lieb-Bauman
  • 1,426
  • 12
  • 25
  • What problems/errors are you experienceing? Can we see your controller action which handles this form post? – Jesse Webb Sep 12 '12 at 19:29
  • 1
    I would also be interested in seeing the controller action that populates your ViewModel. It is interesting that your Month class already has a reference to Weeks and an indirect reference to Days (through Weeks) yet you have added these as extra fields to your ViewModel. Why? – Jesse Webb Sep 12 '12 at 19:30
  • I've added some psudo action methods. I'm new to MVC, and I'm following a philosophy that the view should not run queries (send the view everything it needs in a viewmodel). If you dont think it's needed then I can, of course, switch things around. My main problem (which I'm having trouble articulating), is that I dont know what the "name" property of the checkbox needs to be in order for the model binder to assign the day. I will add another small paragraph explaining the situation better. – John Lieb-Bauman Sep 12 '12 at 19:55
  • RE "send the view everything it needs in a viewmodel" - you are already doding this by giving the viewModel a `Month` property. The Month class has a Week collection, which in turn has a Day collection. No need to give the ViewModel extra copies of these collections. – Jesse Webb Sep 12 '12 at 20:04
  • About your problems of knowing how to bind the view inputs to your viewmodel fields, this post explains how to do it effectively. http://stackoverflow.com/questions/5700558/how-can-i-bind-nested-viewmodels-from-view-to-controller-in-mvc3 The jist is that you shouldn't be using loops in your Views, use editorTemplates instead. – Jesse Webb Sep 12 '12 at 20:08
  • Taking a look at this question may help. It seems like the best data structure for you to bind your form to would be a dictionary: http://stackoverflow.com/questions/5191303/asp-net-mvc-binding-to-a-dictionary – Jacob Sep 12 '12 at 20:12
  • Try to keep in mind that this is an example, a metaphor for my real model and real data. I have only included the data that the problem concerns. Are you implying that my problem stems from redundant data in the viewmodel? How would you address the problem I have presented for assigning days? Even if I ignore "weeks" in the view model and access a Month's weeks with Model.Month.Weeks instead of Model.Weeks wont, I still have the same problem with Days? – John Lieb-Bauman Sep 12 '12 at 20:14
  • Problem with collection items to work on default model binder is that all items need to have unique identifier ie. List[u_id].Property1... If that identifier is integer it needs to be in order with no skipping (1,2,3,4...) so adding and removing that would be cumbersome. I advise you to look at my implementation of Sandersons handling of collection items here: http://stackoverflow.com/questions/11267354/how-to-produce-non-sequential-prefix-collection-indices-with-mvc-html-editor-tem/11267659#11267659. – MiBu Sep 12 '12 at 20:23

1 Answers1

0

It looks like the easiest thing to do is to make it a goal for your form to populate a data structure of the type IEnumerable<DayModel>, where DayModel is defined as:

public class DayModel
{
    public int WeekId { get; set; }
    public int DayId { get; set; }
    public bool IsIncluded { get; set; }
}

You could keep your Razor code as is for the most part, but then when it comes to rendering the checkboxes, you can do something like this:

@{
    var modelIdx = 0;
}

// ...

<input type="hidden" name="days[@modelIdx].WeekId" value="@Model.Weeks[i].Id" />
<input type="hidden" name="days[@modelIdx].DayId" value="@Model.AllDays[d].Id" />
<input type="checkbox" name="days[@modelIdx].IsIncluded" value="@(Model.Weeks[i].Days.Contains(Model.AllDays[d]) ? "checked" : "")" />
@{ modelIdx++; }

Then, your controller action you post to could have this signature:

[HttpPost]
public ActionResult Edit(IEnumerable<DayModel> days)
{
    //...
}

Something that helps me is to never confuse view models, which should only be used for the model for views (GET actions generally) and non-view models (what we call plain models). Avoid having your POST actions try to bind to view models, and it will simplify your life greatly.

Jacob
  • 77,566
  • 24
  • 149
  • 228