Как обработать привязку модели сложного типа с помощью списка интерфейсов и вложенного списка интерфейсов, возможно, без значений - PullRequest
0 голосов
/ 19 апреля 2019

Я успешно (возможно, не элегантно) создал связыватель модели, который будет связывать Список Интерфейсов при публикации.Каждый интерфейс имеет отдельные свойства, а некоторые имеют вложенный список другого интерфейса.Список интерфейсов отображается правильно в представлении, как и вложенные элементы списка.После публикации все работает, вызывается привязка пользовательских моделей и создаются правильные типы.Проблема в том, что я застрял в том, что если во вложенном Списке интерфейсов нет элементов для отображения, то при повторной публикации связыватель модели не будет создавать этот объект и любые объекты после этого.

Я использую бритвенные страницы иих соответствующие модели страниц.Я использую аннотацию [BindProperty] внутри модели страницы.

Интерфейсы и объекты

Обрезка Интерфейсы с конкретными реализациями: я сократил классы и пропустил ненужный код с помощью ..

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-код, который не работает.Один из пунктов Вопроса, в частности второй элемент в списке, не имеет Answers в AnswerList.

Как мы видим, у второго Вопроса в списке нет пунктов «Ответить» в Списке ответов ».Это означает, что при отправке сообщения я получу только первый Вопрос в списке.Если я удалю второй Вопрос из списка, я получу все вопросы обратно.

Я удалил все стили, классы и элементы 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));

Я пытался предоставить как можно больше информации.

Я занимался этим несколько дней и в итоге получил все привязки, чтобы работать, кроме одного этого случая.

ТАК сообщения, которые помогли получить это далеко:

Я понимаю, что связыватели моделей работают с рекурсией, что приводит меня к мысли, что происходит что-то, что останавливает выполнение, как только оно достигает Question без значений AnswerList.

Единственное, что я заметил, это то, что свойство AnswerList Tag в html имеет 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"

Я не уверен, почему это так.Я не установил это явно.Класс находится в другом пространстве имен, и мы бы предпочли не применять аннотации данных ко всем классам.

Это может быть то, что нарушает привязку, так как она ожидает значения, однако я не уверен.

Это проблема нормального поведения?Если да, то каким может быть решение?

1 Ответ

0 голосов
/ 19 апреля 2019

Я отвечу на свой вопрос.Это решает проблему.Вот как выглядел мой шаблон редактора для 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 помощников.Я использую их, чтобы определить тип интерфейса, который позволяет пользовательским связующим компонентам модели, упомянутым в моем вопросе, выбрать соответствующий тип.

Что для меня не было очевидным, так это то, что когда у «вопроса» не было «ответов»он игнорировал все значения внутри и после цикла 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>

Если поместить его заранее, он всегда будет существовать.Возможно, это не лучший способ справиться со всей этой ситуацией, но, по крайней мере, это решило проблему.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...