Nesting TagHelpers in ASP.NET Core MVC

2020-02-01 04:07发布

问题:

The ASP.NET Core TagHelper documentation gives the following example:

public class WebsiteContext
{
    public Version Version { get; set; }
    public int CopyrightYear { get; set; }
    public bool Approved { get; set; }
    public int TagsToShow { get; set; }
}

[TargetElement("website-information")]
public class WebsiteInformationTagHelper : TagHelper
{
    public WebsiteContext Info { get; set; }

    public override void Process(TagHelperContext context, TagHelperOutput output)
    {
        output.TagName = "section";
        output.Content.SetContent(
            $@"<ul><li><strong>Version:</strong> {Info.Version}</li>
            <li><strong>Copyright Year:</strong> {Info.CopyrightYear}</li>
            <li><strong>Approved:</strong> {Info.Approved}</li>
            <li><strong>Number of tags to show:</strong> {Info.TagsToShow}</li></ul>");
        output.TagMode = TagMode.StartTagAndEndTag;
    }
}

This can then be used in your Razor .cshtml as follows:

<website-information info="new WebsiteContext {
    Version = new Version(1, 3),
    CopyrightYear = 1790,
    Approved = true,
    TagsToShow = 131 }"/>

This will generate the following HTML:

<section>
    <ul>
        <li><strong>Version:</strong> 1.3</li>
        <li><strong>Copyright Year:</strong> 1790</li>
        <li><strong>Approved:</strong> true</li>
        <li><strong>Number of tags to show:</strong> 131 </li>
    </ul>
</section>

This is pretty ugly tag helper syntax. Is there some way to nest another tag helper and get full intelli-sense so that the only allowed child of website-information can be context? See example below:

<website-information>
    <context version="1.3" copyright="1790" approved tags-to-show="131"/>
</website-information>

In my use case, the website-information element already has many attributes and I want to add one or more separate nested elements.

UPDATE

I have raised this suggestion on the ASP.NET GitHub page to implement this feature for TagHelpers.

回答1:

You can certainly nest tag helpers, although maybe other options like view components, partial views or display templates might be better suited for the scenario described by the OP.

This could be a very simple child tag helper:

[HtmlTargetElement("child-tag", ParentTag="parent-tag")]
public class ChildTagHelper : TagHelper
{
    public string Message { get; set; }

    public override void Process(TagHelperContext context, TagHelperOutput output)
    {
        // Create parent div
        output.TagName = "span";
        output.Content.SetContent(Message);
        output.TagMode = TagMode.StartTagAndEndTag;     
    }
}

And this could also be another simple parent tag helper:

[HtmlTargetElement("parent-tag")]
[RestrictChildren("child-tag")]
public class ParentTagHelper: TagHelper
{
    public string Title { get; set; }

    public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
    {            
        output.TagName = "div";

        // Add some specific parent helper html
        var header = new TagBuilder("h1");
        header.Attributes.Add("class", "parent-title");
        header.InnerHtml.Append(this.Title);
        output.PreContent.SetContent(header);

        // Set the inner contents of this helper(Will process any nested tag helpers or any other piece of razor code)
        output.Content.SetContent(await output.GetChildContentAsync());            
    }
}

In a razor view you could then write the following:

<parent-tag title="My Title">
    <child-tag message="This is the nested tag helper" />
</parent-tag>

Which would be rendered as:

<div>
    <h1 class="parent-title">My Title</h1>
    <span>This is the nested tag helper</span>
</div>

You can optionally enforce tag helpers to be nested in a particular way:

  • Use [RestrictChildren("child-tag", "another-tag")] in the parent tag helper to limit the allowed nested tag helpers
  • Use the ParentTag parameter when declaring the child tag helper to enforce the tag being nested inside a particular parent tag [HtmlTargetElement("child-tag", ParentTag = "parent-tag")]


回答2:

I did not find a good example of multiply nested tag helpers on the web; so, I created one at MultiplyNestedTagHelpers GitHub Repository.

When working with more than one level of nested tag helper, it is important to setup a "context" class for each tag that can contain child tags. These context classes are used by the child tags to write their output. The context class is a regular, POCO class that has a property for each child tag. The properties may be strings, but I use StringBuilder. For example,

public class MyTableContext{
    public StringBuilder TableHeaderBuilder { get; set; } = new StringBuilder();
    public StringBuilder TableBodyBuilder { get; set; } = new StringBuilder();
}
public class MyTableHeaderContext {
    public StringBuilder RowBuilder { get; set; } = new StringBuilder();
}
//...etc.

In each parent tag's Process method, you need to instantiate the parent's associated context class and add this new object to the TagHelperContext object's Items collection. For example:

    //create context for this tag helper
    var tableContext = new MyTableContext();
    context.Items.Add(typeof(MyTableContext), tableContext);

In the child tag's Process method, you write to the parent's registered context object like this:

    //get reference to parent context, and append content to the relevant builder
    var tableContext = context.Items[typeof(MyTableContext)] as MyTableContext;
    tableContext.TableHeaderBuilder.Append(sb.ToString());

    //suppress output (for any tag with a parent tag)
    output.SuppressOutput();

Back in the parent tag's Process method, you receive the child tag's output like this:

    //you can use a StringBuilder to build output 
    //    or just write to output.Content.AppendHtml() for top-level tags
    var sb = new StringBuilder();
    //...      

    //retrieve the child output and append it to sb
    await output.GetChildContentAsync();
    sb.Append(tableHeaderContext.RowBuilder.ToString());
    //...

    //write to TagHelperOutput for top-level tags      
    output.Content.AppendHtml(sb.ToString());