Aspnetcore: Rendering raw HTML content

Created on 26 Feb 2018  Β·  38Comments  Β·  Source: dotnet/aspnetcore

I'm looking for a way to render raw HTML strings as part of a Blazor component. I tried the following, but like regular Razor, this escapes the HTML tags (so the output is literally the text <p>Some text</p> in a cyan div):

<div style="background-color: cyan">@MyHtml</div>

@functions{
    public string MyHtml { get; set; } = "<p>Some text</p>";
}

Is there already something like @Html.Raw(...) in Blazor?

My use case: I have some markdown that is transformed to HTML by sending it to the GitHub API using an HttpClient. I'd like to display the generated HTML in my Blazor component.

area-blazor

Most helpful comment

Thanks for requesting this. I agree there should be an easy way to do it. Currently it's not implemented.

All 38 comments

.NET has HtmlString Class for that, not sure if it's what you're looking for though.
HTML String Class
So it should look like this.

<div>@MyHtml</div>
@functions {
    public HTMLString MyHtml { get; set; }
}

// assignment
MyHtml = new HtmlString("<p>Some Text</p>");

Yes, that's what I'm looking for, thanks. Unfortunately it doesn't seem to be supported (yet) in Blazor. The rendered output is exactly the same compared to the simple string approach. I guess Blazor just invokes MyHtml.ToString() at some point (which returns the wrapped string) and then escapes characters as usual.

Thanks for requesting this. I agree there should be an easy way to do it. Currently it's not implemented.

Here's a quick'n'dirty, temporary solution using JS:

HtmlView.cshtml

@using System.Web

<span>
    <img src="" onerror="(function (e) { e.parentElement.innerHTML = '@HttpUtility.JavaScriptStringEncode(Content)'; })(this)"/>
</span>

@functions{
    public string Content { get; set; }
}

Usage

<c:HtmlView Content=@MyHtml />

@functions{
    public string MyHtml { get; set; } = "<p>Some text</p>";
}

I need functionality like this as well.

I've built a Markdown component that needs the ability to render raw HTML. I can use the <img hack above, but it doesn't allow the value to be updated via data binding.

You can add a method that returns a RenderFragment, like this

public RenderFragment GetSomeRawHtml() {
  return builder => {
    builder.AddContent(0, "Whatever <b>you</b> want here");
  };
}

And then use it in your component

@GetSomeRawHtml

This works to some extent. You can't manipulate the Html once it's rendered.

This might be an edge case. But consider BlazeDown

What I was looking for there was Html I can treat as a value and update via data binding.

Consider:

    <div id="markdown-component">
        @RenderHtml()
    </div>

@functions {
    public string Content { get; set; } = "# Hello";
    private <T?> RenderHtml() => { //converts # Hello to <h1>Hello</h1> };
}

In this scenario, when Content changes, I'd like to re-render the HTML being generated from RenderHtml.

When render tree builder is used, it essentially becomes a component (I think?). Which doesn't allow me to re-render it.

Again, this could be an edge case, but I built BlazeDown to test various aspects of Blazor to see what was possible.

@mrpmorris If you can find a way to update the existing/previously rendered output without JavaScript I'd love to see it. Thanks πŸ‘

Update:
For my use case, the following may solve this.
https://github.com/aspnet/Blazor/pull/685
https://github.com/aspnet/Blazor/issues/495

@mrpmorris @EdCharbeneau Did either of you try this technique (of writing HTML inside a string and passing it to AddContent) and find that it really works?

It definitely shouldn't work to do that, as it would be a security issue. Please let me know if you find that you actually can get live HTML to show up that way!

@SteveSandersonMS I just tried it and it didn't work for me. Sorry @mrpmorris, I have to retract my "πŸ‘" πŸ˜ƒ

@SteveSandersonMS @SvenEV Just tried it and no it renders the literal string including the element tags as plain text Whatever <b>you</b> want here.

This works

    public RenderFragment GetSomeRawHtml()
    {
        return builder =>
        {
            builder.OpenElement(0, "h1");
            builder.AddContent(0, Title);
            builder.CloseElement();
        };
    } // renders an H1 tag with text inside.

Good πŸ‘

For the short term, I agree that the JS interop approach would be a good solution for BlazeDown until we implement a simpler way of expressing raw HTML.

@SteveSandersonMS I think that's a valid plan considering I'm probably doing something outside the norm. BlazeDown isn't a serious project, but one to challenge myself and Blazor.

With that said I came up with a fun way around the <img onerror hack. I grabbed the HTML Agility pack from NuGet and traversed the HTML converting it to a RenderTreeBuilder. :)

