Model binder for abstract class in asp.net core mvc 2

喜欢而已 提交于 2019-11-28 14:48:32

I had incorrectly added the ModelBinder attribute to the class upon which I wanted to perform custom binding.

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

    public ActionBase Action { get; set; }
}

This caused the provider code to be bypassed - removing this attribute resolved several issues.

I refactored the provider and binder to be generic so there's no need to duplicate code.

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

And register the providers in configuraton:

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

It's also possible to change the AbstractModelBinderProvider to accept a parameterized collection of types to handle instead of the generic type to reduce the number of providers if there are many abstract classes to handle.

In regards to be able to nest children there are some limitations one must be aware of.

See: In an Editor Template call another Editor Template with the same Model

Short answer is to use partials instead, like this:

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

The BeginCollectionItem is a html helper.

See: https://github.com/danludwig/BeginCollectionItem

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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!