MVC6 TagHelpers with disposable

2019-02-18 09:58发布

问题:

In the older MVC HTML Helpers, one could use IDisposable to wrap content - for example the BeginForm helper would automatically wrap *stuff* with a closing form tag

<% using (Html.BeginForm()) {%>
   *stuff*
<% } %> 

Is this wrapping of content supported with MVC6 TagHelpers? For example I would like this

<widget-box title="My Title">Yay for content!</widget-box>

to be expanded into a bootstrap widget-box with wrapping divs:

<div class="widget-box">
    <div class="widget-header">
        <h4 class="widget-title">My Title</h4>
    </div>
    <div class="widget-body">
        <div class="widget-main">
            Yay for content!
        </div>
    </div>
</div>

Is this possible with TagHelpers?

Solution: I have baked @DanielJG's answer into a working demo on github which consumes WidgetBoxTagHelper.cs (will stay current with Beta/RC/RTM as am using the lib in my production app)

回答1:

Tag helpers have to implement the interface ITagHelper (as pointed by @NTaylorMullen, the TagHelper class is just a convenience class you can use when implementing it) which forces you to use the methods Process and ProcessAsync, so you cannot rely on adding contents in a Dispose method.

However you have full control over the output content so you can replace/modify it as you need. For example, a quick approximation to your widget tag helper (Using the 1.0 version of the framework):

[HtmlTargetElement("widget-box")]
public class WidgetTagHelper : TagHelper
{
    public string Title { get; set; }

    public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
    {
        var outerTag = new TagBuilder("div");
        outerTag.Attributes.Add("class", output.TagName);
        output.MergeAttributes(outerTag);
        output.TagName = outerTag.TagName;

        //Create the header
        var header = new TagBuilder("div");
        header.Attributes.Add("class", "widget-header");
        header.InnerHtml.Append(this.Title);
        output.PreContent.SetHtmlContent(header);

        //Create the body and replace original tag helper content
        var body = new TagBuilder("div");
        body.Attributes.Add("class", "widget-body");
        var originalContents = await output.GetChildContentAsync();
        body.InnerHtml.Append(originalContents.GetContent());
        output.Content.SetHtmlContent(body);
    }
}

In your razor you will have:

<widget-box title="My Title">Yay for content!</widget-box>

Which will be rendered as:

<div class="widget-box">
    <div class="widget-header">My Title</div>
    <div class="widget-body">Yay for content!</div>
</div>

Don´t forget to register the tag helpers in your assembly by adding a @addTagHelper directive to the _ViewImports.cshtml file. For example this will register all helpers in my application:

@addTagHelper *, WebApplication2

OLD beta7 code

  • In beta7 you had to use the [TargetElement] attribute.
  • The TagBuilder class had a SetInnerText method you could use to set its context as text.

The code looked like:

[TargetElement("widget-box")]
public class WidgetTagHelper : TagHelper
{
    private IHtmlEncoder encoder;
    public WidgetTagHelper(IHtmlEncoder encoder)
    {
        this.encoder = encoder;
    }

    public string Title { get; set; }

    public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
    {
        var outerTag = new TagBuilder("div");
        outerTag.Attributes.Add("class", output.TagName);
        output.MergeAttributes(outerTag);
        output.TagName = outerTag.TagName;

        //Create the header
        var header = new TagBuilder("div");
        header.Attributes.Add("class", "widget-header");
        header.SetInnerText(this.Title);
        output.PreContent.SetContent(header);

        //Create the body and replace original tag helper content
        var body = new TagBuilder("div");
        body.Attributes.Add("class", "widget-body");
        var originalContents = await context.GetChildContentAsync();           
        using (var writer = new StringWriter())
        {
            body.TagRenderMode = TagRenderMode.StartTag;
            body.WriteTo(writer, encoder);
            originalContents.WriteTo(writer, encoder);
            body.TagRenderMode = TagRenderMode.EndTag;
            body.WriteTo(writer, encoder);
            output.Content.SetContent(writer.ToString());
        }                            
    }
}

OLD beta5 code

  • There was an InnerHtml property in the tag helpers.
  • There was a ToHtmlString method in the tag helpers used to render them as html.

The code looked like:

[TargetElement("widget-box")]
public class WidgetTagHelper: TagHelper
{
    public string Title { get; set; }

    public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
    {
        var outerTag = new TagBuilder("div");
        outerTag.Attributes.Add("class", output.TagName);
        output.MergeAttributes(outerTag);
        output.TagName = outerTag.TagName;

        //Create the header
        var header = new TagBuilder("div");
        header.Attributes.Add("class", "widget-header");
        header.InnerHtml = this.Title;
        output.PreContent.SetContent(header.ToHtmlString(TagRenderMode.Normal).ToString());

        //Create the body and replace original tag helper content
        var body = new TagBuilder("div");
        body.Attributes.Add("class", "widget-body");
        var originalContents = await context.GetChildContentAsync();
        body.InnerHtml = originalContents.GetContent();
        output.Content.SetContent(body.ToHtmlString(TagRenderMode.Normal).ToString());
    }
}