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中定义,并将添加到自定义模型绑定器中的视图模型实例中。这种情况也行不通因为所有添加的验证器都会丢失

Modelbinder创建集合项的新实例,而不是更新现有的实例

解决方案是为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); }