30

I have a model object structure with a Foo class that contains a Bar with a string value.

public class Foo
{
    public Bar Bar;
}

public class Bar
{
    public string Value { get; set; }
}

And a view model that uses that structure like this

public class HomeModel
{
    public Foo Foo;
}

I then have a form in view that in Razor looks something like this.

<body>
    <div>
        @using (Html.BeginForm("Save", "Home", FormMethod.Post))
        {
            <fieldset>
                @Html.TextBoxFor(m => m.Foo.Bar.Value)
                <input type="submit" value="Send"/>
            </fieldset>
        }

    </div>
</body>

In html that becomes.

<form action="/Home/Save" method="post">
    <fieldset>
        <input id="Foo_Bar_Value" name="Foo.Bar.Value" type="text" value="Test">
        <input type="submit" value="Send">
    </fieldset>
</form>

Finally the controller to handle the post loos like this

[HttpPost]
public ActionResult Save(Foo foo)
{
    // Magic happends here
    return RedirectToAction("Index");
}

My problem is that Bar in Foo is null once it hits the Save controller action (Foo is created but with an null Bar field).

I thought the model binder in MVC would be able to create the Foo and the Bar object and set the Value property as long as it looks like the above. What am I missing?

I also know my view model is a bit over complicated and could be simpler but I for what I'm trying to do I'd really help me if I could use the deeper object structure. The examples above uses ASP.NET 5.

Riri
  • 11,501
  • 14
  • 63
  • 88
  • 1
    Your post method parameter needs to be `HomeModel` or you need to use the `[Bind(Prefix="foo")]` attribute. But you should never name a parameter in an `ActionResult` method with the same name as a model property (binding will fail) –  Jan 29 '15 at 10:16
  • @StephenMuecke thanks for the response. I did however change to receive `HomeModel` in the `Save` action and changed the parameter name - but still an empty object in Foo ... – Riri Jan 29 '15 at 10:26
  • 4
    That's because its a field, not a property. Add the getter and setter :) –  Jan 29 '15 at 10:28
  • @StephenMuecke dammit ... I thought I could use fields ... Didn't event think of changing that ... Thanks! Just change your comment to a post if you want the points. – Riri Jan 29 '15 at 10:32
  • 1
    @Riri, thanks for giving a simple example. I have to confess I'm getting upset about how often the reverse happens (which is purely the questioner's fault of course). I'm sorry if this is a meta note, but after wading through so many contrary bloated examples on this related question, it was refreshing. – Nicholas Petersen Feb 16 '16 at 22:59

4 Answers4

43

Firstly, the DefaultModelBinder will not bind to fields so you need to use properties

public class HomeModel
{
  public Foo Foo { get; set; }
}

Secondly, the helpers are generating controls based on HomeModel but you posting back to Foo. Either change the POST method to

[HttpPost]
public ActionResult Save(HomeModel model)

or use the BindAttribute to specify the Prefix (which essentially strips the value of prefix from the posted values - so Foo.Bar.Value becomes Bar.Value for the purposes of binding)

[HttpPost]
public ActionResult Save([Bind(Prefix="Foo")]Foo model)

Note also that you should not name the method parameter with the same name as one of your properties otherwise binding will fail and your model will be null.

  • 1
    "should not name the method parameter with the same name as one of your properties otherwise binding will fail and your model will be null." -- Thanks a million for this note! But OH my goodness, what a frustrating gotcha. I'm sure there was some technical difficulty, but surely they could have overcome this unexpected and hard to ever discover limitation. Even with SO, I've read many articles before finding this one, and you could easily not have put this. It seems to me it would have been worth overcoming this limitation. Anyways, thanks a million. – Nicholas Petersen Feb 16 '16 at 22:42
  • 2
    @NicholasPetersen, You right that its frustrating and not obvious, but there is really no way to overcome this because of the way model binding works. The actual behavior is explained in a bit more detail in [this answer](http://stackoverflow.com/questions/34863167/asp-net-mvc-why-is-my-view-passing-null-models-back-to-my-controller/34864052). Its a pity that its not better documented. –  Feb 16 '16 at 22:50
  • Pulled my hair out for the past few hours over the properties vs fields gotcha. I can't believe it was something so simple as that. uggggggh – ferr Jun 27 '17 at 17:07
2

I just discovered another reason this can happen, which is if your property is named Settings! Consider the following View model:

public class SomeVM
{
    public SomeSettings DSettings { get; set; } // named this way it will work

    public SomeSettings Settings { get; set; } // property named 'Settings' won't bind!

    public bool ResetToDefault { get; set; }
}

In code, if you bind to the Settings property, it fails to bind (not just on post but even on generating the form). If you rename Settings to DSettings (etc) it suddenly works again.

Nicholas Petersen
  • 9,104
  • 7
  • 59
  • 69
0

I had the same problem and after I followed @Stephen Muecke steps I realized that the problem was caused because my inputs were disabled (I was disabling them with JQuery on document ready) as you can see it here: How do I submit disabled input in ASP.NET MVC?. At the end I used read-only instead of disabled attribute and all the values were sent successfully to the controller.

Community
  • 1
  • 1
joalcego
  • 1,098
  • 12
  • 17
0

I had the same problem, but once I created a HIDDEN FIELD for the foreign-key...it all worked just fine...

FORM EXAMPLE:

@using (Html.BeginForm("save", "meter", FormMethod.Post))
{
    @Html.AntiForgeryToken()
    @Html.ValidationSummary(true)

    @Html.HiddenFor(model => Model.Entity.Id)
    @Html.HiddenFor(model => Model.Entity.DifferentialMeter.MeterId)
    @Html.HiddenFor(model => Model.Entity.LinearMeter.MeterId)
    @Html.HiddenFor(model => Model.Entity.GatheringMeter.MeterId)

    ... all your awesome controls go here ...
}

ACTION EXAMPLE:

// POST: /Meter/Save
[HttpPost]
public ActionResult Save(Meter entity)
{
    ... world-saving & amazing logic goes here ...
}

PRETTY PICTURES:
enter image description here

Prisoner ZERO
  • 13,848
  • 21
  • 92
  • 137