aspnet / Mvc

[Archived] ASP.NET Core MVC is a model view controller framework for building dynamic web sites with clean separation of concerns, including the merged MVC, Web API, and Web Pages w/ Razor. Project moved to https://github.com/aspnet/AspNetCore
Apache License 2.0
5.62k stars 2.14k forks source link

Request: Serializing IHtmlContent to a JavaScript string #5061

Closed tuespetre closed 7 years ago

tuespetre commented 8 years ago

I've come to appreciate the approach to writing dynamic server-generated applications here:

https://signalvnoise.com/posts/3697-server-generated-javascript-responses

Thanks to the awesome new View Components feature, I've been having a lot of success with this approach since beta7 or so. I can write view components with a top-level container element with an id attribute and easily update multiple components on a page in a single request, even when they don't have a common ancestor in the document short of replacing the entire thing.

Example: a shopping cart that in itself consist of several components, and a 'shopping cart toolbar component' that exists in the top navbar of the site. By processing user input to the various forms in the shopping cart, AJAX requests are issued to the server, which then responds with something like this:

<script>
    $('#some-cart-thing').replaceWith(@Component.Invoke("SomeCartThing", argsForSomeCartThing).ForJavaScript());
    // We need to make sure the cart total in the toolbar stays up-to-date
    $('#toolbar-cart').replaceWith(@Component.Invoke("ToolbarCart", argsForToolbarCart).ForJavaScript());
</script>

This prevents us from having to work with multiple templating solutions and JavaScript MVC/MVVC stuff like Knockout or Angular.

I don't know exactly how efficient or reliable it is, but I've been using this drop-in file that I wrote:

using Microsoft.AspNetCore.Html;
using System.IO;
using System.Text;
using System.Text.Encodings.Web;
using System.Threading.Tasks;

public static class HtmlContentExtensions
{
    public static IHtmlContent ForJavaScript(this IHtmlContent content)
    {
        return new JsonEncodedHtmlString(content);
    }

    private class JsonEncodedHtmlString : IHtmlContent
    {
        private IHtmlContent inner;

        public JsonEncodedHtmlString(IHtmlContent inner)
        {
            this.inner = inner;
        }

        public void WriteTo(TextWriter writer, HtmlEncoder encoder)
        {
            using (var json = new CustomJsonWriter(writer))
            {
                writer.Write('\'');
                inner.WriteTo(json, encoder);
                writer.Write('\'');
            }
        }
    }

    private class CustomJsonWriter : TextWriter
    {
        private readonly TextWriter inner;

        public CustomJsonWriter(TextWriter inner)
        {
            this.inner = inner;
        }

        public override Encoding Encoding => Encoding.UTF8;

        public override void Write(string value)
        {
            inner.Write(JavaScriptEncoder.Default.Encode(value));
        }

        public override Task WriteAsync(string value)
        {
            return inner.WriteAsync(JavaScriptEncoder.Default.Encode(value));
        }
    }
}

I just wanted to share my background and put this out there for discussion. I know the current hotness is Aurelia or React or whatever other framework, but this approach allows for some pretty rapid MVC development while letting us do more than simply replacing one component with a single partial returned from the server and I thought it would be good to bring up.

Eilon commented 8 years ago

Looks pretty neat! From a performance perspective you might want to see if all the string creation in memory is a potential performance problem because it'll be buffering entire JSON strings in memory instead of streaming them out to whoever is listening.

I don't see this as something we'll include in ASP.NET at this time, so I'm marking this issue as a "discussion" to see if there's further interest in this from the broader community.

rynowak commented 8 years ago

Ideally we would have made the parameter on WriteTo(...) a TextEncoder instead of an HtmlEncoder... :sob: - but TextEncoder was added later and I never noticed.

You could probably achieve your goals in a simpler way by combining the two encoders. For instance with your sample, you really need to override most of the methods on TextWriter, which is a pain.

public class HtmlAndJavaScriptEncoder : HtmlEncoder
{
     private readonly HtmlEncoder _html;
     private readonly JavaScriptEncoder _javaScript;

    public HtmlAndJavaScriptEncoder(HtmlEncoder html, JavaScriptEncoder javaScript)
    {
        _html = html;
        _javaScript = javaScript;
    }

    public override int MaxOutputCharactersPerInputCharacter
    {
        get { return Math.Max(_html.MaxOutputCharactersPerInputCharacter, _javaScript.MaxOutputCharactersPerInputCharacter); }
    }

    ...
}
tuespetre commented 8 years ago

@rynowak I did try that at first but I saw unsafe and I tucked tail and ran. :dog2:

rynowak commented 8 years ago

If you ask nicely on https://github.com/dotnet/corefx someone will probably help you, it looks like all of the complexity is in TryEncodeUnicodeScalar

Eilon commented 7 years ago

We are closing this issue because no further action is planned for this issue. If you still have any issues or questions, please log a new issue with any additional details that you have.