Связыватель модели для абстрактного класса в asp.net core mvc 2

Я пытался реализовать связыватель модели для абстрактного класса в ASP.NET Core 2 без успеха.

В частности, я изучил две статьи, которые выглядят очень хорошо:

http://www.dotnetcurry.com/aspnet-mvc/1368/aspnet-core-mvc-custom-model-binding

Asp Net Core RC2. Связывание абстрактной модели класса

Есть две цели, которые я пытаюсь достичь,

  1. Автоматически создайте столько вложенных редакторов, сколько необходимо из модели (дочернее вложение).
  2. Правильно отобразите значения формы обратно в модель.

Вот мой код, основанный на статьях, упомянутых выше.

public class Trigger
{
    public ActionBase Action { get; set; }
}

[ModelBinder(BinderType = typeof(ActionModelBinder))]
public abstract class ActionBase
{
    public string Type => GetType().FullName;

    public ActionBase Action { get; set; }
}

public class ActionA : ActionBase
{
    public int IntProperty { get; set; }
}

public class ActionB : ActionBase
{
    public string StringProperty { get; set; }
}

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

        if (context.Metadata.ModelType != typeof(ActionBase))
            return null;

        var binders = new Dictionary<string, IModelBinder>();
        foreach (var type in typeof(ActionModelBinderProvider).GetTypeInfo().Assembly.GetTypes())
        {
            var typeInfo = type.GetTypeInfo();
            if (typeInfo.IsAbstract || typeInfo.IsNested)
                continue;

            if (!(typeInfo.IsClass && typeInfo.IsPublic))
                continue;

            if (!typeof(ActionBase).IsAssignableFrom(type))
                continue;

            var metadata = context.MetadataProvider.GetMetadataForType(type);
            var binder = context.CreateBinder(metadata); // This is a BinderTypeModelBinder
            binders.Add(type.FullName, binder);
        }

        return new ActionModelBinder(context.MetadataProvider, binders);
    }
}

public class ActionModelBinder : IModelBinder
{
    private readonly IModelMetadataProvider _metadataProvider;
    private readonly Dictionary<string, IModelBinder> _binders;

    public ActionModelBinder(IModelMetadataProvider metadataProvider, Dictionary<string, IModelBinder> binders)
    {
        _metadataProvider = metadataProvider;
        _binders = binders;
    }

    public async Task BindModelAsync(ModelBindingContext bindingContext)
    {
        var messageTypeModelName = ModelNames.CreatePropertyModelName(bindingContext.ModelName, "Type");
        var messageTypeResult = bindingContext.ValueProvider.GetValue(messageTypeModelName);
        if (messageTypeResult == ValueProviderResult.None)
        {
            bindingContext.Result = ModelBindingResult.Failed();
            return;
        }

        IModelBinder binder;
        if (!_binders.TryGetValue(messageTypeResult.FirstValue, out binder))
        {
            bindingContext.Result = ModelBindingResult.Failed();
            return;
        }

        // Now know the type exists in the assembly.
        var type = Type.GetType(messageTypeResult.FirstValue);
        var metadata = _metadataProvider.GetMetadataForType(type);

        ModelBindingResult result;
        using (bindingContext.EnterNestedScope(metadata, bindingContext.FieldName, bindingContext.ModelName, model: null))
        {
            await binder.BindModelAsync(bindingContext);
            result = bindingContext.Result;
        }

        bindingContext.Result = result;
    }
}

Шаблоны редактора размещены в правильном месте:

ActionA.cshtml

@model WebApplication1.Models.ActionA

<div class="row">
    <h4>Action A</h4>
    <div class="col-md-4">
        <div class="form-group">
            <label asp-for="IntProperty" class="control-label"></label>
            <input asp-for="IntProperty" class="form-control" />
            <span asp-validation-for="IntProperty" class="text-danger"></span>
        </div>
        <div class="form-group">
            <label asp-for="Type" class="control-label"></label>
        </div>
        @Html.EditorFor(x => x.Action)
    </div>
</div>

ActionB.cshtml

@model WebApplication1.Models.ActionB

<div class="row">
    <h4>Action B</h4>
    <div class="col-md-4">
        <div class="form-group">
            <label asp-for="StringProperty" class="control-label"></label>
            <input asp-for="StringProperty" class="form-control" />
            <span asp-validation-for="StringProperty" class="text-danger"></span>
        </div>
        <div class="form-group">
            <label asp-for="Type" class="control-label"></label>
        </div>
        @Html.EditorFor(x => x.Action)
    </div>
</div>

Index.cshtml

@model WebApplication1.Models.Trigger

<h2>Edit</h2>

<h4>Trigger</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form asp-action="Index">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            @Html.EditorFor(x=>x.Action)
            <div class="form-group">
                <input type="submit" value="Save" class="btn btn-default" />
            </div>
        </form>
    </div>
</div>

HomeController.cshtml

