【问题标题】:Model binder for abstract class in asp.net core mvc 2asp.net core mvc 2中抽象类的模型绑定器
【发布时间】:2018-06-21 05:19:23
【问题描述】:

我一直在尝试为 ASP.NET Core 2 中的抽象类实现模型绑定器,但没有成功。

我特别研究了两篇文章,看起来很不错:

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

Asp net core rc2. Abstract class model binding

我正在努力实现两个目标,

  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>

索引.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:尝试激活“WebApplication1.ActionModelBinder”时无法解析“System.Collections.Generic.Dictionary`2[System.String,Microsoft.AspNetCore.Mvc.ModelBinding.IModelBinder]”类型的服务。

非常感谢您对此提供的任何帮助!

【问题讨论】:

    标签: c# asp.net asp.net-mvc asp.net-core asp.net-core-mvc-2.0


    【解决方案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;
        }
    }
    

    并在配置中注册提供者:

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

    如果有许多抽象类要处理,也可以更改 AbstractModelBinderProvider 以接受要处理的类型的参数化集合而不是泛型类型,以减少提供者的数量。

    关于能够嵌套孩子,必须注意一些限制。

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

    简短的回答是改用部分,如下所示:

    @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

    【讨论】:

    • 嗨! ModelBinder 属性的正确位置是什么?我遇到了同样的错误。
    • 我想不起来了,但我相信你跳过了使用属性,而是让绑定器从 ModelBinderProviders 中解析。
    • 谢谢,这行得通。但是,我仍然有一个模型验证问题。你也有这个问题吗? stackoverflow.com/questions/53334891/…
    • 不,我没有通过数据注释使用客户端验证。我使用 FluentValidation 运行了完整的服务器端验证。
    • 谢谢,我应该看看 FluentValidation :-)
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-04-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-03-14
    相关资源
    最近更新 更多