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?
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:
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"));