views:

951

answers:

1

I have a pair of views in my application that both display the same Editor Template for one of my model items; of the two views ("Add" and "Edit"), "Edit" works fine, but "Add" is returning null for the model when my controller action handles the post.

I found that if I give the "Add" view a custom ViewModel and call Html.EditorFor(p => p.PageContent) rather than simply calling the EditorFor() on the whole Model object- Html.EditorFor(p => p), then the form returns the correct, non-null model, but that generates other problems pertaining to my client-side scripting and control IDs (as now all of the fields are prefixed with "PageContent_"). I am using the same Editor Template technique in a few different places throughout my application and none of the others are exhibiting this odd dependency on a ViewModel.

Has anyone else ever experienced similar problems?

Edit View

<%@ Page Title="" Language="C#" MasterPageFile="~/Areas/Admin/Views/Shared/Site.Master"
Inherits="System.Web.Mvc.ViewPage<PageContent>" %>

<% using (Html.BeginForm())
   { %>
<%=Html.Hidden("PageID", Model.Page.ID) %>
<%=Html.EditorFor(p => p)%>
<input type="submit" name="btnSave" value="Save" />
<input type="submit" name="btnCancel" value="Cancel" class="cancel" />
<% }

Action (Working)

[HttpPost, ValidateInput(false)]
public ActionResult EditContent(int id, FormCollection formCollection) {}


Add View

<%@ Page Title="" Language="C#" MasterPageFile="~/Areas/Admin/Views/Shared/Site.Master"
Inherits="System.Web.Mvc.ViewPage<PageContent>" %>

<% using (Html.BeginForm())
   { %>
<%=Html.Hidden("PageID", ViewData["PageID"]) %>
<%=Html.EditorFor(p => p)%>
<input type="submit" name="btnSave" value="Save" />
<input type="submit" name="btnCancel" value="Cancel" class="cancel" />
<% } %>

Action (Failing)

// content is ALWAYS null
[HttpPost, ValidateInput(false)]
public ActionResult AddContent(PageContent content, FormCollection formCollection) {}


Before you cry "duplicate"

This question does relate to this one, but this question is intended to target the specific problem I am experiencing rather than the more general question asked there.

+1  A: 

I tracked down the problem and it's a rather interesting one. I guarantee no one here would have guessed it.

When the DefaultModelBinder attempts to resolve a model item one of the first things it does is check to see if there are any prefixed fields in the data being bound; it does this by checking for any form items that begin with the name of the model object (this seems extremely arbitrary, if you ask me). If any "prefixed" fields are found then it results in different binding logic being invoked.

ASP.NET MVC 2 Preview 2 BindModel() Source

public virtual object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) {
    if (bindingContext == null) {
        throw new ArgumentNullException("bindingContext");
    }
    bool performedFallback = false;
    if (!String.IsNullOrEmpty(bindingContext.ModelName) && !DictionaryHelpers.DoesAnyKeyHavePrefix(bindingContext.ValueProvider, bindingContext.ModelName)) {
        // We couldn't find any entry that began with the prefix. If this is the top-level element, fall back
        // to the empty prefix.
        if (bindingContext.FallbackToEmptyPrefix) {
             /* omitted for brevity */
            };
            performedFallback = true;
        }
        else {
            return null;
        }
    }

    // Simple model = int, string, etc.; determined by calling TypeConverter.CanConvertFrom(typeof(string))
    // or by seeing if a value in the request exactly matches the name of the model we're binding.
    // Complex type = everything else.
    if (!performedFallback) {
       /* omitted for brevity */
    }
    if (!bindingContext.ModelMetadata.IsComplexType) {
        return null;
    }
    return BindComplexModel(controllerContext, bindingContext);
}

The controller action I defined to handle the Add action defines a PageContent item called "content" and in my domain PageContent has a property called "Content" which "matched" with the model name of "content" thus causing the DefaultModelBinder to assume I had a prefixed value when in fact it was simply a member of PageContent. By changing the signature-

from:

[HttpPost, ValidateInput(false)]
public ActionResult AddContent(PageContent content, FormCollection formCollection) {}

to:

[HttpPost, ValidateInput(false)]
public ActionResult AddContent(PageContent pageContent, FormCollection formCollection) {}

The DefaultModelBinder was once again able to correctly bind to my PageContent model item. I'm not sure why the Edit view didn't also display this behavior, but either way I've tracked down the source of the issue.

It seems to me that this issue falls very close to "bug" status. It makes sense that my view worked initially with the ViewModel because "content" was getting prefixed with "PageContent_", but a core framework feature/bug like this ought not be unaddressed IMHO.

Nathan Taylor
Conventions by their nature are arbitrary. I think I had a similar problem with MVC so I triple check names and prefixes. It seriously makes me want to be able to use Linq for everything.
Min
When you guarantee that no-one would have gotten it, I'm honest in saying that that scenario actually went through my head when I was asking you to post the makeup of the PageContent class ;-) Anyway, glad you figured it out :-)
Charlino
@Charlino Way to defy expectations haha
Nathan Taylor
I agree that it should be addressed. When you explicitly use strongly typed ViewPage, you expect bindings to be strongly typed too.
majocha