FWIW HtmlAgilityPack works off-the-shelf. Impressive :)

        using HtmlAgilityPack;

        protected override void BuildRenderTree(RenderTreeBuilder builder)
        {

            if (HtmlContent == null) return;
            var htmlDoc = new HtmlDocument();
            htmlDoc.LoadHtml(HtmlContent);

            var htmlBody = htmlDoc.DocumentNode;
            Decend(htmlBody, builder);
        }

        private void Decend(HtmlNode ds, RenderTreeBuilder b)
        {
            foreach (var nNode in ds.ChildNodes)
            {
                if (nNode.HasChildNodes)
                {
                    if (nNode.NodeType == HtmlNodeType.Element)
                        b.OpenElement(0, nNode.Name);
                    if (nNode.HasAttributes) Attributes(nNode, b);
                    Decend(nNode, b);
                    b.CloseElement();
                }
                else
                {
                    if (nNode.NodeType == HtmlNodeType.Text)
                    {
                        b.AddContent(0, nNode.InnerText);
                    }
                }
            }
        }

        private void Attributes(HtmlNode n, RenderTreeBuilder b)
        {
            foreach (var a in n.Attributes)
            {
                b.AddAttribute(0, a.Name, a.Value);
            }
        }

Update: I finally have a version working without JavaScript interops, data binding seems to work with this solution as well. I can load dynamic Html parsed from Markdown and update it dynamically.

https://github.com/EdCharbeneau/BlazeDown/blob/master/MarkdownComponent/Markdown.cshtml

I'm not that knowledgeable of what the RenderTree is doing when I manipulate it like this. Buyer bewared, there could be memory implications using this approach.

As of 0.3.0, the img onerror hack, which was the simplest way to do this if you don't need data binding, is now broken.

