Modelbinder创建集合项的新实例,而不是更新现有的实例
本文关键字:实例 更新 创建 新实例 Modelbinder 集合 | 更新日期: 2023-09-27 18:03:33
我想创建一个基于CMS内容的动态表单。我的字段的标签(以及更多属性)将在CMS中生成。为了说明这一点,我在控制器GET操作中静态地创建视图模型。使用自定义模型绑定器,我将为POST操作创建相同的视图模型结构。
以下是视图模型:
[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; }
}
索引视图:
@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" />
}
字段的局部视图:
@model Website.Models.FieldViewModel
@Html.LabelFor(model => model.Value, Model.Label)
@Html.TextBoxFor(model => model.Value)
控制器:
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);
}
和我的模型绑定器:
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;
}
}
GET操作按预期工作。但我有一个问题,而绑定模型。张贴表单后,我得到以下错误:
No parameterless constructor defined for this object.
堆栈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
正如错误消息所说,这是因为FieldViewModel
没有默认构造函数。但同时也说,DefaultModelBinder
试图创建一个FieldViewModel
的新实例?这感觉很奇怪,因为我已经创建了我的模型实例,并用FieldViewModel
的2个实例填充了Fields
列表,我希望模型绑定器更新现有的实例,而不是创建新的。
在深入研究DefaultModelBinder
之后,我在创建innerContext
时在UpdateCollection
中发现了以下代码行:
ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, elementType)
如果这一行看起来像这样,我认为它会工作:
ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(existingCollectionElement, elementType)
这是预期的行为吗?我能做些什么来摆脱这个并使用集合中的现有实例?重写这似乎不是一个好选择,因为这段代码是内部的,我将复制许多代码,为这个简单的行。有没有别的方法可以达到我的目的,还是我做错了什么?
Update:这不是绑定本身的问题。当我从FieldViewModel
中删除构造函数时,发布的值被正确地添加到视图模型的Value
属性中。问题是,label属性将为空(因为它创建了一个新实例,而不是我在自定义模型绑定器中创建的实例),并且将在两个字段中设置为"Value"。最后,FieldViewModel
还应该实现IValidatableObject
接口,并由DefaultModelBinder
进行验证。每个字段的可用验证器也在CMS中定义,并将添加到自定义模型绑定器中的视图模型实例中。这种情况也行不通因为所有添加的验证器都会丢失
解决方案是为IList<FieldViewModel>
创建一个自定义模型绑定器,并处理列表的模型绑定:
public class ListModelBinder : System.Web.Mvc.DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var model = bindingContext.Model;
var collectionBindingContext = new ModelBindingContext
{
ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, bindingContext.ModelType),
ModelName = bindingContext.ModelName,
ModelState = bindingContext.ModelState,
PropertyFilter = bindingContext.PropertyFilter,
ValueProvider = bindingContext.ValueProvider
};
return this.UpdateCollection(controllerContext, collectionBindingContext, model.GetType().GetGenericArguments()[0]);
}
private object UpdateCollection(ControllerContext controllerContext, ModelBindingContext bindingContext, Type elementType)
{
var collection = (IList)bindingContext.Model;
var elementBinder = Binders.GetBinder(elementType);
var modelList = new List<object>();
for (var index = 0; index < collection.Count; index++)
{
var innerContext = new ModelBindingContext
{
ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => collection[index], elementType),
ModelName = CreateSubIndexName(bindingContext.ModelName, index),
ModelState = bindingContext.ModelState,
PropertyFilter = bindingContext.PropertyFilter,
ValueProvider = bindingContext.ValueProvider
};
modelList.Add(elementBinder.BindModel(controllerContext, innerContext));
}
return modelList;
}
}
并将此粘合剂注册到global.asax
中的列表中:
ModelBinders.Binders.Add(typeof(IList<FieldViewModel>), new ListModelBinder());
据我所知,你需要动态地添加/编辑一个集合到你的父实体。对于那个,我想你可以检查BeginCollectionItemHelper,它会在这种情况下做。你可以从这里开始。
我想你甚至不需要ModelBinder,然后根据您的图示FieldViewModel,编辑器将看起来像这样:
@using (Html.BeginCollectionItem("Fields"))
{
<div>
@Html.LabelFor(model => model.Value, Model.Label)
@Html.TextBoxFor(model => model.Value)
</div>
}
在主视图中你可以调用:
@foreach (var field in Model.Fields)
{ Html.RenderPartial("FieldEditor", field); }