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

标签 c# asp.net asp.net-mvc model-binding

我想创建一个基于 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/

相关文章:

c# - 在 C# .Net 4.0 中使用旧的 DLL

c# - Linq to object 动态查询是否可能?

javascript - 创建类型为 ="image"的 &lt;input&gt;,它重定向到另一个带参数的 aspx 页面

c# - Entity Framework 计数子实体

asp.net - 如何在我的 vnext 应用程序中启用 roslyn 自动构建

c# - 关于接口(interface)实现和可扩展性的架构问题

c# - CRM 2011,插件中的日期和夏令时

asp.net - asp.net 中的下拉列表值

asp.net-mvc - ASP MVC - 使用参数模型中的列表调用 Controller

asp.net-mvc - 更新到 MVC 5 后,iframe 不再工作