如何使用接口列表和嵌套的接口列表(可能没有值)来处理复杂类型的模型绑定 [英] How to handle model binding of Complex Type with List of interfaces and nested list of interfaces with possibly no values

查看:80
本文介绍了如何使用接口列表和嵌套的接口列表(可能没有值)来处理复杂类型的模型绑定的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我已经成功(也许不是很优雅)创建了一个模型绑定器,该绑定器将在发布时绑定接口列表.每个接口都有单独的属性,有些具有另一个接口的嵌套列表.接口列表会正确显示在视图中,嵌套列表项也会正确显示.发布后一切正常,将调用自定义模型联编程序并构建正确的类型.让我困扰的问题是,如果嵌套的接口列表中没有要显示的项目,则在回发时,模型绑定程序将不会建立该对象,并且此后也不会建立任何对象.

I have successfully(maybe not elegantly) created a model binder that will bind a List of Interfaces on post. Each interface has separate properties and some have a nested List of another interface. The list of interfaces get displayed correctly in the view and so do the nested list items. On post everything works, the custom model binders get called and the correct types get built. The issue that has me stuck is that if a nested List of interfaces has no items to display, on post back the model binder will not build that object up and any objects after that.

我正在使用剃刀页面及其各自的页面模型.我利用 页面模型内的[BindProperty]批注.

I am using razor pages and their respective page models. I make use of the [BindProperty] annotation inside the pagemodel.

修剪了具体实现的接口:我修剪了类,并用..省略了不必要的代码.

Trimmed down Interfaces with concrete implementations: I have trimmed down the classes and omitted unnecessary code with ..

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; }
        ..
    }

剃刀页面模型

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.

Generated Html

This is the generated html of the code that does not work. One of the Question items specifically the second item in the list, does not have any Answers in the AnswerList.

我们可以看到,列表中的第二个问题在AnswerList中没有"Answer"项.这意味着回发后,我只会收到列表中的第一个问题.如果我从列表中删除了第二个问题,那么我会把所有问题都找回来.

As we can see, the second Question in the list has no 'Answer' items in the AnswerList'. This means that on post back, I only receive the first Question in the list. If I remove the second Question from the list then I get all the questions back.

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

I have removed all styling, classes and divs for the sake of brevity.

对于问题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相似.

The rest of the questions after question 2 are similar to question 1.

我知道这不是执行此操作的最佳方法,并且将TargetTypeName包括在内并不理想.确实没有什么可以解决这个问题的.我是ASP Web开发人员的新手.

I understand that this isn't the best way to do this, and including the TargetTypeName is not ideal. There really isn't much out there that I could find that helps with this problem. I am newbie when it comes to ASP web dev.

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个活页夹):

Similar for the IAnswer interface (could potentially refactor to not have 2 binders):

  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));

我试图提供尽可能多的信息.

I have tried to provide as much as info as possible.

我已经处理了好几天,最终除了这一种情况之外,所有绑定都可以使用.

I have been on this for days and eventually have got all the bindings to work except for this one case.

SO帖子帮助实现了这一目标:

SO posts that helped get this far:

  • How to extend complextypemodelbinder
  • Post a list interface

我知道模型绑定程序可以递归工作,这使我相信正在发生的事情会在没有AnswerList值的情况下一旦击中Question就会立即停止执行.

I understand that the model binders work with recursion, which leads me to believe that something is happening that is stopping execution as soon as it hits the Question with no AnswerList values.

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

The only thing I noticed is that the AnswerList Tag property in the html has the data-val set to true and data-val-required as well.

<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"

我不确定为什么会这样.我没有明确设置.该类位于不同的命名空间中,我们宁愿不在整个类上应用数据注释.

I am not sure why this would be the case. I have not explicitly set this. The class is in a different namespace and we would rather not apply data annotations all over the classes.

这可能是打破绑定的原因,因为它期望值,但是我不确定.

This could be what is breaking the binding as it is expecting a value, however I cannot be sure.

此问题是否正常?如果是这样,解决方案是什么?

Is this problem normal behaviour? If so what could the solution be?

推荐答案

我将继续回答我自己的问题.这样就解决了问题. 这是Question

I shall proceed to answer my own question. This solves the problem. This is what my editor template looked like for a 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帮助器.我正在使用这些来识别接口是什么类型,这使我的问题中提到的自定义模型绑定程序可以选择相关类型.

Towards the end you can see there are 2 columns that include HiddenFor helpers. I am using these to identify what Type the interface is which allows the custom Model Binders mentioned in my question to pick the relevant type.

对我来说,不明显的是,当问题"没有答案"时,它将忽略for循环内和之后的所有值.因此,自定义资料夹永远无法找到Question的类型,因为该数据完全丢失了.

What was not obvious to me was that when a 'Question' had no 'Answers' it was ignoring all the values within and after the for loop. So the custom binder was never able to find the type of the Question as that data was completely lost.

此后,我开始重新订购已解决问题的Html.HiddenFor助手.现在,我的编辑器如下所示:

I have since proceeded to re-order the Html.HiddenFor helpers which has solved the issue. My editor now looks as follows:

@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>

预先放置它可以确保它始终存在.这可能不是解决整个情况的最佳方法,但是至少它已经解决了问题.

Placing it upfront makes sure it always exists. This may not be the best way to handle this whole situation, but at least it has solved the problem.

这篇关于如何使用接口列表和嵌套的接口列表(可能没有值)来处理复杂类型的模型绑定的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