2

I am migrating projects from .NET Framework to .NET core. This question is about ASP MVC projects. I have a lot of HtmlHelper extension methods and many of these use the TagBuilder class to generate HTML elements while generating content into a StringBuilder.

public static HtmlString DisplayTiles(this IHtmlHelper html, TileDashboardModel groups, bool displayNotEnabled = false, bool displayUnauthorized = false)
{
    if (groups == null) throw new ArgumentNullException(nameof(groups));

    var content = new StringBuilder();
    var div = new TagBuilder("div");
    content.AppendLine(div.ToString(TagRenderMode.StartTag));                     // does not build
    foreach (var group in groups.Groups)
    {
        DisplayTiles(html, content, group, displayNotEnabled, displayUnauthorized);
    }

    content.AppendLine(div.ToString(TagRenderMode.EndTag));                       // does not build

    ////return new MvcHtmlString(sb.ToString());                                  // does not build
    return new HtmlString(content.ToString());
}

In .NET Core the TagBuilder.ToString() method does not accept any argument. And rendering method seem to force you to use a StringWriter.

What ways would you change the code to make it work?

Consider: ease of change, memory consumption, extension method interference, code readability.


Related but different: Migrating TagBuilder core methods from ASP.NET MVC 5 to ASP.NET Core, How to display content of StringBuilder as HTML?

3 Answers 3

1

The ways I found are these, from the "best" to the worst. full code here

Initial code

This only works with .NET Framework.

    /// <summary>
    /// The old way.
    /// </summary>
    [Fact]
    public void Test0()
    {
        var contents = new StringBuilder();
        var tag = new TagBuilder("div");
        tag.SetInnerText("hello");                              // method does not exist
    
        contents.Append(tag.ToString(TagRenderMode.StartTag));  // missing method overload
        Assert.Equal("<div>", contents.ToString());

        contents.Append(tag.InnerHtml);
        Assert.Equal("<div>hello", contents.ToString());

        contents.Append(tag.ToString(TagRenderMode.EndTag));    // missing method overload
        Assert.Equal("<div>hello</div>", contents.ToString());

        var result = MvcHtmlString.Create(contents.ToString()); // class does not exist
        Assert.Equal("<div>hello</div>", result.ToString());
    }

Minimal changes

This method focuses on minimal changes. You only need to change one type and declare a 3 extension methods.

    /// <summary>
    /// Minimal code changes: extension methods.
    /// </summary>
    [Fact]
    public void Test4()
    {
        var contents = new StringWriter();                     // change this type
        var tag = new TagBuilder("div");
        tag.SetInnerText("hello");                             // 1 extension method

        contents.Append(tag.ToString(TagRenderMode.StartTag)); // 2 extension methods
        Assert.Equal("<div>", contents.ToString());

        contents.Append(tag.InnerHtml);                        // 1 extension method
        Assert.Equal("<div>hello", contents.ToString());

        contents.Append(tag.ToString(TagRenderMode.EndTag));   // 2 extension methods
        Assert.Equal("<div>hello</div>", contents.ToString());
    }

    public static void SetInnerText(this TagBuilder tag, string value)
    {
        tag.InnerHtml.Append(value);
    }

    public static void Append(this StringWriter writer, IHtmlContent html)
    {
        html.WriteTo(writer, HtmlEncoder.Default);
    }

    public static IHtmlContent ToString(this TagBuilder tag, TagRenderMode mode)
    {
        if (mode == TagRenderMode.StartTag)
        {
            return tag.RenderStartTag();
        }
        else if (mode == TagRenderMode.EndTag)
        {
            return tag.RenderEndTag();
        }
        else
        {
            throw new ArgumentException();
        }
    }

Minimal call stack

Here a focus on not using extra methods; code changes a lot.


    /// <summary>
    /// Use StringWriter instead of StringBuilder, no ext. methods.
    /// </summary>
    [Fact]
    public void Test3()
    {
        var contents = new StringWriter();                           // changed
        var tag = new TagBuilder("div");
        tag.InnerHtml.Append("hello");

        tag.RenderStartTag().WriteTo(contents, HtmlEncoder.Default); // changed
        Assert.Equal("<div>", contents.ToString());

        tag.RenderBody()?.WriteTo(contents, HtmlEncoder.Default);    // changed
        Assert.Equal("<div>hello", contents.ToString());

        tag.RenderEndTag().WriteTo(contents, HtmlEncoder.Default);   // changed
        Assert.Equal("<div>hello</div>", contents.ToString());
    }

The memory hogger

