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

时间:2014-05-16 05:16:56

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

我想根据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.

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

正如错误消息所说,这是因为FieldViewModel没有默认构造函数。但也说,DefaultModelBinder尝试创建FieldViewModel的新实例?这感觉很奇怪,因为我创建了我的模型实例,并在Fields列表中填充了FieldViewModel的2个实例,我希望模型绑定器更新现有实例而不是创建新实例... < / p>

在深入DefaultModelBinder后,我在创建UpdateCollection时在innerContext内找到了以下代码行:

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

如果这一行看起来像这样,我认为它会起作用:

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

这是预期的行为吗?我该怎么做才能摆脱这个并在集合中使用我现有的实例?覆盖这似乎不是一个好选择,因为这个代码是内部的,我会复制许多代码,对于这个简单的行。是否有其他方式可以做我想做的事情或者我做错了什么?

更新:这不是绑定本身的问题。当我从FieldViewModel中删除构造函数时,已发布的值会正确添加到视图模型的Value属性中。问题是,label属性将为空(因为它创建了一个新实例而不是我在自定义模型绑定器中创建的实例),并且将设置为&#34; Value&#34;在这两个领域。最后,FieldViewModel还应实现IValidatableObject接口,并应由DefaultModelBinder验证。每个字段的可用验证器也在CMS中定义,并将添加到自定义模型绑定器中的视图模型实例中。因此,这种情况实际上也不会起作用,因为所有添加的验证器都将丢失。

2 个答案:

答案 0 :(得分:1)

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

答案 1 :(得分:0)

据我了解,您需要动态地向父实体添加/编辑集合。 为此我想你可以检查在这种情况下将执行的BeginCollectionItemHelper。 您可以轻松快速启动here

我想你甚至不需要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); }