Search code examples
c#asp.net-coretag-helpers

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


Code available here

In an ASP.NET Core 3.0 web application, I have added the following simple tag helper:

[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>");
    }
}

Which I intend to use like this:

<submit-button></submit-button>

However, the act of adding the SubmitButtonTagHelper without even attempting to use it results in the following runtime exception:

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

I have imported the tag helpers by adding this line to the Pages/_ViewImports.cshtml file:

@addTagHelper *, Web

My _Layout.cshtml page looks like this:

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

How am I going wrong here? On debugging, I can see that the a breakpoint is being hit in the SubmitButtonTagHelper but yet I haven't referenced it anywhere? It was my understanding that the [HtmlTargetElement] attribute would mean it would only apply where the element tag was "submit-button". Is that not correct?

I have one other tag helper in my project and I also noticed that the breakpoint in that class is also being hit in places where I haven't referenced it.

I'm surely doing something silly, but what?


Solution

  • ASP.NET Core MVC has the concept of 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>.

    Also from the docs, here's an implementation of a Tag Helper Component:

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

    Once this is registered (shown next), AddressStyleTagHelperComponent will run twice: once for the head element; once for the body element. Here's how it's registered with DI:

    services.AddTransient<ITagHelperComponent, AddressScriptTagHelperComponent>();
    

    At this point (or perhaps even earlier), you're likely thinking I've gone mad. What does any of this have to do with SubmitButtonTagHelper?

    Through its inheritence tree, SubmitButtonTagHelper ends up implementing ITagHelperComponent. If you were to add the following DI registration, SubmitButtonTagHelper would run as a Tag Helper Component, once for head and once for body:

    services.AddTransient<ITagHelperComponent, SubmitButtonTagHelper>();
    

    SubmitButtonTagHelper is destructive, replacing the entire contents of the element it operates on. If it were to replace the content of a body element, the body would, of course, lose its RenderBody directive.

    So, that's a long explanation of what could happen if SubmitButtonTagHelper were registered as a Tag Helper Component. It shouldn't be too surprising that that's exactly what's happening in the sample you've uploaded to GitHub (source):

    private static void WebRegistration(ContainerBuilder builder)
    {
        builder.RegisterAutowiredAssemblyInterfaces(Assembly.Load(Web));
        builder.RegisterAutowiredAssemblyTypes(Assembly.Load(Web));
    }
    

    I don't know much about Autofac, but it's clear that the call to RegisterAutowiredAssemblyInterfaces shown above finds SubmitButtonTagHelper and registers it against all of its interfaces, including ITagHelperComponent.

    Again, I don't know much about Autofac, but, ultimately, Tag Helpers that aren't intended to run as Tag Helper Components should be excluded from this auto-registration process. Here's a suggestion for how to do this filtering, although I expect it's a terrible Autofac approach:

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