views:

200

answers:

1

There are a few related questions, but I can't find an answer that works.

Assuming I have the following models:

public class EditorViewModel
{
  public Account Account {get;set;}
  public string SomeSimpleStuff {get;set;}
}

public class Account
{
  public string AccountName {get;set;}
  public int MorePrimitivesFollow {get;set;}
}

and a view that extends ViewPage<EditorViewModel> which does the following:

<%= Html.TextBoxFor(model => model.Account.AccountName)%>
<%= Html.ValidationMessageFor(model => model.Account.AccountName)%>
<%= Html.TextBoxFor(model => model.SomeSimpleStuff )%>
<%= Html.ValidationMessageFor(model => model.SomeSimpleStuff )%>

and my controller looks like:

[HttpPost]
public virtual ActionResult Edit(EditorViewModel account)
{ /*...*/ }

How can I get the DefaultModelBinder to properly bind my EditorViewModel? Without doing anything special, I get an empty instance of my EditorViewModel with everything null or default.

The closest I've come is by calling UpdateModel manually:

[HttpPost]
public virtual ActionResult Edit(EditorViewModel account)
{
    account.Account = new Account();
    UpdateModel(account.Account, "Account");
    // this kills me:
    UpdateModel(account);

This successfully updates my Account property model, but when I call UpdateModel on account (to get the rest of the public properties of my EditorViewModel) I get the completely unhelpful "The model of type ... could not be updated." There is no inner exception, so I can't figure out what's going wrong.

What should I do with this?

+3  A: 

The binder is getting confused because it sees that the parameter to your action method is named account, and it sees incoming form fields named account.accountname, so it's looking for an AccountName property on your EditorViewModel.

You can fix this by renaming the parameter to something else that doesn't conflict with an incoming form field, or you can stick an [Bind(Prefix = "")] attribute on the parameter. This attribute says "ignore the fact that the parameter is named account, and pretend I had given it an empty string name instead." Then the binder will look for account.accountname instead of account.account.accountname.

Edit - further info:

When the binder sees a complex parameter named foo, it looks at the current request for anything named foo.*. So if your parameter were named foo and its type had a property named FirstName, the incoming value would be expected to be foo.FirstName=John, for example.

However, if the binder does not see a foo.* as part of the request, it just looks for the * directly (without the foo prefix). So as long as there wasn't a foo.* present in the request, you could submit FirstName=John and the binder would understand this correctly. But if there is any foo.* as part of the request, the FirstName=John value will not match the FirstName property.

You can see now how giving the parameter to your action method the same name as one of its properties would throw off this logic.

Levi
Yknow, I think you got something there. I'll check it out. Thanks.
Will
Yep, the name was screwing it up. Side effects of magic strings.
Will