如何处理复杂类型与接口列表和可能没有值的嵌套接口列表的模型绑定

How to handle model binding of Complex Type with List of interfaces and nested list of interfaces with possibly no values

我已经成功(可能不够优雅)创建了一个模型绑定器,它将在 post 上绑定一个接口列表。每个接口都有单独的属性,有些接口有另一个接口的嵌套列表。接口列表在视图中正确显示,嵌套列表项也是如此。在 post 上一切正常,调用自定义模型绑定器并构建正确的类型。让我卡住的问题是,如果嵌套的接口列表没有要显示的项目,在 post 后面,模型活页夹将不会构建该对象以及之后的任何对象。

我正在使用 razor 页面及其各自的页面模型。我利用 [BindProperty] 页面模型中的注释。

接口和对象

通过具体实现精简了接口:我精简了 classes 并使用 ..

省略了不必要的代码
public interface IQuestion
{
    Guid Number{ get; set; }
    string Text{ get; set; }
    List<IAnswer> AnswerList{ get; set; }
    ..
}
public interface IAnswer
    {
        string Label { get; set; }
        string Tag { get; set; }
        ..
    }
public class MetaQuestion: IQuestion
    {
        public int Number{ get; set; }
        public string Text{ get; set; }
        public List<IAnswer> AnswerList{ get; set; }
        ..
    }
public class Answer: IAnswer
    {
        public string Label { get; set; }
        public string Tag { get; set; }
        ..
    }

Razor 页面模型

public class TestListModel : PageModel
    {
        private readonly IDbSession _dbSession;

        [BindProperty]
        public List<IQuestion> Questions { get; set; }

        public TestListModel(IDbSession dbSession)
        {
            _dbSession= dbSession;
        }

        public async Task OnGetAsync()
        {
            //just to demonstrate where the data is comming from
            var allQuestions = await _dbSession.GetAsync<Questions>();

            if (allQuestions == null)
            {
                return NotFound($"Unable to load questions.");
            }
            else
            {                
                Questions = allQuestions;
            }
        }

        public async Task<IActionResult> OnPostAsync()
        {
            //do something random with the data from the post back
            var question = Questions.FirstOrDefault();
            ..          
            return Page();
        }
    }

生成Html

这是生成的html代码,无效。问题项之一,特别是列表中的第二项,在 AnswerList 中没有任何 Answers

如我们所见,列表中的第二个问题在 AnswerList 中没有 'Answer' 项。这意味着在 post 返回时,我只收到列表中的第一个问题。如果我从列表中删除第二个问题,那么我会得到所有问题。

为了简洁起见,我删除了所有样式,classes 和 div。

对于问题 1:

<input id="Questions_0__Number" name="Questions[0].Number" type="text" value="sq1">
<input id="Questions_0__Text" name="Questions[0].Text" type="text" value="Are you:">
<input name="Questions[0].TargetTypeName" type="hidden" value="Core.Model.MetaData.MetaQuestion, Core, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null">
<input data-val="true" data-val-required="The Tag field is required." id="Questions_0__AnswerList_0__Tag" name="Questions[0].AnswerList[0].Tag" type="text" value="1">
<input id="Questions_0__AnswerList_0__Label" name="Questions[0].AnswerList[0].Label" type="text" value="Male">
<input id="Questions_0__AnswerList_0__TargetTypeName" name="Questions[0].AnswerList[0].TargetTypeName" type="hidden" value="Core.Common.Implementations.Answer, Core.Common, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null">

对于问题2:

<input id="Questions_1__Number" name="Questions[1].Number" type="text" value="sq1">
<input id="Questions_1__Text" name="Questions[1].Text" type="text" value="Are you:">
<input name="Questions[1].TargetTypeName" type="hidden" value="Core.Model.MetaData.MetaQuestion, Core, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null">

第2题后的其余题目与第1题类似

自定义模型绑定器和提供程序

我知道这不是最好的方法,包括 TargetTypeName 也不理想。我真的找不到太多可以帮助解决这个问题的东西。关于 ASP 网络开发,我是新手。

public class IQuestionModelBinder : IModelBinder
    {
        private readonly IDictionary<Type, ComplexTypeModelBinder> modelBuilderByType;

        private readonly IModelMetadataProvider modelMetadataProvider;

        public IQuestionModelBinder(IDictionary<Type, ComplexTypeModelBinder> modelBuilderByType, IModelMetadataProvider modelMetadataProvider)
        {
            this.modelBuilderByType = modelBuilderByType ?? throw new ArgumentNullException(nameof(modelBuilderByType));
            this.modelMetadataProvider = modelMetadataProvider ?? throw new ArgumentNullException(nameof(modelMetadataProvider));
        }

        public Task BindModelAsync(ModelBindingContext bindingContext)
        {
            var str = ModelNames.CreatePropertyModelName(bindingContext.ModelName, "TargetTypeName");

            var modelTypeValue = bindingContext.ValueProvider.GetValue(ModelNames.CreatePropertyModelName(bindingContext.ModelName, "TargetTypeName"));

            if (modelTypeValue != null && modelTypeValue.FirstValue != null)
            {
                Type modelType = Type.GetType(modelTypeValue.FirstValue);
                if (this.modelBuilderByType.TryGetValue(modelType, out var modelBinder))
                {
                    ModelBindingContext innerModelBindingContext = DefaultModelBindingContext.CreateBindingContext(
                        bindingContext.ActionContext,
                        bindingContext.ValueProvider,
                        this.modelMetadataProvider.GetMetadataForType(modelType),
                        null,
                        bindingContext.ModelName);

                    modelBinder.BindModelAsync(innerModelBindingContext);

                    bindingContext.Result = innerModelBindingContext.Result;
                    return Task.CompletedTask;
                }
            }

            bindingContext.Result = ModelBindingResult.Failed();
            return Task.CompletedTask;
        }
    }