Error BL9986 Component attributes do not support complex content (mixed C# and markup). Attribute: 'onerror', text '(function (e) { e.parentElement.innerHTML = 'HttpUtility.JavaScriptStringEncode(Content)'; })(this)'

As this is a breaking change that destroys needed functionality for which there is no good workaround, (and no, dragging in a 3rd party HTML parser inside the browser, to build a shadow DOM so it can be converted to HTML for the browser's own HTML parser to convert to its own DOM, is not in any way a good workaround,) I would consider this a high-priority bug. Could you guys please immediately revert whatever is doing this (ComplexAttributeContentPass, it looks like?) and issue an update with it gone, until such time as HTML literal rendering is properly supported?

@SteveSandersonMS

Did either of you try this technique (of writing HTML inside a string and passing it to AddContent) and find that it really works?

It definitely shouldn't work to do that, as it would be a security issue. Please let me know if you find that you actually can get live HTML to show up that way!

I disagree. As far as I can tell, any security problems that would show up by having a simple "dump raw HTML into the output" method, you would see the exact same security problems by passing the exact same raw HTML through @EdCharbeneau 's HTML Agility Pack workaround. The only difference is how much of a hassle it is to code it.

The security problem isn't in having an easy way to render raw HTML, it's in blindly rendering input from an untrusted source, and that's not an API problem; it's an education problem. If someone doesn't know that's a bad thing, they're still going to find a way to render unsafe raw HTML even without a convenient API for it. (Such as the aforementioned HTML Agility Pack workaround.) All that not having a convenient API does is makes it that much harder for those of us who actually do know what we're doing to get stuff done.

@masonwheeler By default ASP.NET frameworks always encode HTML content unless you use an explicit API (like @Html.Raw) to signal that you want raw content. This is to protect users from accidentally rendering untrusted content and we consider it an important security guarantee. If you consider what it would be like to write an MVC app without this guarantee and I think you will agree that it puts a heavy burden on the user. We're trying to help web developers fall into the pit of success when trying to write secure code.

@danroth27 Sure, I get that, and I'm totally fine with it. But until such time as @Html.Raw is ready on the Blazor side, we need a working alternative, and the img onerror hack is the best we have at the moment. It also is something that you can only do explicitly and not accidentally, so it offers the same security guarantee in this area. All I'm asking is that you please un-break it until you have a proper alternative implemented.

Here's a new workaround:

HtmlView.cshtml

<span ref="Span"></span>

@functions{
    [Parameter] string Content { get; set; }
    private ElementRef Span;

    protected override void OnAfterRender()
    {
        Microsoft.AspNetCore.Blazor.Browser.Interop.RegisteredFunction.Invoke<bool>("RawHtml", Span, Content);
    }
}

index.html

    <script>
        Blazor.registerFunction('RawHtml', function (element, value) {
            element.innerHTML = value;
            for (var i = element.childNodes.length - 1; i >= 0; i--) {
                var childNode = element.childNodes[i];
                element.parentNode.insertBefore(childNode, element);
            }
            element.parentNode.removeChild(element);
            return true;
        });
    </script>

@Suchiman Thanks, that seems to work. :)

@masonwheeler

I just want to clarify that I wouldn't recommend my hack using HtmlAgility pack to anyone. I was simply showing I could get it done at all costs. I just wanted to make that clear and I'm pretty sure we're on the same page already πŸ˜‰

I made a modification of some of the ideas I saw here. This version allows you to drop <raw></raw> tags into your HTML.

site.js

...
Blazor.registerFunction('RawHtml', () => {
    $("raw").each((i, e) => {
        e.innerHTML = $('<textarea />').html(e.innerHTML).text();
        $(e).contents().unwrap();
    });
});

component.cshtml

...
<raw>@html</raw>
...
@functions {
    ...
    [Parameter]
    private string html { get; set; }

    protected override void OnAfterRender()
    {
        RegisteredFunction.Invoke<bool>("RawHtml");
    }
}

I'd like to find a way to call the RawHtml function after every section is rendered, and preferably limit it's scope to just check that section for the tags. Also, it depends on jQuery right now, but I'm sure that is an easy fix.

With all the workarounds, does anyone want to create a PR with an ideal solution?

@MisinformedDNA Better question: Why has the Blazor team not simply taken @Html.Raw from the ASP.Net codebase and dropped it as-is into Blazor? (This is an actual question. I'm sure there is a good reason why it's not that simple; I just don't know what it is.)

@MisinformedDNA i did look into it but this part of blazor is non trivial.
@masonwheeler i did start with that foolish thought but it works entirely different here.

Perhaps @SteveSandersonMS could provide feedback on my idea i had:
Add a RawHtml Type to RenderTreeFrameType.cs and introduce a new property on RenderTreeFrame, e.g. making a copy of RenderTreeFrame.ElementName and name it RawHtml and then append that RawHtml in to the DOM in BrowserRenderer.ts insertFrame

@Suchiman Yes, we have a plan for this. Not looking for any PR on it though as it's part of a different block of work that is planned. Thanks!

I wanted to expand on @Suchiman's solution. Instead of having a .cshtml file for my component, I created the component as a class. It uses the same RawHtml js function as above, with the exception that I changed element.parentNode.insertBefore(childNode, element); to element.parentNode.insertAfter(childNode, element);

public class RawHtml : BlazorComponent
{
    [Parameter]
    string Value { get; set; }
    private ElementRef element;

    protected override void BuildRenderTree(RenderTreeBuilder builder)
    {
        if (Value == null) return;

        builder.OpenElement(0, "rawHtml");
        builder.AddElementReferenceCapture(0, (elementReference) =>
        {
            this.element = elementReference;
        });
        builder.CloseElement();
    }

    protected override void OnAfterRender()
    {
        if (Value == null) return;
        Microsoft.AspNetCore.Blazor.Browser.Interop.RegisteredFunction.Invoke<bool>("RawHtml", element, Value);
    }
}

Usage

<RawHtml Value="<p>Hello World</p>"></RawHtml>

Does it work with several RawHtml’s in one page?

30 maj 2018 kl. 17:35 skrev MaxxDelusional notifications@github.com:

I wanted to expand on @Suchiman's solution. Instead of having a .cshtml file for my component, I created the component as a class. It uses the same RawHtml js function as above.

public class RawHtml : BlazorComponent
{
[Parameter]
string Value { get; set; }
private ElementRef element;

protected override void BuildRenderTree(RenderTreeBuilder builder)
{
    if (Value == null) return;

    builder.OpenElement(0, "rawHtml");
    builder.AddElementReferenceCapture(0, (elementReference) =>
    {
        this.element = elementReference;
    });
    builder.CloseElement();
}

protected override void OnAfterRender()
{
    if (Value == null) return;
    Microsoft.AspNetCore.Blazor.Browser.Interop.RegisteredFunction.Invoke<bool>("RawHtml", element, Value);
}

}
Usage

β€”
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub, or mute the thread.

I believe it should. I'm going to give it a try be the end of the weekend when I get a chance. I like this solution because it shouldn't call it multiple times on the same element, and you shouldn't have to call it explicitly.

@GoranHalvarsson good question. Without modification, it appears that my solution does NOT work with multiple <RawHtml> tags on the same page. I'll see if I can make changes to allow this.

Yes, I liked your approach. Its clean

31 maj 2018 kl. 21:05 skrev MaxxDelusional notifications@github.com:

@GoranHalvarsson good question. Without modification, it appears that my solution does NOT work with multiple tags on the same page. I'll see if I can make changes to allow this.

β€”
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub, or mute the thread.

Okay, so my class did [sort of] work with multiple <RawHtml> tags after all. The problem was actually with the change I made to the JavaScript function. By changing insertBefore to insertAfter, the interop call was failing. Even though the output looked right in Chrome and Edge, an Exception was being thrown back to the component because insertAfter isn't actually a valid JavaScript function.

In order to make my class work with multiple tags, you need to change the JavaScript from

element.parentNode.insertBefore(childNode, element);

to

element.parentNode.insertBefore(childNode, element.nextSibling);

I do wonder why folks keep trying to improve my solution, is there anything bad with it?
@MaxxDelusional yes, i was looking for insertAfter originally but i only found complex workarounds for the lack of insertAfter so i simply used insertBefore with a reverse for loop.

@Suchiman Your method worked for me! :)

@Suchiman - I prefer mine :)

