当传入数据与操作参数不同时如何正确实现 DefaultModelBinder

How to propertly Implement DefaultModelBinder when incoming data is different than action argument

我有一个自定义模型活页夹,可以将可以反序列化为 IEnumerable<GenericModel> 的请求转换为操作参数 IEnumerable<IDataItem>。我无法弄清楚要从 DefaultModelBinder 覆盖哪个方法,如何实现它,以及它到底应该做什么 return。

最近几天通过谷歌进行梳理并找到了大量信息,我仍然没有很好地理解虚拟方法 BindModel vs. CreateModel vs. OnModelUpdated vs. 等的明确目的


请求是 json 个 GenericModel 数组,需要将其转换为 IEnumerable<IDataItem> 以执行此操作...

public ActionResult Save(IEnumerable<IDataItem> models)

我的模型活页夹(继承自 DefaultModelBinder)正在实施 CreateModel 并尝试手动将 bindingContext.ModelTypeIDataItem 设置为 GenericModel 以便我可以为每个 属性 正确获取 PropertyDescriptor,获取其值,并为 GenericModel 实例设置属性。一旦我有一个水合的 GenericModel 我会创建正确的混凝土 IDataItem 和 return 它回到 DefaultModelBinder 继续它的绑定和验证(我需要因为一些属性IDataItem 装饰有 ValidationAttributes).

问题是我永远无法弄清楚如何正确地遍历 GenericModel 属性并从 ValueProvider 设置它的值。

我的想法是我可以将 bindingContext.ModelTypeIDataItem 更改为 GenericModel 并遍历它的 PropertyDescriptors,但是 bindingContext.ModelType 没有a setter,虽然它似乎源自 bindingContext.ModelMetadata.Model,但更改无效 - bindingContext.ModelType 仍然是 IDataItem.

public class DataItemModelBinder : DefaultModelBinder {
    protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType) {
        GenericModel model = CreateGenericModel(controllerContext, bindingContext);         
        // just return a dummy instance of IDataItem so I can debug 
        return new TextboxModel();
    }

    private GenericModel CreateGenericModel(ControllerContext controllerContext, ModelBindingContext bindingContext) {
        GenericModel model = new GenericModel();
        // I was thinking this would change bindingContext.ModelType but it doesn't
        bindingContext.ModelMetadata.Model = model;

        foreach (PropertyDescriptor descriptor in GetFilteredModelProperties(controllerContext, bindingContext)) {
            string prefix = CreateSubPropertyPrefix(bindingContext.ModelName, descriptor.Name);
            ModelBindingContext context = new ModelBindingContext {
                ModelMetadata = bindingContext.PropertyMetadata[descriptor.Name],
                ModelName = prefix,
                ModelState = bindingContext.ModelState,
                ValueProvider = bindingContext.ValueProvider
            };
            IModelBinder propertyBinder = Binders.GetBinder(descriptor.PropertyType);
            object value = GetPropertyValue(controllerContext, context, descriptor, propertyBinder);

            // Great, I've set the descriptor, but how do I set the actual model property?
            descriptor.SetValue(context.Model, value);
        }
        return model;
    }
}

我是否覆盖了正确的方法?针对我给定的问题实施解决方案的正确方法是什么?

希望我的情况和问题是清楚的。如果我可以进一步详细说明来回答任何问题,我会:)

我明白了。我离得不远。

为了循环访问一个对象 PropertyDescriptor,您需要从现有的绑定上下文中创建一个新的 ModelBindingContext,并将其传递给您覆盖的方法。这是一个片段,您可以在下面找到完整的答案。

GenericModel model = new ModelBindingContext {
    ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, typeof(GenericModel)),
    ValueProvider = bindingContext.ValueProvider,
    ModelName = bindingContext.ModelName
};

当您遍历属性时,您为每个要迭代的 属性 创建一个新的 ModelBindingContext,获取 IModelBinder,获取 属性 值,最后设置 属性 值,这会滋润您为 ModelBindingContext 创建的 GenericModel

public class DataItemModelBinder: DefaultModelBinder {

    protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType) {
        GenericModel model = CreateGenricModel(controllerContext, bindingContext);
        //this is basically a factory that creates the correct IDataItem 
        // given the newly hydrated GenericModel. It doesn't really apply
        // for the given question/answer so I left it out.
        return CreateDataItem(model);
    }

    private IDataItem CreateDataItem(GenericModel genericModel) {
        IModelRetriever retriever = GetRetreiver(genericModel.DataType);
        return retriever.GetModel(genericModel);
    }

    private GenericModel CreateGenricModel(ControllerContext controllerContext, ModelBindingContext bindingContext) {
        GenericModel model = new GenericModel();
        ModelBindingContext context = CreateGenericFieldBindingContext(bindingContext, model);

        foreach (PropertyDescriptor descriptor in GetFilteredModelProperties(controllerContext, context)) {
            string prefix = CreateSubPropertyPrefix(context.ModelName, descriptor.Name);
            ModelBindingContext propertyContext = new ModelBindingContext {
                ModelMetadata = context.PropertyMetadata[descriptor.Name],
                ModelName = prefix,
                ValueProvider = context.ValueProvider
            };
            IModelBinder propertyBinder = Binders.GetBinder(descriptor.PropertyType);
            object value = GetPropertyValue(controllerContext, propertyContext, descriptor, propertyBinder);

            SetProperty(controllerContext, context, descriptor, value);
        }
        return model;
    }

    private ModelBindingContext CreateGenericFieldBindingContext(ModelBindingContext bindingContext, GenericModel model) {
        return new ModelBindingContext {
            ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, typeof(GenericModel)),
            ValueProvider = bindingContext.ValueProvider,
            ModelName = bindingContext.ModelName
        };
    }

    private string CreateSubPropertyPrefix(string prefix, string propertyName) {
        if (string.IsNullOrEmpty(prefix))
            return propertyName;

        if (string.IsNullOrEmpty(propertyName))
            return prefix;

        return (prefix + "." + propertyName);
    }   
}