使用FluentValidator时,EditorFor Tag Helper不会呈现验证属性 [英] EditorFor Tag Helper doesn't render validation attributes when using FluentValidator

查看:88
本文介绍了使用FluentValidator时,EditorFor Tag Helper不会呈现验证属性的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有一个像这样的简单表单,它使用@ Html.EditorFor扩展名:

 < form method ="post">@ Html.EditorFor(x => x.SystemSettings.EmailFromAddress)< submit-button title =保存"></submit-button></form> 

我想利用.NET Core的标签帮助器,使我的表单看起来像这样:

 < form method ="post"><编辑器asp-for ="SystemSettings.EmailFromAddress"/>< submit-button title =保存"></submit-button></form> 

我最终也希望拥有自己的自定义标签帮助程序,因此我可以改为执行以下操作:

 <文本框asp-for ="SystemSettings.EmailFromAddress"></文本框> 

我有一个 string 模板,该模板由@ Html.EditorFor扩展名呈现:

  @model字符串< div class ="form-group">< label asp-for ="@ Model" class ="m-b-none"></label>< span asp-description-for ="@ Model" class ="help-block m-b-none small m-t-none"</span>< div class ="input-group"><输入asp-for ="@ Model" class ="form-control"/>< partial name ="_ ValidationIcon"/></div>< span asp-validation-for ="@ Model" class ="validation-message"></span></div> 

为此,我看到有人实现了一个 EditorTagHelper ,如下所示:

  [HtmlTargetElement("editor",TagStructure = TagStructure.WithoutEndTag,属性= ForAttributeName)]公共类EditorTagHelper:TagHelper{私有只读IHtmlHelper _htmlHelper;私有常量字符串ForAttributeName ="asp-for";私有常量字符串TemplateAttributeName ="asp-template";[HtmlAttributeName(ForAttributeName)]公共ModelExpression对于{放;}[HtmlAttributeName(TemplateAttributeName)]公用字串范本{get;放;}[ViewContext][HtmlAttributeNotBound]公共ViewContext ViewContext {放;}公共EditorTagHelper(IHtmlHelper htmlHelper){_htmlHelper = htmlHelper;}公共重写异步任务ProcessAsync(TagHelperContext上下文,TagHelperOutput输出){如果(上下文== null)抛出新的ArgumentNullException(nameof(context));如果(输出== null)抛出新的ArgumentNullException(nameof(output));如果(!output.Attributes.ContainsName(nameof(Template))){output.Attributes.Add(nameof(Template),Template);}output.SuppressOutput();(_htmlHelper as IViewContextAware).Contextualize(ViewContext);output.Content.SetHtmlContent(_htmlHelper.Editor(For.Name,Template));等待Task.CompletedTask;}} 

不过,当我使用EditorTagHelper时,似乎缺少了醒目的Java验证属性:

使用 @ Html.EditorFor ,它将呈现:

 < input class ="form-control valid" type ="text" data-val ="true" data-val-required =电子邮件发件人地址不能为空" id ="SystemSettings_EmailFromAddress" name ="SystemSettings.EmailFromAddress"值="whatever@test.com" aria-required ="true" aria-invalid ="false" aria- describeby ="SystemSettings_EmailFromAddress-error"> 

它具有data-val属性,因此可以应用客户端验证.

当我改用 EditorTagHelper 时,将显示:

 < input class ="form-control valid" type ="text" id ="SystemSettings_EmailFromAddress" name ="SystemSettings.EmailFromAddress" value ="whatever@test.com" aria-invalid ="false"> 

