Usar secciones en plantillas de editor / visualización

104

Quiero mantener todo mi código JavaScript en una sección; justo antes de la bodyetiqueta de cierre en mi página de diseño maestro y me pregunto qué es lo mejor para hacerlo, al estilo MVC.

Por ejemplo, si creo un DisplayTemplate\DateTime.cshtmlarchivo que usa el selector de fecha y hora de jQuery UI, incrustaría el JavaScript directamente en esa plantilla, pero luego se procesará en la mitad de la página.

En mis vistas normales, puedo usar @section JavaScript { //js here }y luego @RenderSection("JavaScript", false)en mi diseño maestro, pero esto no parece funcionar en las plantillas de visualización / editor, ¿alguna idea?

eth0
fuente
4
para cualquiera que venga a esto más tarde, hay un paquete nuget para manejar esto: nuget.org/packages/Forloop.HtmlHelpers
Russ Cam

Respuestas:

189

Puede proceder con una conjunción de dos ayudantes:

public static class HtmlExtensions
{
    public static MvcHtmlString Script(this HtmlHelper htmlHelper, Func<object, HelperResult> template)
    {
        htmlHelper.ViewContext.HttpContext.Items["_script_" + Guid.NewGuid()] = template;
        return MvcHtmlString.Empty;
    }

    public static IHtmlString RenderScripts(this HtmlHelper htmlHelper)
    {
        foreach (object key in htmlHelper.ViewContext.HttpContext.Items.Keys)
        {
            if (key.ToString().StartsWith("_script_"))
            {
                var template = htmlHelper.ViewContext.HttpContext.Items[key] as Func<object, HelperResult>;
                if (template != null)
                {
                    htmlHelper.ViewContext.Writer.Write(template(null));
                }
            }
        }
        return MvcHtmlString.Empty;
    }
}

y luego en tu _Layout.cshtml:

<body>
...
@Html.RenderScripts()
</body>

y en algún lugar de alguna plantilla:

@Html.Script(
    @<script src="@Url.Content("~/Scripts/jquery-1.4.4.min.js")" type="text/javascript"></script>
)
Darin Dimitrov
fuente
3
Como un diccionario no está ordenado, ¿cómo lo haría primero en entrar, primero en salir? El orden de salida es aleatorio (presumiblemente debido al Guid) ..
eth0
Tal vez podría configurar un campo entero estático y usar Interlocked.Increment () en lugar del GUID para ordenar, pero incluso entonces creo que un diccionario nunca garantiza el pedido. Pensándolo bien, tal vez un campo estático sea dudoso, ya que podría mantenerse en las pantallas de página. En su lugar, podría agregar un número entero al diccionario de elementos, pero tendría que ponerle un candado.
Mark Adamson
Comencé a usar esta solución recientemente, pero parece que no puedo colocar dos scripts en una sola línea @ Html.Script (), porque no estoy seguro de cómo funciona HelperResult. ¿No es posible hacer 2 bloques de script en 1 llamada Html.Script?
Langdon
2
@TimMeers, ¿qué quieres decir? Para mí todo esto siempre ha quedado obsoleto. No usaría esos ayudantes en absoluto. Nunca tuve la necesidad de incluir guiones en mis vistas parciales. Simplemente me quedaría con el Razor estándar sections. En MVC4 Bundling podría usarse de hecho y ayuda a reducir el tamaño de los scripts.
Darin Dimitrov
4
Este enfoque no funciona si desea colocar sus scripts o estilos en la headetiqueta en lugar de al final de la bodyetiqueta, porque @Html.RenderScripts()se ejecutará antes de su vista parcial y, por lo tanto, antes @Html.Script().
Maksim Vi.
41

Versión modificada de la respuesta de Darin para garantizar el pedido. También funciona con CSS:

public static IHtmlString Resource(this HtmlHelper HtmlHelper, Func<object, HelperResult> Template, string Type)
{
    if (HtmlHelper.ViewContext.HttpContext.Items[Type] != null) ((List<Func<object, HelperResult>>)HtmlHelper.ViewContext.HttpContext.Items[Type]).Add(Template);
    else HtmlHelper.ViewContext.HttpContext.Items[Type] = new List<Func<object, HelperResult>>() { Template };

    return new HtmlString(String.Empty);
}

public static IHtmlString RenderResources(this HtmlHelper HtmlHelper, string Type)
{
    if (HtmlHelper.ViewContext.HttpContext.Items[Type] != null)
    {
        List<Func<object, HelperResult>> Resources = (List<Func<object, HelperResult>>)HtmlHelper.ViewContext.HttpContext.Items[Type];

        foreach (var Resource in Resources)
        {
            if (Resource != null) HtmlHelper.ViewContext.Writer.Write(Resource(null));
        }
    }

    return new HtmlString(String.Empty);
}

Puede agregar recursos JS y CSS como este:

@Html.Resource(@<script src="@Url.Content("~/Scripts/jquery-1.4.4.min.js")" type="text/javascript"></script>, "js")
@Html.Resource(@<link rel="stylesheet" href="@Url.Content("~/CSS/style.css")" />, "css")

Y renderice recursos JS y CSS como este:

@Html.RenderResources("js")
@Html.RenderResources("css")

Puede hacer una verificación de cadena para ver si comienza con un script / enlace para que no tenga que definir explícitamente qué es cada recurso.

eth0
fuente
Gracias eth0. Me comprometí con este problema, pero tendré que comprobarlo.
one.beat.consumer
Lo sé hace casi 2 años, pero ¿hay alguna manera de verificar si el archivo css / js ya existe y no representarlo? Gracias
CodingSlayer
1
Okay. No estoy seguro de cuán eficiente es, pero actualmente estoy haciendo esto: var httpTemplates = HtmlHelper.ViewContext.HttpContext.Items [Type] as List <Func <object, HelperResult >>; var prevItem = from q en httpTemplates donde q (nulo) .ToString () == Plantilla (nulo) .ToString () seleccione q; if (! prevItem.Any ()) {// Agregar plantilla}
CodingSlayer
@imAbhi gracias, justo lo que necesitaba, parece un bucle for 1 de paquetes con item.ToString, así que creo que debería ser lo suficientemente rápido
Kunukn
35

Enfrenté el mismo problema, pero las soluciones propuestas aquí funcionan bien solo para agregar una referencia al recurso y no son muy adecuadas para el código JS en línea. Encontré un artículo muy útil y envolví todos mis JS en línea (y también las etiquetas de script) en

@using (Html.BeginScripts())
{
    <script src="@Url.Content("~/Scripts/jquery-ui-1.8.18.min.js")" type="text/javascript"></script>
    <script>
    // my inline scripts here
    <\script>
}

Y en la vista _Layout colocada @Html.PageScripts()justo antes de cerrar la etiqueta 'body'. Funciona como un encanto para mí.


Los propios ayudantes:

public static class HtmlHelpers
{
    private class ScriptBlock : IDisposable
    {
        private const string scriptsKey = "scripts";
        public static List<string> pageScripts
        {
            get
            {
                if (HttpContext.Current.Items[scriptsKey] == null)
                    HttpContext.Current.Items[scriptsKey] = new List<string>();
                return (List<string>)HttpContext.Current.Items[scriptsKey];
            }
        }

        WebViewPage webPageBase;

        public ScriptBlock(WebViewPage webPageBase)
        {
            this.webPageBase = webPageBase;
            this.webPageBase.OutputStack.Push(new StringWriter());
        }

        public void Dispose()
        {
            pageScripts.Add(((StringWriter)this.webPageBase.OutputStack.Pop()).ToString());
        }
    }

    public static IDisposable BeginScripts(this HtmlHelper helper)
    {
        return new ScriptBlock((WebViewPage)helper.ViewDataContainer);
    }

    public static MvcHtmlString PageScripts(this HtmlHelper helper)
    {
        return MvcHtmlString.Create(string.Join(Environment.NewLine, ScriptBlock.pageScripts.Select(s => s.ToString())));
    }
}
John.W. Harding
fuente
3
Esta es la mejor respuesta; también te permite inyectar prácticamente cualquier cosa y retrasarlo hasta el final
drzaus
1
¡Debe copiar y pegar el código del artículo en caso de que alguna vez se caiga! ¡Esta es una excelente respuesta!
Shaamaan
¿Cómo podemos hacer esto en asp.net core?
ramanmittal
13

Me gustó la solución publicada por @ john-w-harding, así que la combiné con la respuesta de @ darin-dimitrov para hacer la siguiente solución probablemente demasiado complicada que le permite retrasar la renderización de cualquier html (también scripts) dentro de un bloque de uso.

USO

En una vista parcial repetida, solo incluya el bloque una vez:

@using (Html.Delayed(isOnlyOne: "MYPARTIAL_scripts")) {
    <script>
        someInlineScript();
    </script>
}

En una vista parcial (¿repetida?), Incluya el bloque para cada vez que se use el parcial:

@using (Html.Delayed()) {
    <b>show me multiple times, @Model.Whatever</b>
}

En una vista parcial (¿repetida?), Incluya el bloque una vez y luego renderícelo específicamente por su nombre one-time:

@using (Html.Delayed("one-time", isOnlyOne: "one-time")) {
    <b>show me once by name</b>
    <span>@Model.First().Value</span>
}

Para renderizar:

@Html.RenderDelayed(); // the "default" unidentified blocks
@Html.RenderDelayed("one-time", false); // render the specified block by name, and allow us to render it again in a second call
@Html.RenderDelayed("one-time"); // render the specified block by name
@Html.RenderDelayed("one-time"); // since it was "popped" in the last call, won't render anything

CÓDIGO

public static class HtmlRenderExtensions {

    /// <summary>
    /// Delegate script/resource/etc injection until the end of the page
    /// <para>@via https://stackoverflow.com/a/14127332/1037948 and http://jadnb.wordpress.com/2011/02/16/rendering-scripts-from-partial-views-at-the-end-in-mvc/ </para>
    /// </summary>
    private class DelayedInjectionBlock : IDisposable {
        /// <summary>
        /// Unique internal storage key
        /// </summary>
        private const string CACHE_KEY = "DCCF8C78-2E36-4567-B0CF-FE052ACCE309"; // "DelayedInjectionBlocks";

        /// <summary>
        /// Internal storage identifier for remembering unique/isOnlyOne items
        /// </summary>
        private const string UNIQUE_IDENTIFIER_KEY = CACHE_KEY;

        /// <summary>
        /// What to use as internal storage identifier if no identifier provided (since we can't use null as key)
        /// </summary>
        private const string EMPTY_IDENTIFIER = "";

        /// <summary>
        /// Retrieve a context-aware list of cached output delegates from the given helper; uses the helper's context rather than singleton HttpContext.Current.Items
        /// </summary>
        /// <param name="helper">the helper from which we use the context</param>
        /// <param name="identifier">optional unique sub-identifier for a given injection block</param>
        /// <returns>list of delayed-execution callbacks to render internal content</returns>
        public static Queue<string> GetQueue(HtmlHelper helper, string identifier = null) {
            return _GetOrSet(helper, new Queue<string>(), identifier ?? EMPTY_IDENTIFIER);
        }

        /// <summary>
        /// Retrieve a context-aware list of cached output delegates from the given helper; uses the helper's context rather than singleton HttpContext.Current.Items
        /// </summary>
        /// <param name="helper">the helper from which we use the context</param>
        /// <param name="defaultValue">the default value to return if the cached item isn't found or isn't the expected type; can also be used to set with an arbitrary value</param>
        /// <param name="identifier">optional unique sub-identifier for a given injection block</param>
        /// <returns>list of delayed-execution callbacks to render internal content</returns>
        private static T _GetOrSet<T>(HtmlHelper helper, T defaultValue, string identifier = EMPTY_IDENTIFIER) where T : class {
            var storage = GetStorage(helper);

            // return the stored item, or set it if it does not exist
            return (T) (storage.ContainsKey(identifier) ? storage[identifier] : (storage[identifier] = defaultValue));
        }

        /// <summary>
        /// Get the storage, but if it doesn't exist or isn't the expected type, then create a new "bucket"
        /// </summary>
        /// <param name="helper"></param>
        /// <returns></returns>
        public static Dictionary<string, object> GetStorage(HtmlHelper helper) {
            var storage = helper.ViewContext.HttpContext.Items[CACHE_KEY] as Dictionary<string, object>;
            if (storage == null) helper.ViewContext.HttpContext.Items[CACHE_KEY] = (storage = new Dictionary<string, object>());
            return storage;
        }


        private readonly HtmlHelper helper;
        private readonly string identifier;
        private readonly string isOnlyOne;

        /// <summary>
        /// Create a new using block from the given helper (used for trapping appropriate context)
        /// </summary>
        /// <param name="helper">the helper from which we use the context</param>
        /// <param name="identifier">optional unique identifier to specify one or many injection blocks</param>
        /// <param name="isOnlyOne">extra identifier used to ensure that this item is only added once; if provided, content should only appear once in the page (i.e. only the first block called for this identifier is used)</param>
        public DelayedInjectionBlock(HtmlHelper helper, string identifier = null, string isOnlyOne = null) {
            this.helper = helper;

            // start a new writing context
            ((WebViewPage)this.helper.ViewDataContainer).OutputStack.Push(new StringWriter());

            this.identifier = identifier ?? EMPTY_IDENTIFIER;
            this.isOnlyOne = isOnlyOne;
        }

        /// <summary>
        /// Append the internal content to the context's cached list of output delegates
        /// </summary>
        public void Dispose() {
            // render the internal content of the injection block helper
            // make sure to pop from the stack rather than just render from the Writer
            // so it will remove it from regular rendering
            var content = ((WebViewPage)this.helper.ViewDataContainer).OutputStack;
            var renderedContent = content.Count == 0 ? string.Empty : content.Pop().ToString();

            // if we only want one, remove the existing
            var queue = GetQueue(this.helper, this.identifier);

            // get the index of the existing item from the alternate storage
            var existingIdentifiers = _GetOrSet(this.helper, new Dictionary<string, int>(), UNIQUE_IDENTIFIER_KEY);

            // only save the result if this isn't meant to be unique, or
            // if it's supposed to be unique and we haven't encountered this identifier before
            if( null == this.isOnlyOne || !existingIdentifiers.ContainsKey(this.isOnlyOne) ) {
                // remove the new writing context we created for this block
                // and save the output to the queue for later
                queue.Enqueue(renderedContent);

                // only remember this if supposed to
                if(null != this.isOnlyOne) existingIdentifiers[this.isOnlyOne] = queue.Count; // save the index, so we could remove it directly (if we want to use the last instance of the block rather than the first)
            }
        }
    }


    /// <summary>
    /// <para>Start a delayed-execution block of output -- this will be rendered/printed on the next call to <see cref="RenderDelayed"/>.</para>
    /// <para>
    /// <example>
    /// Print once in "default block" (usually rendered at end via <code>@Html.RenderDelayed()</code>).  Code:
    /// <code>
    /// @using (Html.Delayed()) {
    ///     <b>show at later</b>
    ///     <span>@Model.Name</span>
    ///     etc
    /// }
    /// </code>
    /// </example>
    /// </para>
    /// <para>
    /// <example>
    /// Print once (i.e. if within a looped partial), using identified block via <code>@Html.RenderDelayed("one-time")</code>.  Code:
    /// <code>
    /// @using (Html.Delayed("one-time", isOnlyOne: "one-time")) {
    ///     <b>show me once</b>
    ///     <span>@Model.First().Value</span>
    /// }
    /// </code>
    /// </example>
    /// </para>
    /// </summary>
    /// <param name="helper">the helper from which we use the context</param>
    /// <param name="injectionBlockId">optional unique identifier to specify one or many injection blocks</param>
    /// <param name="isOnlyOne">extra identifier used to ensure that this item is only added once; if provided, content should only appear once in the page (i.e. only the first block called for this identifier is used)</param>
    /// <returns>using block to wrap delayed output</returns>
    public static IDisposable Delayed(this HtmlHelper helper, string injectionBlockId = null, string isOnlyOne = null) {
        return new DelayedInjectionBlock(helper, injectionBlockId, isOnlyOne);
    }

    /// <summary>
    /// Render all queued output blocks injected via <see cref="Delayed"/>.
    /// <para>
    /// <example>
    /// Print all delayed blocks using default identifier (i.e. not provided)
    /// <code>
    /// @using (Html.Delayed()) {
    ///     <b>show me later</b>
    ///     <span>@Model.Name</span>
    ///     etc
    /// }
    /// </code>
    /// -- then later --
    /// <code>
    /// @using (Html.Delayed()) {
    ///     <b>more for later</b>
    ///     etc
    /// }
    /// </code>
    /// -- then later --
    /// <code>
    /// @Html.RenderDelayed() // will print both delayed blocks
    /// </code>
    /// </example>
    /// </para>
    /// <para>
    /// <example>
    /// Allow multiple repetitions of rendered blocks, using same <code>@Html.Delayed()...</code> as before.  Code:
    /// <code>
    /// @Html.RenderDelayed(removeAfterRendering: false); /* will print */
    /// @Html.RenderDelayed() /* will print again because not removed before */
    /// </code>
    /// </example>
    /// </para>

    /// </summary>
    /// <param name="helper">the helper from which we use the context</param>
    /// <param name="injectionBlockId">optional unique identifier to specify one or many injection blocks</param>
    /// <param name="removeAfterRendering">only render this once</param>
    /// <returns>rendered output content</returns>
    public static MvcHtmlString RenderDelayed(this HtmlHelper helper, string injectionBlockId = null, bool removeAfterRendering = true) {
        var stack = DelayedInjectionBlock.GetQueue(helper, injectionBlockId);

        if( removeAfterRendering ) {
            var sb = new StringBuilder(
#if DEBUG
                string.Format("<!-- delayed-block: {0} -->", injectionBlockId)
#endif
                );
            // .count faster than .any
            while (stack.Count > 0) {
                sb.AppendLine(stack.Dequeue());
            }
            return MvcHtmlString.Create(sb.ToString());
        } 

        return MvcHtmlString.Create(
#if DEBUG
                string.Format("<!-- delayed-block: {0} -->", injectionBlockId) + 
#endif
            string.Join(Environment.NewLine, stack));
    }


}
drzaus
fuente
Extraño. No recuerdo haber copiado la respuesta a este otro hilo , pero escribí un poco mejor allí ...
drzaus
12

Instale el paquete nuget Forloop.HtmlHelpers: agrega algunos ayudantes para administrar scripts en vistas parciales y plantillas de editor.

En algún lugar de su diseño, debe llamar

@Html.RenderScripts()

Aquí será donde se generarán los archivos de secuencia de comandos y los bloques de secuencia de comandos en la página, por lo que recomendaría colocarlo después de sus secuencias de comandos principales en el diseño y después de una sección de secuencias de comandos (si tiene una).

Si está utilizando el marco de optimización web con agrupación, puede utilizar la sobrecarga

@Html.RenderScripts(Scripts.Render)

de modo que este método se utiliza para escribir archivos de script.

Ahora, en cualquier momento que desee agregar archivos de script o bloques en una vista, vista parcial o plantilla, simplemente use

@using (Html.BeginScriptContext())
{
  Html.AddScriptFile("~/Scripts/jquery.validate.js");
  Html.AddScriptBlock(
    @<script type="text/javascript">
       $(function() { $('#someField').datepicker(); });
     </script>
  );
}

Los ayudantes aseguran que solo se procese una referencia de archivo de script si se agrega varias veces y también asegura que los archivos de script se procesen en el orden esperado, es decir

  1. Diseño
  2. Parciales y plantillas (en el orden en que aparecen en la vista, de arriba a abajo)
Russ Cam
fuente
5

Esta publicación realmente me ayudó, así que pensé en publicar mi implementación de la idea básica. Introduje una función auxiliar que puede devolver etiquetas de script para usar en la función @ Html.Resource.

También agregué una clase estática simple para poder usar variables escritas para identificar un recurso JS o CSS.

public static class ResourceType
{
    public const string Css = "css";
    public const string Js = "js";
}

public static class HtmlExtensions
{
    public static IHtmlString Resource(this HtmlHelper htmlHelper, Func<object, dynamic> template, string Type)
    {
        if (htmlHelper.ViewContext.HttpContext.Items[Type] != null) ((List<Func<object, dynamic>>)htmlHelper.ViewContext.HttpContext.Items[Type]).Add(template);
        else htmlHelper.ViewContext.HttpContext.Items[Type] = new List<Func<object, dynamic>>() { template };

        return new HtmlString(String.Empty);
    }

    public static IHtmlString RenderResources(this HtmlHelper htmlHelper, string Type)
    {
        if (htmlHelper.ViewContext.HttpContext.Items[Type] != null)
        {
            List<Func<object, dynamic>> resources = (List<Func<object, dynamic>>)htmlHelper.ViewContext.HttpContext.Items[Type];

            foreach (var resource in resources)
            {
                if (resource != null) htmlHelper.ViewContext.Writer.Write(resource(null));
            }
        }

        return new HtmlString(String.Empty);
    }

    public static Func<object, dynamic> ScriptTag(this HtmlHelper htmlHelper, string url)
    {
        var urlHelper = new UrlHelper(htmlHelper.ViewContext.RequestContext);
        var script = new TagBuilder("script");
        script.Attributes["type"] = "text/javascript";
        script.Attributes["src"] = urlHelper.Content("~/" + url);
        return x => new HtmlString(script.ToString(TagRenderMode.Normal));
    }
}

Y en uso

@Html.Resource(Html.ScriptTag("Areas/Admin/js/plugins/wysiwyg/jquery.wysiwyg.js"), ResourceType.Js)

Gracias a @Darin Dimitrov, quien proporcionó la respuesta a mi pregunta aquí .

Chris
fuente
2

La respuesta dada en Rellenar una sección de Razor a partir de un parcial utilizando RequireScriptHtmlHelper sigue el mismo patrón. También tiene la ventaja de que busca y suprime las referencias duplicadas a la misma URL de Javascript, y tiene unapriority parámetro que se puede usar para controlar los pedidos.

Extendí esta solución agregando métodos para:

// use this for scripts to be placed just before the </body> tag
public static string RequireFooterScript(this HtmlHelper html, string path, int priority = 1) { ... }
public static HtmlString EmitRequiredFooterScripts(this HtmlHelper html) { ... }

// use this for CSS links
public static string RequireCSS(this HtmlHelper html, string path, int priority = 1) { ... }
public static HtmlString EmitRequiredCSS(this HtmlHelper html) { ... }

Sin embargo, me gustan las soluciones de & eth0 de Darin, ya que utilizan la HelperResultplantilla, que permite bloques de script y CSS, no solo enlaces a archivos JavaScript y CSS.

Martin_W
fuente
1

@Darin Dimitrov y @ eth0 respuestas para usar con el uso de extensión de paquete:

@Html.Resources(a => new HelperResult(b => b.Write( System.Web.Optimization.Scripts.Render("~/Content/js/formBundle").ToString())), "jsTop")
Erkan
fuente