Usando seções em modelos de Editor / Display

104

Quero manter todo o meu código JavaScript em uma seção; pouco antes da bodytag de fechamento em minha página de layout mestre e apenas me perguntando o melhor para fazer isso, estilo MVC.

Por exemplo, se eu criar um DisplayTemplate\DateTime.cshtmlarquivo que usa o seletor de DateTime do jQuery UI, então eu incorporaria o JavaScript diretamente nesse modelo, mas ele renderizará no meio da página.

Em minhas visualizações normais, posso apenas usar @section JavaScript { //js here }e, @RenderSection("JavaScript", false)em seguida, em meu layout mestre, mas isso não parece funcionar em modelos de exibição / editor - alguma ideia?

eth0
fonte
4
para qualquer um que vier a isso mais tarde - há um pacote nuget para lidar com isso: nuget.org/packages/Forloop.HtmlHelpers
Russ Cam

Respostas:

189

Você pode prosseguir com uma conjunção de dois ajudantes:

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;
    }
}

e então em seu _Layout.cshtml:

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

e em algum lugar em algum modelo:

@Html.Script(
    @<script src="@Url.Content("~/Scripts/jquery-1.4.4.min.js")" type="text/javascript"></script>
)
Darin Dimitrov
fonte
3
Como um dicionário não está ordenado, como eu faria o primeiro a entrar, primeiro a sair? A ordem de saída é aleatória (presumivelmente por causa do Guid) ..
eth0
Talvez você possa configurar um campo inteiro estático e usar Interlocked.Increment () no lugar do GUID para obter o pedido, mas mesmo assim acho que um dicionário nunca garante o pedido. Pensando bem, talvez um campo estático seja duvidoso, pois pode ser mantido nas exibições de páginas. Em vez disso, poderia adicionar um inteiro ao dicionário de Itens, mas você teria que colocar um cadeado em torno dele.
Mark Adamson
Comecei a usar essa solução recentemente, mas não consigo colocar dois scripts em uma única linha @ Html.Script (), porque não tenho certeza de como o HelperResult funciona. Não é possível fazer 2 blocos de script em 1 chamada Html.Script?
Langdon
2
@TimMeers, o que você quer dizer? Para mim, tudo isso sempre foi obsoleto. Eu não usaria esses ajudantes. Nunca tive a necessidade de incluir scripts em minhas visualizações parciais. Eu simplesmente me limitaria ao Razor padrão sections. Em MVC4, o Bundling pode realmente ser usado e também ajuda a reduzir o tamanho dos scripts.
Darin Dimitrov
4
Esta abordagem não funciona se você deseja colocar seus scripts ou estilos na headtag em vez de no final da bodytag, porque @Html.RenderScripts()será executado antes de sua visualização parcial e, portanto, antes @Html.Script().
Maksim Vi.
41

Versão modificada da resposta de Darin para garantir o pedido. Também funciona com 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);
}

Você pode adicionar recursos JS e 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")

E renderize recursos JS e CSS como este:

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

Você poderia fazer uma verificação de string para ver se ele começa com script / link para que você não precise definir explicitamente o que cada recurso é.

eth0
fonte
Obrigado, eth0. Eu me comprometi nessa questão, mas terei que verificar isso.
one.beat.consumer
Eu sei disso há quase 2 anos, mas existe uma maneira de verificar se o arquivo css / js já existe e não renderizá-lo? Obrigado
CodingSlayer
1
Está bem. Não tenho certeza de quão eficiente é, mas atualmente estou fazendo isso: var httpTemplates = HtmlHelper.ViewContext.HttpContext.Items [Type] as List <Func <object, HelperResult >>; var prevItem = de q em httpTemplates onde q (nulo) .ToString () == Template (nulo) .ToString () selecione q; if (! prevItem.Any ()) {// Adicionar modelo}
CodingSlayer
@imAbhi obrigado, exatamente o que eu precisava, parece um 1 for-loop de pacotes com item.ToString, então eu acho que deveria ser rápido o suficiente
Kunukn
35

Eu enfrentei o mesmo problema, mas as soluções propostas aqui funcionam bem apenas para adicionar referência ao recurso e não são muito adequadas para código JS embutido. Encontrei um artigo muito útil e envolvi todo o meu JS inline (e também as tags de script) em

@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>
}

E na visualização _Layout posicionada @Html.PageScripts()antes de fechar a tag 'body'. Funciona como um encanto para mim.


Os próprios ajudantes:

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
fonte
3
Esta é a melhor resposta; também permite injetar praticamente qualquer coisa e atrasá-lo até o final
drzaus
1
Você deve copiar e colar o código do artigo para o caso de ele cair! Esta é uma excelente resposta!
Shaamaan
Como podemos fazer isso no núcleo do asp.net
ramanmittal
13

Gostei da solução postada por @ john-w-harding, então combinei com a resposta de @ darin-dimitrov para fazer a seguinte solução provavelmente complicada que permite atrasar a renderização de qualquer html (scripts também) dentro de um bloco em uso.

USO

Em uma visualização parcial repetida, inclua o bloco apenas uma vez:

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

Em uma vista parcial (repetida?), Inclua o bloco para cada vez que a parcial for usada:

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

Em uma vista parcial (repetida?), Inclua o bloco uma vez e, posteriormente, renderize-o especificamente pelo nome 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
fonte
Esquisito. Não me lembro de ter copiado a resposta para este outro tópico , mas
escrevi
12

Instale o pacote nuget Forloop.HtmlHelpers - ele adiciona alguns auxiliares para gerenciar scripts em visualizações parciais e modelos de editor.

Em algum lugar do seu layout, você precisa ligar

@Html.RenderScripts()

É aqui que quaisquer arquivos de script e blocos de script serão produzidos na página, então eu recomendo colocá-lo após seus scripts principais no layout e após uma seção de scripts (se você tiver uma).

Se estiver usando o Web Optimization Framework com empacotamento, você pode usar a sobrecarga

@Html.RenderScripts(Scripts.Render)

para que este método seja usado para escrever arquivos de script.

Agora, sempre que desejar adicionar arquivos de script ou blocos em uma visão, visão parcial ou modelo, basta usar

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

Os ajudantes garantem que apenas uma referência de arquivo de script seja renderizada se adicionada várias vezes e também garante que os arquivos de script sejam renderizados em uma ordem esperada, ou seja,

  1. Layout
  2. Parciais e modelos (na ordem em que aparecem na visualização, de cima para baixo)
Russ Cam
fonte
5

Esse post realmente me ajudou, então pensei em postar minha implementação da ideia básica. Eu introduzi uma função auxiliar que pode retornar tags de script para uso na função @ Html.Resource.

Também adicionei uma classe estática simples para que possa usar variáveis ​​digitadas para identificar um recurso JS ou 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));
    }
}

E em uso

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

Obrigado a @Darin Dimitrov que forneceu a resposta à minha pergunta aqui .

Chris
fonte
2

A resposta dada em Preencher uma seção do Razor de um parcial usando o RequireScriptHtmlHelper segue o mesmo padrão. Ele também tem a vantagem de verificar e suprimir referências duplicadas para o mesmo URL Javascript e possui um códigopriority parâmetro que pode ser usado para controlar o pedido.

Estendi esta solução adicionando 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) { ... }

Eu gosto das soluções da Darin e da eth0, porque eles usam o HelperResultmodelo, que permite blocos de script e CSS, não apenas links para arquivos Javascript e CSS.

Martin_W
fonte
1

Respostas @Darin Dimitrov e @ eth0 para usar com o uso de extensão de pacote:

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