0

I am quite novice in ASP.NET Core. I am trying to migrate our old project to ASP.NET core. We use a lot of script blocks inside of partials that are centrally rendered in predefined place of layout. We use HtmlHelper with disposable pattern as described here. The problem is that due of the new architecture in ASP.NET core it is not possible to use

webPageBase.OutputStack.Pop()

to catch content. I found similar solution with TagHelpers but I still will prefer to stay with HtmlHelper extension to have collecting scripts logic and rendering in single place. Otherwise I will need to write 2 tag helpers, coordinate them and replace all 50-100 occurrences of HtmlHelper extension to tag helper.

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
Yauhen.F
  • 2,382
  • 3
  • 19
  • 25
  • Is there a reason you aren't using [razor sections](https://learn.microsoft.com/en-us/aspnet/core/mvc/views/layout#specifying-a-layout) for your scripts? – SpruceMoose Nov 17 '17 at 00:36
  • yes. sections doesn't work with partials with different level of hierarchy. – Yauhen.F Nov 17 '17 at 07:37

1 Answers1

1

HTML helpers are still a thing in ASP.NET Core. Just because tag helpers are the new and generally more flexible solution to render custom HTML, that does not mean that HTML helpers are gone or that they have no use left. The built-in tag helpers are actually based on the HTML helpers and will use the same internal backend to generate the output. So it’s just a different interface for the same thing.

That being said, due to how ASP.NET Core renders views, capturing the content inside a using block is a bit more difficult compared to how it works in tag helpers (where it’s a very general feature).

I’ve been sitting on this for a while now and came up with the following. This works by temporarily replacing the view writer to a StringWriter for as long as the block is open. Note that this might be a super terrible idea. But it works…

public static class ScriptHtmlHelper
{
    private const string ScriptsKey = "__ScriptHtmlHelper_Scripts";

    public static ScriptBlock BeginScripts(this IHtmlHelper helper)
    {
        return new ScriptBlock(helper.ViewContext);
    }

    public static IHtmlContent PageScripts(this IHtmlHelper helper)
    {
        if (helper.ViewContext.HttpContext.Items.TryGetValue(ScriptsKey, out var scriptsData) && scriptsData is List<object> scripts)
            return new HtmlContentBuilder(scripts);
        return HtmlString.Empty;
    }

    public class ScriptBlock : IDisposable
    {
        private ViewContext _viewContext;
        private TextWriter _originalWriter;
        private StringWriter _scriptWriter;
        private bool _disposed;

        public ScriptBlock(ViewContext viewContext)
        {
            _viewContext = viewContext;
            _originalWriter = viewContext.Writer;

            // replace writer
            viewContext.Writer = _scriptWriter = new StringWriter();
        }

        public void Dispose()
        {
            if (_disposed)
                return;

            try
            {
                List<object> scripts = null;
                if (_viewContext.HttpContext.Items.TryGetValue(ScriptsKey, out var scriptsData))
                    scripts = scriptsData as List<object>;
                if (scripts == null)
                    _viewContext.HttpContext.Items[ScriptsKey] = scripts = new List<object>();

                scripts.Add(new HtmlString(_scriptWriter.ToString()));
            }
            finally
            {
                // restore the original writer
                _viewContext.Writer = _originalWriter;
                _disposed = true;
            }
        }
    }
}

Usage will be like this:

@using (Html.BeginScripts()) {
    <script>console.log('foo');</script>
    <script>console.log('bar');</script>
}

@using (Html.BeginScripts()) {
    <script>console.log('baz');</script>
}

And then to render everything:

@Html.PageScripts()
poke
  • 369,085
  • 72
  • 557
  • 602