There is a known extension method that will allocate too much memory. Don't do this one.

    /// <summary>
    /// Use Render methods and bad extension methods.
    /// </summary>
    /// <remarks>So many StringWriters</remarks>
    [Fact]
    public void Test2()
    {
        var contents = new StringBuilder();
        var tag = new TagBuilder("div");
        tag.InnerHtml.Append("hello");
        
        contents.AppendEx(tag.RenderStartTag());
        Assert.Equal("<div>", contents.ToString());

        contents.AppendEx(tag.RenderBody());
        Assert.Equal("<div>hello", contents.ToString());

        contents.AppendEx(tag.RenderEndTag());
        Assert.Equal("<div>hello</div>", contents.ToString());
    }

    public static void AppendEx(this StringBuilder builder, IHtmlContent? content)
    {
        if (content != null)
        {
            using (var writer = new StringWriter(builder))
            {
                content.WriteTo(writer, HtmlEncoder.Default);
            }
        }
    }

Here is a variant using TagRenderMode.

    /// <summary>
    /// Set RenderMode and use extension methods.
    /// </summary>
    /// <remarks>So many StringWriters</remarks>
    [Fact]
    public void Test1()
    {
        var contents = new StringBuilder();
        var tag = new TagBuilder("div");
        tag.InnerHtml.Append("hello");
        
        tag.TagRenderMode = TagRenderMode.StartTag;
        contents.AppendEx(tag);
        Assert.Equal("<div>", contents.ToString());

        contents.AppendEx(tag.InnerHtml);
        Assert.Equal("<div>hello", contents.ToString());

        tag.TagRenderMode = TagRenderMode.EndTag;
        contents.AppendEx(tag);
        Assert.Equal("<div>hello</div>", contents.ToString());
    }

    public static void AppendEx(this StringBuilder contents, TagBuilder tag)
    {
        // why do we need to do that? my StringBuilder is no good enough?
        using (var writer = new StringWriter(contents))
        {
            tag.WriteTo(writer, HtmlEncoder.Default);
        }
    }
Sign up to request clarification or add additional context in comments.

2 Comments

hi, do you know how to migrate when TagRendermode.Normal? There is no RenderNormal method in the TagBuilder class TagBuilder divMoreInfoText = new TagBuilder("span"); divMoreInfoText.Attributes.Add("class", "helpText alert alert-info mrgn-tp-sm mrgn-bttm-sm"); divMoreInfoText.Attributes.Add("style", "display: block"); TagBuilder pHelpText = new TagBuilder("span"); pHelpText.InnerHtml = text; divMoreInfoText.InnerHtml = pHelpText.ToString(TagRenderMode.Normal);
I guess to be more clear, you have the following extension method. How would you handle TagRenderMode.Normal public static IHtmlContent ToString(this TagBuilder tag, TagRenderMode mode) { if (mode == TagRenderMode.StartTag) { return tag.RenderStartTag(); } else if (mode == TagRenderMode.EndTag) { return tag.RenderEndTag(); } else { throw new ArgumentException(); } }
0

The new TagBuilder (in Microsoft.AspNetCore.Mvc.Rendering) behaves very differently from the old TagBuilder (in System.Web.Mvc). This makes code changes very hard; even harder if you do not have unit tests; you may get "Microsoft.AspNetCore.Mvc.Rendering.TagBuilder" printed everywhere on your pages.

The workaround

To avoid having a hard time migrating things for real, the fastest action is:

  • copy the old TagBuilder.cs in your project
  • rename it TagBuilderOld (to avoid type name conflicts)
  • copy the old TagRenderMode
  • rename it TagRenderModeOld (to avoid type name conflicts)
  • import it in your helpers:
    using TagBuilder = System.Web.Mvc.TagBuilderOld;
    using TagRenderMode = System.Web.Mvc.TagRenderModeOld;
  • you can also import the new one if you wish to become mad:
    using TagBuilderNew = Microsoft.AspNetCore.Mvc.Rendering.TagBuilder;
    using TagRenderModeNew = Microsoft.AspNetCore.Mvc.Rendering.TagRenderMode;

This way it works well. No "Microsoft.AspNetCore.Mvc.Rendering.TagBuilder" everywhere on your pages.

Warning: this is a workaround! You will not get feature and security updates if you do that. See the other answers for a more serious way to solve the issue.

Comments

0

Yes, you should use StringWriter. Example:

var container = new TagBuilder("div");
container.MergeAttributes(htmlAttrs);
using (var writer = new StringWriter())
{
  container.TagRenderMode = TagRenderMode.StartTag;
  container.WriteTo(writer, HtmlEncoder.Default);
  result = writer.ToString();
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.