供应商:

 public class IQuestionModelBinderProvider : IModelBinderProvider
    {
        public IModelBinder GetBinder(ModelBinderProviderContext context)
        {
            if (context == null)
            {
                throw new ArgumentNullException(nameof(context));
            }

            if (context.Metadata.ModelType == typeof(IQuestion))
            {
                var assembly = typeof(IQuestion).Assembly;
                var metaquestionClasses = assembly.GetExportedTypes()
                    .Where(t => !t.IsInterface || !t.IsAbstract)
                    .Where(t => t.BaseType.Equals(typeof(IQuestion)))
                    .ToList();

                var modelBuilderByType = new Dictionary<Type, ComplexTypeModelBinder>();

                foreach (var type in metaquestionClasses)
                {
                    var propertyBinders = new Dictionary<ModelMetadata, IModelBinder>();
                    var metadata = context.MetadataProvider.GetMetadataForType(type);

                    foreach (var property in metadata.Properties)
                    {
                        propertyBinders.Add(property, context.CreateBinder(property));
                    }

                    modelBuilderByType.Add(type, new ComplexTypeModelBinder(propertyBinders: propertyBinders));
                }

                return new IMetaQuestionModelBinder(modelBuilderByType, context.MetadataProvider);
            }

            return null;
        }

类似于 IAnswer 接口(可能重构为没有 2 个绑定器):

  public class IAnswerModelBinder : IModelBinder
    {
        private readonly IDictionary<Type, ComplexTypeModelBinder> modelBuilderByType;

        private readonly IModelMetadataProvider modelMetadataProvider;

        public IAnswerModelBinder(IDictionary<Type, ComplexTypeModelBinder> modelBuilderByType, IModelMetadataProvider modelMetadataProvider)
        {
            this.modelBuilderByType = modelBuilderByType ?? throw new ArgumentNullException(nameof(modelBuilderByType));
            this.modelMetadataProvider = modelMetadataProvider ?? throw new ArgumentNullException(nameof(modelMetadataProvider));
        }

        public Task BindModelAsync(ModelBindingContext bindingContext)
        {
            var str = ModelNames.CreatePropertyModelName(bindingContext.ModelName, "TargetTypeName");

            var modelTypeValue = bindingContext.ValueProvider.GetValue(ModelNames.CreatePropertyModelName(bindingContext.ModelName, "TargetTypeName"));

            if (modelTypeValue != null && modelTypeValue.FirstValue != null)
            {
                Type modelType = Type.GetType(modelTypeValue.FirstValue);
                if (this.modelBuilderByType.TryGetValue(modelType, out var modelBinder))
                {
                    ModelBindingContext innerModelBindingContext = DefaultModelBindingContext.CreateBindingContext(
                        bindingContext.ActionContext,
                        bindingContext.ValueProvider,
                        this.modelMetadataProvider.GetMetadataForType(modelType),
                        null,
                        bindingContext.ModelName);

                    modelBinder.BindModelAsync(innerModelBindingContext);

                    bindingContext.Result = innerModelBindingContext.Result;
                    return Task.CompletedTask;
                }
            }

            bindingContext.Result = ModelBindingResult.Failed();
            return Task.CompletedTask;
        }
    }

供应商:

 public class IAnswerModelBinderProvider : IModelBinderProvider
    {
        public IModelBinder GetBinder(ModelBinderProviderContext context)
        {
            if (context == null)
            {
                throw new ArgumentNullException(nameof(context));
            }

            if (context.Metadata.ModelType == typeof(IAnswer))
            {
                var exportedTypes = typeof(IAnswer).Assembly.GetExportedTypes();

                var metaquestionClasses = exportedTypes
                    .Where(y => y.BaseType != null && typeof(IAnswer).IsAssignableFrom(y) && !y.IsInterface)
                    .ToList();

                var modelBuilderByType = new Dictionary<Type, ComplexTypeModelBinder>();

                foreach (var type in metaquestionClasses)
                {
                    var propertyBinders = new Dictionary<ModelMetadata, IModelBinder>();
                    var metadata = context.MetadataProvider.GetMetadataForType(type);

                    foreach (var property in metadata.Properties)
                    {
                        propertyBinders.Add(property, context.CreateBinder(property));
                    }

                    modelBuilderByType.Add(type, new ComplexTypeModelBinder(propertyBinders: propertyBinders));
                }

                return new IAnswerModelBinder(modelBuilderByType, context.MetadataProvider);
            }

            return null;
        }

这两个注册如下:

  services.AddMvc(
                options =>
                {
                    // add custom binder to beginning of collection (serves IMetaquestion binding)
                    options.ModelBinderProviders.Insert(0, new IMetaQuestionModelBinderProvider());
                    options.ModelBinderProviders.Insert(0, new IAnswerModelBinderProvider());
                })
                .SetCompatibilityVersion(CompatibilityVersion.Version_2_2));

我已尽力提供尽可能多的信息。

我已经处理这个问题好几天了,最终让所有的绑定都可以工作,除了这个案例。

SO post帮助我们走到这一步的人:

我了解模型绑定器与递归一起工作,这让我相信一旦遇到没有 AnswerList 值的 Question 就会停止执行。

我唯一注意到的是 html 中的 AnswerList Tag 属性 将 data-val 设置为 true 并且 data-val-required 还有。

<input data-val="true" data-val-required="The Tag field is required." id="Questions_0__AnswerList_0__Tag" name="Questions[0].AnswerList[0].Tag" type="text" value="1"

我不确定为什么会这样。我没有明确设置这个。 class 位于不同的命名空间中,我们不想在整个 class 中应用数据注释。

这可能是破坏绑定的原因,因为它需要一个值,但我不能确定。

这个问题是正常现象吗?如果是这样,解决方案是什么?

我将继续回答我自己的问题。这解决了问题。 这是我的编辑器模板在 Question

中的样子
@model MetaQuestion
<div class="card card form-group" style="margin-top:10px;">
    <div class="card-header">
        <strong>
            @Html.TextBoxFor(x => x.Number, new { @class = "form-control bg-light", @readonly = "readonly", @style = "border:0px;" })
        </strong>
    </div>
    <div class="card-body text-black-50">
        <h6 class="card-title mb-2 text-muted">
            @Html.TextBoxFor(x => x.Text, new { @class = "form-control", @readonly = "readonly", @style = "background-color:white; border:0px;" })
        </h6>
        @for (int i = 0; i < Model.AnswerList.Count; i++)
        {
        <div class="row">
            <div class="col-1">
                @Html.TextBoxFor(x => x.AnswerList[i].PreCode, new { @class = "form-control", @readonly = "readonly", @style = "background-color:white; border:0px;" })
            </div>
            <div class="col">
                @Html.TextBoxFor(x => x.AnswerList[i].Label, new { @class = "form-control", @readonly = "readonly", @style = "background-color:white; border:0px;" })
            </div>
            <div class="col-1">
                @Html.HiddenFor(x => x.AnswerList[i].TargetTypeName)
            </div>
            <div class="col-1">
                <input name="@(ViewData.TemplateInfo.HtmlFieldPrefix + ".TargetTypeName")" type="hidden" value="@this.Model.GetType().AssemblyQualifiedName" />
            </div>
        </div>
        }
    </div>
</div>

在最后,您可以看到有 2 列包含 HiddenFor 个助手。我正在使用这些来确定接口是什么类型,它允许我的问题中提到的自定义模型绑定器选择相关类型。

对我来说不明显的是,当 'Question' 没有 'Answers' 时,它会忽略 for 循环内部和之后的所有值。因此自定义活页夹永远无法找到 Question 的类型,因为该数据已完全丢失。

我已经着手重新订购 Html.HiddenFor 帮助解决了这个问题。我的编辑器现在看起来如下:

@model MetaQuestion
<div class="card card form-group" style="margin-top:10px;">
    <div class="card-header">
        <input name="@(ViewData.TemplateInfo.HtmlFieldPrefix + ".TargetTypeName")" type="hidden" value="@this.Model.GetType().AssemblyQualifiedName" />
        <strong>
            @Html.TextBoxFor(x => x.Number, new { @class = "form-control bg-light", @readonly = "readonly", @style = "border:0px;" })
        </strong>
    </div>
    <div class="card-body text-black-50">
        <h6 class="card-title mb-2 text-muted">
            @Html.TextBoxFor(x => x.Text, new { @class = "form-control", @readonly = "readonly", @style = "background-color:white; border:0px;" })
        </h6>
        @for (int i = 0; i < Model.AnswerList.Count; i++)
        {
            @Html.HiddenFor(x => x.AnswerList[i].TargetTypeName)
            <div class="row">
                <div class="col-1">
                    @Html.TextBoxFor(x => x.AnswerList[i].PreCode, new { @class = "form-control", @readonly = "readonly", @style = "background-color:white; border:0px;" })
                </div>
                <div class="col">
                    @Html.TextBoxFor(x => x.AnswerList[i].Label, new { @class = "form-control", @readonly = "readonly", @style = "background-color:white; border:0px;" })
                </div>
            </div>
        }
    </div>
</div>

将其放在最前面可确保它始终存在。这可能不是处理整个情况的最佳方法,但至少它解决了问题。