Adding a simple TagHelper causes this error: "RenderBody has not been called for the page "

Adding a simple TagHelper causes this error: "RenderBody has not been called for the page "

Code available here

在 ASP.NET Core 3.0 网络应用程序中,我添加了以下简单的标签助手:

[HtmlTargetElement("submit-button")]
public class SubmitButtonTagHelper : TagHelper
{
    public string Title { get; set; } = "Submit";
    public string Classes { get; set; }
    public string Id { get; set; }

    public override void Process(TagHelperContext context, TagHelperOutput output)
    {
        output.Content.SetHtmlContent(
            $@"<span class=""btn btn-primary {Classes}"" id=""{Id}"">{Title}</span>");
    }
}

我打算这样使用:

<submit-button></submit-button>

但是,添加 SubmitButtonTagHelper 而没有尝试使用它的行为会导致以下运行时异常:

InvalidOperationException: RenderBody has not been called for the page at '/Views/Shared/_Layout.cshtml'. To ignore call IgnoreBody().

我通过将此行添加到 Pages/_ViewImports.cshtml 文件来导入标签助手:

@addTagHelper *, Web

我的 _Layout.cshtml 页面如下所示:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title><vc:product-name></vc:product-name> @ViewData["Title"]</title>

    @RenderSection("Styles", required: false)

    <partial name="_FrameworkStyles" />
</head>
<body class="top-navigation">

    <div id="wrapper">
        <div id="page-wrapper" class="gray-bg">
            <div class="">
                <vc:Navigation></vc:Navigation>

                @RenderBody()

                <div class="footer"></div>
            </div>
        </div>
    </div>


    <partial name="_FrameworkScripts" />

    @RenderSection("Scripts", required: false)
</body>
</html>

我这里哪里出错了?在调试时,我可以看到在 SubmitButtonTagHelper 中遇到了断点,但我还没有在任何地方引用它?据我了解,[HtmlTargetElement] 属性意味着它仅适用于元素标记为 "submit-button" 的位置。那不对吗?

我的项目中还有另一个标记助手,我还注意到 class 中的断点也在我没有引用它的地方被击中。

我肯定在做一些愚蠢的事情,但是什么?

ASP.NET Core MVC有Tag Helper Components的概念:

A Tag Helper Component is a Tag Helper that allows you to conditionally modify or add HTML elements from server-side code.

...

Tag Helper Components don't require registration with the app in _ViewImports.cshtml.

...

Two common use cases of Tag Helper Components include:

  1. Injecting a <link> into the <head>.
  2. Injecting a <script> into the <body>.

同样来自文档,这里是 Tag Helper 组件的实现:

public class AddressStyleTagHelperComponent : TagHelperComponent
{
    private readonly string _style = 
        @"<link rel=""stylesheet"" href=""/css/address.css"" />";

    public override int Order => 1;

    public override Task ProcessAsync(TagHelperContext context,
                                      TagHelperOutput output)
    {
        if (string.Equals(context.TagName, "head", 
                          StringComparison.OrdinalIgnoreCase))
        {
            output.PostContent.AppendHtml(_style);
        }

        return Task.CompletedTask;
    }
}

一旦注册(如下所示),AddressStyleTagHelperComponent 将 运行 两次:一次用于 head 元素;一次用于 body 元素。这是它在 DI 中的注册方式:

services.AddTransient<ITagHelperComponent, AddressScriptTagHelperComponent>();

此时(或更早),您可能认为我疯了。这与 SubmitButtonTagHelper 有什么关系?

通过其继承树,SubmitButtonTagHelper 最终实现了 ITagHelperComponent。如果您要添加以下 DI 注册,SubmitButtonTagHelper 将 运行 作为 Tag Helper 组件,一次用于 head,一次用于 body:

services.AddTransient<ITagHelperComponent, SubmitButtonTagHelper>();

SubmitButtonTagHelper 是破坏性的,会替换它操作的元素的全部内容。如果要替换 body 元素的内容,正文当然会丢失其 RenderBody 指令。

所以,如果 SubmitButtonTagHelper 被注册为标签助手组件, 可能 会发生什么,这是一个很长的解释。您上传到 GitHub (source):

的示例中确实发生了这种情况,这不足为奇
private static void WebRegistration(ContainerBuilder builder)
{
    builder.RegisterAutowiredAssemblyInterfaces(Assembly.Load(Web));
    builder.RegisterAutowiredAssemblyTypes(Assembly.Load(Web));
}

我对 Autofac 了解不多,但很明显,上面显示的对 RegisterAutowiredAssemblyInterfaces 的调用会找到 SubmitButtonTagHelper 并将其注册到其所有接口,包括 ITagHelperComponent

同样,我对 Autofac 了解不多,但最终,不打算 运行 作为标签助手组件的标签助手应该被排除在这个自动注册过程之外。这是一个关于如何进行这种过滤的建议,尽管我认为这是一种糟糕的 Autofac 方法:

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