4

I have trouble understanding how asp.net mvc model binders are working.

Models

public class Detail
{
  public Guid Id { get; set; }
  public string Title {get; set; }    
}

public class Master
{
  public Guid Id { get; set;}
  public string Title { get; set; }
  public List<Detail> Details { get; set; }
}

View

 <!-- part of master view in ~/Views/Master/EditMaster.cshtml -->
 @model Master

 @using (Html.BeginForm())
 {
     @Html.HiddenFor(m => m.Id)
     @Html.TextBoxFor(m => m.Title)

     @Html.EditorFor(m => m.Details)

     <!-- snip -->
 }

 <!-- detail view in ~/Views/Master/EditorTemplates/Detail.cshtml -->
 @model Detail

 @Html.HiddenFor(m => m.Id)
 @Html.EditorFor(m => m.Title)

Controller

// Alternative 1 - the one that does not work
public ActionResult Save(Master master)
{
   // master.Details not populated!
}

// Alternative 2 - one that do work
public ActionResult Save(Master master, [Bind(Prefix="Details")]IEnumerable<Detail> details)
{
   // master.Details still not populated, but details parameter is.
}

Rendered html

<form action="..." method="post">
  <input type="hidden" name="Id" value="....">
  <input type="text" name="Title" value="master title">
  <input type="hidden" name="Details[0].Id" value="....">
  <input type="text" name="Details[0].Title value="detail title">
  <input type="hidden" name="Details[1].Id" value="....">
  <input type="text" name="Details[1].Title value="detail title">
  <input type="hidden" name="Details[2].Id" value="....">
  <input type="text" name="Details[2].Title value="detail title">
  <input type="submit">
</form>

Why want the default model binder populate the Details-property on the model? Why do I have to include it as a separate parameter to the controller?

I have read multiple posts about asp and binding to lists, including Haackeds that is referred to multiple times in other questions. It was this thread on SO that lead me to the [Binding(Prefix...)] option. It says that 'the model probably are too complex', but what exactly is 'too complex' for the default model binder to work with?

Community
  • 1
  • 1
Vegar
  • 12,828
  • 16
  • 85
  • 151
  • Have you looked at possible creating your own model binders? Here is a good reference for custom model binders: [From MSDN Magazine](http://msdn.microsoft.com/en-us/magazine/hh781022.aspx) – jacqijvv Feb 21 '14 at 12:22
  • no, I have seen the possibility, but I'm more curious of why asp can't resolve this case by it self... – Vegar Feb 21 '14 at 12:22
  • you should take a look at this old [post](http://haacked.com/archive/2008/10/23/model-binding-to-a-list.aspx/) by Phil Haack, he explains quite clearly what makes a type to complex too bind. But in summary it is almost impossible to dynamically (MVC's DefaultModelBinder) determine that a 'random' collection of **id** and **title** in the Request object, is actually part of a collection, without the use of indices. – jacqijvv Feb 21 '14 at 12:40
  • Eh... when I say 'I have read multiple posts about asp and binding to lists, including Haacks...' it's because I already have read that article... And it's not a 'random' collection of ids and titles. Its multiple ids and titles formatted the way Haack describes it... – Vegar Feb 21 '14 at 12:47
  • Use a collection that implements the IList. `IEnumerable` interface does not have an `Add` method thus the model binder will not bind to your `IEnumerable` – cleftheris Mar 05 '19 at 10:38

2 Answers2

0

The model in your question is not 'too complex'. However a list of a complex type (like your Details object) would be a complex binding.

For complex binding EditorTemplates are used. This editorTemplate specifies how an editor for a complex typ should be rendered. EditorTemplates are being search for in an 'EditorTemplates' folder within the folder the view is located. An editortemplate has to have by default, the name of your complex type class. So in you case you should name it Detail.cshtml

inside your editortemplate you could use something like:

@model Detail

@Html.HiddenFor(m => m.Id)
@Html.TextBoxFor(m => m.Title)

Now when you call@Html.EditorFor(m => m.Details) in your regualar model, for each item in the Details list, the specified editortemplated will be rendered.

In you controller where the action points to, you can just ask for an instance of you Model like:

public ActionResult Save(Master model)
{ 

}

now inside the Save method, model.Details will be filled with the data from your view.

Note that MVC will only return data to your controller that is available within your form. All your data that isn't rendered, or not within the form, will not be returned.

middelpat
  • 2,555
  • 1
  • 20
  • 29
  • this is exactly what I have done. I can see that it's not completely clear. I will edit the question a little... – Vegar Feb 21 '14 at 12:29
  • That would be odd, because this should work. Then we must have missed something. Is this EditorFor(m => m.Details) rendered within your form? – middelpat Feb 21 '14 at 12:32
  • Yes, everything is rendered and every thing is posted back to the server. The difference is that in alternative 1 the detail items is not converted from form data into detail objects, but in alternative 2 they are. – Vegar Feb 21 '14 at 12:34
  • Can you post the html that is being rendered for your form? And how big is your model (without stripping it for the question). Because the model in you question should by default always work. – middelpat Feb 21 '14 at 12:36
  • added relevant parts from the rendered html. – Vegar Feb 21 '14 at 12:59
0

If the data is sent using AJAX, make sure the request Content-Type header is set to application/x-www-form-urlencoded and not another type such as application/json.

Martin D.
  • 1,950
  • 3
  • 23
  • 33