Implemented in aspnet/Blazor#1146

From the discussion at least, aspnet/Blazor#1146 appears to be about an efficiency optimization in the renderer. Where's the part where you're able to take an arbitrary string and have it rendered as raw HTML?

@masonwheeler You can render raw html like this: https://github.com/aspnet/Blazor/pull/1146/files#diff-0a1a9a2202c82156228acbfecab64aa2R25

Normally your HTML helper would return IHtmlContent, so just do this instead:

public RenderFragment Render() => builder => builder.AddMarkupContent(0, _divIconContainer.IHtmlContentToString());

public static string IHtmlContentToString(this IHtmlContent htmlBuilder)
{
    if (htmlBuilder == null)
        throw new ArgumentNullException(nameof(htmlBuilder));

    var sw = new StringWriter();
    htmlBuilder.WriteTo(sw, HtmlEncoder.Default);
    var htmlStr = sw.ToString();
    sw.Dispose();
    return htmlStr;
}

and call it like this in your Component.razor:

@Html.Icon(LightIconType.TachometerAltFast).Class(".my-menu-tile-icon").Render()

also if you are doing this in an external library, the RenderFragment is in Microsoft.AspNetCore.Components in C:\Program Files\dotnet\packs\Microsoft.AspNetCore.App.Ref\3.0.0\ref\netcoreapp3.0\Microsoft.AspNetCore.Components.dll

Injecting html helpers into razor components is beyond the scope of this answer but if one got this far then one probably did it anyway.

Also dear Microsoft, Blazor is brilliant but do not discontinue support for @Html helpers completely, I find them far more useful then Tags (I prefer to work with C# instead of markup language and I know that a lot of people share this opinion).

Was this page helpful?
0 / 5 - 0 ratings