不应用非侵入式验证属性.我正在使用FluentValidation,并且已经指定了AbstractValidator,如下所示:

 公共类SystemSettingsValidator:AbstractValidator< SystemSettings>{公共SystemSettingsValidator(){RuleFor(x => x.EmailFromAddress).NotEmpty().WithMessage(发件人的地址不能为空");}} 

我发现,如果删除了AbstractorValidator并仅向模型属性添加了[Required]属性,则验证可以正常进行.这表明FluentValidation出了点问题.也许存在配置问题.

我正在使用Autofac依赖项注入来扫描我的程序集并注册验证器类型:

  builder.RegisterAssemblyTypes(Assembly.Load(assembly))哪里(t => t.IsClosedTypeOf(typeof(IValidator<>)))).AsImplementedInterfaces().PropertiesAutowired().InstancePerLifetimeScope(); 

这似乎工作正常.以防万一,我还尝试通过如下流利的验证选项注册验证器:

  .AddFluentValidation(fv =>{fv.RegisterValidatorsFromAssemblies(new List< Assembly>{Assembly.GetExecutingAssembly(),Assembly.Load(nameof(Entities))});}) 

这似乎也很好.

要注意的一件事是,我遇到的一个较早的问题是,当包含标签助手时,使用Autofac程序集扫描会破坏应用程序.我添加了一个过滤器,以确保注册这些依赖项时不包括标签助手,例如

  builder.RegisterAutowiredAssemblyInterfaces(Assembly.Load(Web)).Where(x =>!x.Name.EndsWith("TagHelper")); 

我在这里上传了代码的工作示例:

诸如 PropertyName 之类的内容设置为null,并且未为 Editor 设置,而是设置为 EmailFromAddress EditorFor SystemSettings.EmailFromAddress 成为引起我们所看到的行为的潜在原因.

痛苦的部分是,标记助手通过 For 属性具有有效的 ModelExplorer 实例.但是没有内置的规定可以将其提供给html帮助器.

关于分辨率,显而易见的似乎是使用 EditorFor 而不是 Editor ,但这看起来并不容易.可能涉及反射和构建表达式.

另一种选择是,考虑标记帮助器正确解析了 ModelExplorer ,是扩展 HtmlHelper 并覆盖了 GenerateEditor 方法-两者 Editor EditorFor 最终会被调用-因此您可以传入ModelExplorer并解决该问题.

 公共类CustomHtmlHelper:HtmlHelper,IHtmlHelper{公共CustomHtmlHelper(IHtmlGenerator htmlGenerator,ICompositeViewEngine viewEngine,IModelMetadataProvider元数据提供者,IViewBufferScope bufferScope,HtmlEncoder htmlEncoder,UrlEncoder urlEncoder):base(htmlGenerator,viewEngine,metadataProvider,bufferScope,htmlEncoder,urlEncoder){}公共IHtmlContent CustomGenerateEditor(ModelExplorer modelExplorer,字符串htmlFieldName,字符串templateName,对象AdditionalViewData){返回GenerateEditor(modelExplorer,htmlFieldName,templateName,AdditionalViewData);}受保护的重写IHtmlContent GenerateEditor(ModelExplorer modelExplorer,字符串htmlFieldName,字符串templateName,对象AdditionalViewData){返回base.GenerateEditor(modelExplorer,htmlFieldName,templateName,AdditionalViewData);}} 

更新您的标签助手以使用它:

 公共重写异步任务ProcessAsync(TagHelperContext上下文,TagHelperOutput输出){如果(上下文== null)抛出新的ArgumentNullException(nameof(context));如果(输出== null)抛出新的ArgumentNullException(nameof(output));如果(!output.Attributes.ContainsName(nameof(Template))){output.Attributes.Add(nameof(Template),Template);}output.SuppressOutput();(_htmlHelper as IViewContextAware).Contextualize(ViewContext);var customHtmlHelper = _htmlHelper为CustomHtmlHelper;var content = customHtmlHelper.CustomGenerateEditor(For.ModelExplorer,For.Metadata.DisplayName?For.Metadata.PropertyName,Template,null);output.Content.SetHtmlContent(content);等待Task.CompletedTask;} 

最后注册新的帮助者,我说的越早越好

  services.AddScoped< IHtmlHelper,CustomHtmlHelper>(); 

工作解决方案

I have a simple form like this which makes use of the @Html.EditorFor extension:

<form method="post">
    @Html.EditorFor(x => x.SystemSettings.EmailFromAddress)
    <submit-button title="Save"></submit-button>
</form>

I want to take advantage of .NET Core's tag helpers so that my form looks like this instead:

<form method="post">
    <editor asp-for="SystemSettings.EmailFromAddress"/>
    <submit-button title="Save"></submit-button>
</form>

I also eventually would like to have my own custom tag helpers so I can do something like this instead:

<text-box asp-for="SystemSettings.EmailFromAddress"></text-box>

I have a string template which gets rendered by the @Html.EditorFor extension:

@model string
<div class="form-group">
    <label asp-for="@Model" class="m-b-none"></label>
    <span asp-description-for="@Model" class="help-block m-b-none small m-t-none"></span>
    <div class="input-group">
        <input asp-for="@Model" class="form-control" />
        <partial name="_ValidationIcon" />
    </div>
    <span asp-validation-for="@Model" class="validation-message"></span>
</div>

To do that, I saw someone implemented an EditorTagHelper, which looks like this:

[HtmlTargetElement("editor", TagStructure = TagStructure.WithoutEndTag,
    Attributes = ForAttributeName)]
public class EditorTagHelper : TagHelper
{
    private readonly IHtmlHelper _htmlHelper;

    private const string ForAttributeName = "asp-for";
    private const string TemplateAttributeName = "asp-template";

    [HtmlAttributeName(ForAttributeName)]
    public ModelExpression For { get; set; }


    [HtmlAttributeName(TemplateAttributeName)]
    public string Template { get; set; }

    [ViewContext]
    [HtmlAttributeNotBound]
    public ViewContext ViewContext { get; set; }

    public EditorTagHelper(IHtmlHelper htmlHelper)
    {
        _htmlHelper = htmlHelper;
    }

    public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
    {
        if (context == null)
            throw new ArgumentNullException(nameof(context));

        if (output == null)
            throw new ArgumentNullException(nameof(output));

        if (!output.Attributes.ContainsName(nameof(Template)))
        {
            output.Attributes.Add(nameof(Template), Template);
        }

        output.SuppressOutput();

        (_htmlHelper as IViewContextAware).Contextualize(ViewContext);

        output.Content.SetHtmlContent(_htmlHelper.Editor(For.Name, Template));

        await Task.CompletedTask;
    }
}

When I use the EditorTagHelper though, it seems to be missing the unobtrusive Javascript validation attributes:

Using @Html.EditorFor, this gets rendered:

<input class="form-control valid" type="text" data-val="true" data-val-required="Email From Address cannot be empty" id="SystemSettings_EmailFromAddress" name="SystemSettings.EmailFromAddress" value="whatever@test.com" aria-required="true" aria-invalid="false" aria-describedby="SystemSettings_EmailFromAddress-error">

It's got the data-val attributes so client-side validation gets applied.

When I use the EditorTagHelper instead, this gets rendered:

<input class="form-control valid" type="text" id="SystemSettings_EmailFromAddress" name="SystemSettings.EmailFromAddress" value="whatever@test.com" aria-invalid="false">

The unobtrusive validation attributes are not being applied. I am using FluentValidation and I have specified an AbstractValidator like this:

public class SystemSettingsValidator : AbstractValidator<SystemSettings>
{
    public SystemSettingsValidator()
    {
        RuleFor(x => x.EmailFromAddress).NotEmpty()
            .WithMessage("Email From Address cannot be empty");
    }
}

I found that if I removed the AbstractorValidator and simply added a [Required] attribute to my model property the validation then works properly. This suggests that there is something wrong with FluentValidation. Perhaps there is a configuration issue.

I am using Autofac dependency injection to scan my assemblies and register validator types:

builder.RegisterAssemblyTypes(Assembly.Load(assembly))
    .Where(t => t.IsClosedTypeOf(typeof(IValidator<>)))
    .AsImplementedInterfaces()
    .PropertiesAutowired()
    .InstancePerLifetimeScope();

This seems to work fine. In case it wasn't fine, I also tried registering the validators from the fluent validation options like this:

.AddFluentValidation(fv =>
{
    fv.RegisterValidatorsFromAssemblies(new List<Assembly>
        {Assembly.GetExecutingAssembly(), Assembly.Load(nameof(Entities))});
})

This also seemed to be fine.

One thing to note is that an earlier problem I had was that using Autofac assembly scanning was breaking the application when tag helpers were included. I added a filter to ensure that tag helpers are not included when registering these dependencies, e.g.

builder.RegisterAutowiredAssemblyInterfaces(Assembly.Load(Web))
    .Where(x => !x.Name.EndsWith("TagHelper"));

I have uploaded a working sample of the code here: https://github.com/ciaran036/coresample2

Navigate to the Settings Page to see the field I am trying to validate.

This issue also appears to affect view components.

Thanks.

解决方案

I believe the issue is in the tag helper, in that it uses IHtmlHelper.Editor rather than IHtmlHelper<TModel>.EditorFor to generate the HTML content. They are not quite the same.

As you point out FluentValidation injects the validation attributes as you'd expect for @Html.EditorFor(x => x.SystemSettings.EmailFromAddress). However for @Html.Editor("SystemSettings.EmailFromAddress"), which is what your custom tag helper is doing, FluentValidation doesn't inject the validation attributes. So that rules out the tag helper itself and moves the problem to the Editor invocation.

I also noticed that Editor doesn't resolve <label asp-for (or the other <span asp-description-for tag helper you're using) so that suggests it's not a FluentValidation specific issue.

I wasn't able to replicate your success with the Required attribute for the custom tag helper/Editor - the Required attribute only injected the validation attributes when using EditorFor.

The internals for Editor and EditorFor are similar but with a key difference, the way they resolve the ModelExplorer instance used to generate the HTML content differs and I suspect this is the problem. See below for these differences.

Things like PropertyName set to null and Metadata.Property not being set for Editor, but set to EmailFromAddress and SystemSettings.EmailFromAddress for EditorFor are standing out as potential causes for the behaviour we're seeing.

The painful part is the tag helper has a valid ModelExplorer instance via the For property. But there is no built in provision to provide it to the html helper.

As to the resolution, the obvious one seems to be to use EditorFor rather than Editor however it doesn't look easy. It'd likely involve reflection and building an expression.

Another option is, considering the tag helper resolves the ModelExplorer correctly, is to extend HtmlHelper and override the GenerateEditor method - what both Editor and EditorFor end up invoking - so you can pass in the ModelExplorer and work around the problem.

public class CustomHtmlHelper : HtmlHelper, IHtmlHelper
{
    public CustomHtmlHelper(IHtmlGenerator htmlGenerator, ICompositeViewEngine viewEngine, IModelMetadataProvider metadataProvider, IViewBufferScope bufferScope, HtmlEncoder htmlEncoder, UrlEncoder urlEncoder) : base(htmlGenerator, viewEngine, metadataProvider, bufferScope, htmlEncoder, urlEncoder) { }

    public IHtmlContent CustomGenerateEditor(ModelExplorer modelExplorer, string htmlFieldName, string templateName, object additionalViewData)
    {
        return GenerateEditor(modelExplorer, htmlFieldName, templateName, additionalViewData);
    }

    protected override IHtmlContent GenerateEditor(ModelExplorer modelExplorer, string htmlFieldName, string templateName, object additionalViewData)
    {
        return base.GenerateEditor(modelExplorer, htmlFieldName, templateName, additionalViewData);
    }
}

Update your tag helper to use it:

public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
    if (context == null)
        throw new ArgumentNullException(nameof(context));

    if (output == null)
        throw new ArgumentNullException(nameof(output));

    if (!output.Attributes.ContainsName(nameof(Template)))
    {
        output.Attributes.Add(nameof(Template), Template);
    }

    output.SuppressOutput();

    (_htmlHelper as IViewContextAware).Contextualize(ViewContext);

    var customHtmlHelper = _htmlHelper as CustomHtmlHelper;
    var content = customHtmlHelper.CustomGenerateEditor(For.ModelExplorer, For.Metadata.DisplayName ?? For.Metadata.PropertyName, Template, null);
    output.Content.SetHtmlContent(content);

    await Task.CompletedTask;
}

Finally register the new helper, the earlier the better I'd say

services.AddScoped<IHtmlHelper, CustomHtmlHelper>();

Working solution

这篇关于使用FluentValidator时,EditorFor Tag Helper不会呈现验证属性的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