2

I ran across a problem today where the Code value of a nested object in one of our forms was being changed to an incorrect value. After some digging, I discovered that it is being assigned the value of the Parent object Code, only after POSTing, and only when I try to set the Name attribute explicitly with Html.TextBoxFor's second object parameter.

I setup a simple MVC(Version 5.2.2.0) project to isolate the issue. Here is the code for that.

Models

public class Parent
{
    public string Code { get; set; }
    public Child Child { get; set; } 
}

public class Child
{
    public string Code { get; set; }
}

Controllers

public class ParentController : Controller
{
    public ActionResult Show()
    {
        var child = new Child() { Code = "999"};
        var parent = new Parent() { Code = "1", Child = child };

        return View("Show", parent);
    }

    public ActionResult Update(Parent parent)
    {
        return View("Show", parent);
    }
}

Views/Parent/Show

@model TextBoxForBugTest.Models.Parent

@using (Html.BeginForm("Update", "Parent"))
{
    @Html.TextBoxFor(o => o.Code)
    @Html.Partial("~/Views/Child/Show.cshtml", Model.Child)
    <button type="submit">Submit</button>
}

Views/Child/Show

@model TextBoxForBugTest.Models.Child

@Html.TextBoxFor(o => o.Code, new { Name = "Child.Code" })

When I first load /Parent/Show, I see the correct values in the inputs: 1(Code), and 999(Child.Code).

Before POST

However, after returning from the Update Action Method after submitting the form, Child.Code has been assigned the Value "1" - the Parent Code.

After POST

I've found that I can fix the issue by setting the HtmlFieldPrefix.

@model TextBoxForBugTest.Models.Child

@{ Html.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix = "Child"; }

@Html.TextBoxFor(o => o.Code)

or by using a local variable

@model TextBoxForBugTest.Models.Child

@{ var theCode = Model.Code; }

@Html.TextBoxFor(o => theCode, new { Name = "Child.Code" })

but I'd like to understand why. What is going on here? Why is Child.Code being assigned the value of Parent.Code after POSTing?

I also found some related questions that get into using extensions, but they seem to be answering different questions

ASP.NET MVC partial views: input name prefixes

ASP.MVC 3 Razor Add Model Prefix in the Html.PartialView extension

***Edit - It's clear from the answers that I did a poor job of stating my actual question, so I'll attempt to clarify a bit more here.

The problem I was seeing that was leading to an end user identified bug was that

@Html.TextBoxFor(o => o.Code, new { Name = "Child.Code" })

was generating html with a different "value" the second time it was called(after POSTing).

I was able to solve that problem by setting the Html.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix. Stephen Muecke also pointed out another - probably better - solution to that problem in Editor Templates.

What I was trying to ask though was this:

Why does

@Html.TextBoxFor(o => o.Code, new { Name = "Child.Code" })

generate

<input name="Child.Code" id="Code" type="text" value="999">

the first time (/Parent/Show), but then generate

<input name="Child.Code" id="Code" type="text" value="1">

the second time (after POSTing to /Parent/Update)?

The Form Data that gets POSTed is

enter image description here

and the binded Model in

public ActionResult Update(Parent parent)
{
    return View("Show", parent);
}

has the expected values of Parent.Code == 1 and Child.Code == 999.

I think Stephen Muecke is probably close to the answer I'm looking for in his comment

Note also that the new { Name = "Child.Code" } hack does not change the id attribute and you have invalid html. – Stephen Muecke

Indeed, using

@Html.TextBoxFor(o => o.Code, new { Name = "Child.Code" })

, I end up with 2 inputs with id="Code", which is invalid according to the spec.

Even knowing that though, I still don't understand why the value attribute generated by TextBoxFor is different based on whether I'm GETing /Parent/Show or POSTing to /Parent/Update.

