我想创建一个基于 CMS 内容的动态表单。我的字段(以及更多属性)的标签将在 CMS 中生成。为了说明,我在 Controller GET 操作中静态创建了 View 模型。使用自定义模型绑定(bind)器,我将为 POST 操作创建相同结构的 View 模型。
这里是 View 模型:
[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; }
}
索引 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" />
}
字段的部分 View :
@model Website.Models.FieldViewModel
@Html.LabelFor(model => model.Value, Model.Label)
@Html.TextBoxFor(model => model.Value)
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);
}
还有我的模型 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;
}
}
GET 操作按预期工作。但是我在绑定(bind)模型时遇到了问题。发布表单后,出现以下错误:
No parameterless constructor defined for this object.
堆栈轨迹:
[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
列表,我希望模型绑定(bind)器更新现有实例而不是创建新的……
在深入了解 DefaultModelBinder
之后,我在创建 innerContext
时在 UpdateCollection
中发现了以下代码行:
ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, elementType)
如果这条线看起来像这样,我认为它会起作用:
ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(existingCollectionElement, elementType)
这是预期的行为吗?我该怎么做才能摆脱这种情况并使用集合中的现有实例?覆盖这似乎不是一个好的选择,因为这段代码是内部代码,我会为这个简单的行复制很多代码。是否有另一种方法可以做我想做的事,或者我做错了什么?
更新:这不是绑定(bind)本身的问题。当我从 FieldViewModel
中删除构造函数时,发布的值会正确添加到 View 模型的 Value
属性中。问题是,label 属性将为空(因为它创建了一个新实例,而不是采用我在自定义模型绑定(bind)器中创建的实例)并将在两个字段中设置为“Value”。最后,FieldViewModel
还应实现 IValidatableObject
接口(interface),并应由 DefaultModelBinder
进行验证。每个字段的可用验证器也在 CMS 中定义,并将添加到自定义模型 Binder 中的 View 模型实例。所以这种情况实际上也不会起作用,因为所有添加的验证器都将丢失。
最佳答案
解决方案是为 IList<FieldViewModel>
创建自定义模型绑定(bind)器并处理列表的模型绑定(bind):
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;
}
}
并将此 Binder 注册到 global.asax
中的列表中:
ModelBinders.Binders.Add(typeof(IList<FieldViewModel>), new ListModelBinder());
关于c# - Modelbinder 创建集合项的新实例而不是更新现有实例,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/23693084/