jeudi 15 mai 2014

Modelbinder creates new instance of collection item instead of updating existing


Vote count:

0




I want to create a dynamic form based on content from a CMS. The label of my fields (and also many more properties) will be generated in the CMS. To illustrate I statically create the view model within the controller GET action. With a custom model binder I will create the same structure of the view model for POST action.


Here are view models:



[ModelBinder(typeof(TestModelBinder))]
public class TestViewModel
{
public TestViewModel()
{
this.Fields = new List<FieldViewModel>();
}

public string Title { get; set; }

public IList<FieldViewModel> Fields { get; private set; }
}

public class FieldViewModel
{
public FieldViewModel(string label)
{
this.Label = label;
}

public string Label { get; set; }

public string Value { get; set; }
}


The index view:



@model Website.Models.TestViewModel

<h1>Title: @Model.Title</h1>

@using (Html.BeginForm())
{
for (int i = 0; i < Model.Fields.Count; i++)
{
@Html.Partial("Field", Model.Fields[i], new ViewDataDictionary(ViewData)
{
TemplateInfo = new TemplateInfo
{
HtmlFieldPrefix = string.Format("Fields[{0}]", i)
}
})
}

<input type="submit" value="submit" />
}


The partial view for the fields:



@model Website.Models.FieldViewModel

@Html.LabelFor(model => model.Value, Model.Label)
@Html.TextBoxFor(model => model.Value)


The controller:



public ActionResult Index()
{
var model = new TestViewModel { Title = "GET action" };
model.Fields.Add(new FieldViewModel("Name"));
model.Fields.Add(new FieldViewModel("E-Mail"));

return View(model);
}

[HttpPost]
public ActionResult Index(TestViewModel model)
{
return View(model);
}


And my model binder:



public class TestModelBinder : System.Web.Mvc.DefaultModelBinder
{
protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
{
var model = new TestViewModel { Title = "POST action" };
model.Fields.Add(new FieldViewModel("Name"));
model.Fields.Add(new FieldViewModel("E-Mail"));
return model;
}
}


The GET action works as expected. But I have a problem while binding the model. After posting the form, I get the following error:



No parameterless constructor defined for this object.


Stack Strace:



[MissingMethodException: No parameterless constructor defined for this object.]
System.RuntimeTypeHandle.CreateInstance(RuntimeType type, Boolean publicOnly, Boolean noCheck, Boolean& canBeCached, RuntimeMethodHandleInternal& ctor, Boolean& bNeedSecurityCheck) +0
System.RuntimeType.CreateInstanceSlow(Boolean publicOnly, Boolean skipCheckThis, Boolean fillCache, StackCrawlMark& stackMark) +159
System.RuntimeType.CreateInstanceDefaultCtor(Boolean publicOnly, Boolean skipCheckThis, Boolean fillCache, StackCrawlMark& stackMark) +256
System.Activator.CreateInstance(Type type, Boolean nonPublic) +127
System.Activator.CreateInstance(Type type) +11
System.Web.Mvc.DefaultModelBinder.CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType) +275

[MissingMethodException: No parameterless constructor defined for this object. Object type 'Website.Models.FieldViewModel'.]
System.Web.Mvc.DefaultModelBinder.CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType) +370
System.Web.Mvc.DefaultModelBinder.BindComplexModel(ControllerContext controllerContext, ModelBindingContext bindingContext) +151
System.Web.Mvc.DefaultModelBinder.UpdateCollection(ControllerContext controllerContext, ModelBindingContext bindingContext, Type elementType) +569
System.Web.Mvc.DefaultModelBinder.BindComplexModel(ControllerContext controllerContext, ModelBindingContext bindingContext) +974
System.Web.Mvc.DefaultModelBinder.GetPropertyValue(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor, IModelBinder propertyBinder) +33
System.Web.Mvc.DefaultModelBinder.BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor) +441
System.Web.Mvc.DefaultModelBinder.BindProperties(ControllerContext controllerContext, ModelBindingContext bindingContext) +180
System.Web.Mvc.DefaultModelBinder.BindComplexElementalModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Object model) +68
System.Web.Mvc.DefaultModelBinder.BindComplexModel(ControllerContext controllerContext, ModelBindingContext bindingContext) +997
System.Web.Mvc.ControllerActionInvoker.GetParameterValue(ControllerContext controllerContext, ParameterDescriptor parameterDescriptor) +437
System.Web.Mvc.ControllerActionInvoker.GetParameterValues(ControllerContext controllerContext, ActionDescriptor actionDescriptor) +153
System.Web.Mvc.ControllerActionInvoker.InvokeAction(ControllerContext controllerContext, String actionName) +642


As the error message say, this is because FieldViewModel doesn't have a default constructor. But says also, that the DefaultModelBinder tries to create a new instance of a FieldViewModel? This feels weird, because I have created my model instance and filled the Fields list with 2 instances of FieldViewModel and I would expect that the model binder updates the existing instances instead of creating new ones...


After digging into the DefaultModelBinder I found the following line of code within the UpdateCollection while creating the innerContext:



ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, elementType)


If this line would look something like this, I think it would work:



ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(existingCollectionElement, elementType)


Is this the expected behavior? What can I do to get rid of this and use my existing instance within the collection? Overriding this seems not a good option, because this code is internal and I would copy many code, for this simple line. Is there maybe another way of doing what I want or am I doing something wrong?



asked 51 secs ago






Aucun commentaire:

Enregistrer un commentaire