Community
  • 1
  • 1
  • Have you observed the form values as the browser is submitting them (using Firebug or your browser's built-in dev tools)? Sounds like you've caused a duplicate `name=""` value on one of the generated inputs. – Tieson T. Dec 07 '15 at 21:49
  • @TiesonT. looks like that's not the case here. The Form Data that gets posted - according to Chrome - is Code: 1, Child.Code:999. I can also confirm that the model has the correct property values as it leaves the Update method. Good idea though, I hadn't thought to examine the form data in the browser. – richard12511 Dec 08 '15 at 14:25
  • Your problem is rooted in the fact you are doing the wrong thing, as Stephen pointed out. You should *NEVER* override the name attribute unless there is a very specific reason (and just "getting it to work" is not a good reason). MVC provides mechanisms to do what you need to do without fighting against the framework. Basically, you're saying "Doctor, it hurts when I put my hand in this running blender", and the doctor will rightfully say "Why the HECK are you putting your hand in a running blender? Don't do that!" – Erik Funkenbusch Dec 10 '15 at 16:27
  • @ErikFunkenbusch I don't doubt that is the root of my problem. I also don't doubt that overriding the name attribute is a pretty stupid thing to do in this context. Looking back on my past code, it seems that I do stupid things often. I guess I'm asking though, "Why does the running blender hurt me sometimes(when POSTing), and not others(when GETing)?" – richard12511 Dec 10 '15 at 16:41
  • Getting doesn't require model binding. – Erik Funkenbusch Dec 10 '15 at 16:42
  • Ok, that makes sense. I'm still a bit lost though as the binded model that is handed to the View Method in the Update Action Method seems to be exactly the same as the "newed up" Model that is handed to the View Method in the Show Action Method. What is it about model binding that makes those situations different? – richard12511 Dec 10 '15 at 16:56
  • model binding is when you take the key:value pair data being posted by your web browser, and attempting to locate, and instantiate the correct model, and populate its fields. When you pass a model to a view for rendering, it already knows what those fields are, because the model is already populated. – Erik Funkenbusch Dec 10 '15 at 19:31

1 Answers1

4

Your use of @Html.Partial("~/Views/Child/Show.cshtml", Model.Child) is generating an input with

<input name="Code" ... />

whereas it need to be

<input = name="Child.Code" ... />

Do not use a partial to generate form controls. Instead use an EditorTemplate which will generate the correct name attributes with the prefix.

Rename you partial to Child.cshtml and place it in the /Views/Shared/EditorTemplates folder, and in the main view use

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

Edit (based on revised question)

To explain what is happening. When you pass a view to the model and use the HtmlHelpers to generate a form control for a property of your model, the helper first evaluates the expression and gets the ModelMetadata for the property. The ModelMetadata includes the value of the model itself plus addition properties used to determine how you html needs to be generated and how the value of the model is displayed. The helper also adds the htmlAttributes as defined in the 2nd parameter of your TextBoxFor() method.

Now assuming you have edited the value of your second textbox with the value of 999, when you submit the form, its posts back Code=1&Child.Code=999 because you have given the input element an attribute name="Child.Code". The DefaultModelBinder reads the form data, finds a match for both Code and Child.Code in your model and sets their values to 1 and 999 respectively

Now when you return the view your second TextBoxFor() method is binding to property Code (not Child.Code) which has a value of 1 (not 999). Just adding a name attribute does not change the property you binding to (but it does screw up model binding when your send the data back to the controller).

  • Thanks for the suggestion. This gives me another solution to the problem, though I don't think it really answers the question of why I can't set the Name explicitly like I was trying to do. Please correct me if I'm wrong. – richard12511 Dec 08 '15 at 14:09
  • 1
    Sorry I don't understand. Your using the wrong tool for the job. Its not a 'suggestion' - its the correct approach. And in any case what you were claiming is not even correct. If you omit `new { Name = "Child.Code" }` then the value of `Child` will be `null` when you submit and an exception will be thrown when you return the view. If you do include the hack, then the value will be `999` when you submit. If that is not what you are seeing, then you have other code modifying the default MVC behavior. –  Dec 08 '15 at 21:20
  • Note also that the `new { Name = "Child.Code" }` hack does not change the `id` attribute and you have invalid html. –  Dec 08 '15 at 21:26
  • Sorry, it's probably my fault for not asking a clear enough question. I was already able to solve the end user problem by setting the HtmlFieldPrefix. Your answer was helpful to me in that it gave me another - probably better - solution to the end user problem, and for that reason I still upvoted your answer. I've edited the question to hopefully make my actual question more clear. – richard12511 Dec 10 '15 at 16:18
  • Also - just to be clear - this is a new MVC project with no custom code other than the code posted in the original question. – richard12511 Dec 10 '15 at 16:23
  • I have updated the answer with and explanation of the behavior based on your revised question. Its over simplified but hopefully it gives you an understanding of what is happening. If you want to understand more about how model binding works, I suggest you inspect the [source code](https://aspnetwebstack.codeplex.com/SourceControl/latest#src/System.Web.Mvc/Html/InputExtensions.cs) –  Dec 10 '15 at 23:58
  • Interestingly the MVC team included code to try and prevent what your doing (with the following line of code - `tagBuilder.MergeAttribute("name", fullName, true);`) which means if you tried to use `new { name = "Child.Code" }` (lowercase) it would not change the name attribute. Unfortunately, they did not take into account casing so capital N `new { Name = "Child.Code" }` does change it. Hopefully this will be fixed in future releases –  Dec 11 '15 at 00:02