public class HomeController : Controller
{
    public IActionResult Index()
    {
        var trigger = new Trigger()
        {
            Action = new ActionA()
            {
                IntProperty = 1,
                Action = new ActionB()
                {
                    StringProperty = "foo"
                }
            }
        };

        return View(trigger);
    }

    [HttpPost]
    public IActionResult Index(Trigger model)
    {
        return View(model);
    }
}

Что касается цели нет. 1 отображается только первое действие, даже если оно имеет дочернее действие.

При попытке отправить обратно (цель № 2) я получаю исключение:

InvalidOperationException: невозможно разрешить службу для типа 'System.Collections.Generic.Dictionary`2[System.String,Microsoft.AspNetCore.Mvc.ModelBinding.IModelBinder]' при попытке активировать "WebApplication1.ActionModelBinder".

Любая помощь в этом с благодарностью!

1 ответ

Решение

Я неправильно добавил атрибут ModelBinder в класс, для которого я хотел выполнить пользовательское связывание.

[ModelBinder(BinderType = typeof(ActionModelBinder))]
public abstract class ActionBase
{
    public string Type => GetType().FullName;

    public ActionBase Action { get; set; }
}

Это привело к тому, что код поставщика был обойден - удаление этого атрибута решило несколько проблем.

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

public class AbstractModelBinderProvider<T> : IModelBinderProvider where T : class
{
    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        if (context == null)
            throw new ArgumentNullException(nameof(context));

        if (context.Metadata.ModelType != typeof(T))
            return null;

        var binders = new Dictionary<string, IModelBinder>();
        foreach (var type in typeof(AbstractModelBinderProvider<>).GetTypeInfo().Assembly.GetTypes())
        {
            var typeInfo = type.GetTypeInfo();
            if (typeInfo.IsAbstract || typeInfo.IsNested)
                continue;

            if (!(typeInfo.IsClass && typeInfo.IsPublic))
                continue;

            if (!typeof(T).IsAssignableFrom(type))
                continue;

            var metadata = context.MetadataProvider.GetMetadataForType(type);
            var binder = context.CreateBinder(metadata);
            binders.Add(type.FullName, binder);
        }

        return new AbstractModelBinder(context.MetadataProvider, binders);
    }
}

public class AbstractModelBinder : IModelBinder
{
    private readonly IModelMetadataProvider _metadataProvider;
    private readonly Dictionary<string, IModelBinder> _binders;

    public AbstractModelBinder(IModelMetadataProvider metadataProvider, Dictionary<string, IModelBinder> binders)
    {
        _metadataProvider = metadataProvider;
        _binders = binders;
    }

    public async Task BindModelAsync(ModelBindingContext bindingContext)
    {
        var messageTypeModelName = ModelNames.CreatePropertyModelName(bindingContext.ModelName, "Type");
        var typeResult = bindingContext.ValueProvider.GetValue(messageTypeModelName);
        if (typeResult == ValueProviderResult.None)
        {
            bindingContext.Result = ModelBindingResult.Failed();
            return;
        }

        IModelBinder binder;
        if (!_binders.TryGetValue(typeResult.FirstValue, out binder))
        {
            bindingContext.Result = ModelBindingResult.Failed();
            return;
        }

        var type = Type.GetType(typeResult.FirstValue);

        var metadata = _metadataProvider.GetMetadataForType(type);

        ModelBindingResult result;
        using (bindingContext.EnterNestedScope(metadata, bindingContext.FieldName, bindingContext.ModelName, model: null))
        {
            await binder.BindModelAsync(bindingContext);
            result = bindingContext.Result;
        }

        bindingContext.Result = result;

        return;
    }
}

И зарегистрируйте провайдеров в configuraton:

services.AddMvc(opts =>
{
    opts.ModelBinderProviders.Insert(0, new AbstractModelBinderProvider<ActionViewModel>());
    opts.ModelBinderProviders.Insert(0, new AbstractModelBinderProvider<TriggerViewModel>());
});

Также можно изменить AbstractModelBinderProvider, чтобы он принимал параметризованную коллекцию типов для обработки вместо универсального типа, чтобы уменьшить количество поставщиков, если есть много абстрактных классов для обработки.

Что касается возможности гнездования детей, существуют некоторые ограничения, о которых нужно знать.

См.: В шаблоне редактора вызовите другой шаблон редактора с той же моделью.

Короткий ответ - использовать вместо этого партиалы, например:

@model ActionViewModel

@if (Model == null)
{
    return;
}

<div class="actionRow">
    @using (Html.BeginCollectionItem("Actions"))
    {
        <input type="hidden" asp-for="Type" />
        <input type="hidden" asp-for="Id" />

        if (Model is CustomActionViewModel)
        {
            @Html.Partial("EditorTemplates/CustomAction", Model);
        }

    }
</div>

BeginCollectionItem является помощником HTML.

Смотрите: https://github.com/danludwig/BeginCollectionItem

И: https://github.com/saad749/BeginCollectionItemCore

Другие вопросы по тегам