ASP.NET MVC - a form returns a null model, unless the model is wrapped in a custom ViewModel

I have a couple of views in my application that display the same editor template for one of my model elements; of the two views ("Add" and "Change"), "Edit" works fine, but "Add" returns null for the model when my controller action processes the message.

I found that if I give the "Add" view to a custom ViewModel and call Html.EditorFor(p => p.PageContent) , and not just call EditorFor () for the whole Model object - Html.EditorFor(p => p) , then the form will return correct, non-empty, but this causes other problems related to my script and control identifiers on the client side (since now all fields are prefixed with "PageContent_"). I use the same template editor technique in several different places in my application, and none of the others show this odd dependency on ViewModel.

Has anyone else encountered similar problems?

Change 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 (Failure)

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

Before you start crying, "duplicate"

This question refers to this , but this question is intended to focus on the specific problem that I am experiencing, and not on the more general question asked there.

+4
source share
1 answer

I tracked the problem and it is quite interesting.

When DefaultModelBinder tries to resolve a model element, one of the first things it does is to check if there are any prefix fields in the data being bound; he does this by checking for any form elements that begin with the name of the model object (this seems extremely arbitrary if you ask me). If any "prefix" fields are found, then this will cause a different binding logic.

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 that I defined to handle the Add action defines a PageContent element called "content", and in my PageContent domain has a "Content" property that "matches" with the model name "content", as a result of which DefaultModelBinder supposes that I had a prefix value when in fact it was just a PageContent element. By changing the signature -

from

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

in

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

DefaultModelBinder was again able to bind correctly to the PageContent model element. I'm not sure why the Edit view also did not display this behavior, but in any case, I was tracking the source of the problem.

It seems to me that this question is very close to the status of "error". It makes sense that my view first worked with the ViewModel, because the "content" received a prefix with "PageContent_", but the main function / error in this structure should not be inadequate IMHO.

+14
source

All Articles