asp.net 核心 mvc 2 中抽象 class 的模型绑定器
Model binder for abstract class in asp.net core mvc 2
我一直在尝试为 ASP.NET 核心 2 中的抽象 class 实现模型绑定器,但没有成功。
我特别研究了两篇文章,看起来很不错:
http://www.dotnetcurry.com/aspnet-mvc/1368/aspnet-core-mvc-custom-model-binding
Asp net core rc2. Abstract class model binding
我想达到两个目标,
- 根据需要从模型(子嵌套)自动创建尽可能多的嵌套编辑器。
- 将表单值正确映射回模型。
这是我根据上述文章编写的代码。
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 仅呈现第一个动作,即使它有子动作。
当我试图 post 返回(目标 2)时,我得到一个例外:
InvalidOperationException: Unable to resolve service for type 'System.Collections.Generic.Dictionary`2[System.String,Microsoft.AspNetCore.Mvc.ModelBinding.IModelBinder]' while attempting to activate 'WebApplication1.ActionModelBinder'.
非常感谢对此的任何帮助!
我错误地将 ModelBinder 属性添加到 class,我想对其执行自定义绑定。
[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 以接受要处理的参数化类型集合而不是泛型类型,以减少提供者的数量,如果有许多抽象 class 要处理的话。
关于嵌套子项,必须注意一些限制。
参见: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 助手。
我一直在尝试为 ASP.NET 核心 2 中的抽象 class 实现模型绑定器,但没有成功。
我特别研究了两篇文章,看起来很不错:
http://www.dotnetcurry.com/aspnet-mvc/1368/aspnet-core-mvc-custom-model-binding
Asp net core rc2. Abstract class model binding
我想达到两个目标,
- 根据需要从模型(子嵌套)自动创建尽可能多的嵌套编辑器。
- 将表单值正确映射回模型。
这是我根据上述文章编写的代码。
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 仅呈现第一个动作,即使它有子动作。
当我试图 post 返回(目标 2)时,我得到一个例外:
InvalidOperationException: Unable to resolve service for type 'System.Collections.Generic.Dictionary`2[System.String,Microsoft.AspNetCore.Mvc.ModelBinding.IModelBinder]' while attempting to activate 'WebApplication1.ActionModelBinder'.
非常感谢对此的任何帮助!
我错误地将 ModelBinder 属性添加到 class,我想对其执行自定义绑定。
[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 以接受要处理的参数化类型集合而不是泛型类型,以减少提供者的数量,如果有许多抽象 class 要处理的话。
关于嵌套子项,必须注意一些限制。
参见: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 助手。