Complex object and model binder ASP.NET MVC

2019-01-11 03:21发布

问题:

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.

回答1:

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.



回答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.



回答3:

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.



回答4:

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: