it-swarm-pt.tech

Injetando conteúdo em seções específicas a partir de uma visão parcial da ASP.NET MVC 3 com o Razor View Engine

Eu tenho esta seção definida no meu _Layout.cshtml

@RenderSection("Scripts", false)

Eu posso facilmente usá-lo a partir de uma visão: 

@section Scripts { 
    @*Stuff comes here*@
}

O que eu estou lutando é como obter algum conteúdo injetado dentro desta seção a partir de uma visão parcial.

Vamos supor que esta é a minha página de visualização: 

@section Scripts { 

    <script>
        //code comes here
    </script>
}

<div>
    poo bar poo
</div>

<div>
  @Html.Partial("_myPartial")
</div>

Eu preciso injetar algum conteúdo dentro da seção Scripts da visão parcial _myPartial.

Como posso fazer isso?

293
tugberk

Seções não funcionam em visões parciais e isso é por design. Você pode usar alguns ajudantes personalizados para obter um comportamento semelhante, mas honestamente, é responsabilidade da visão incluir os scripts necessários, não a responsabilidade da parcial. Eu recomendaria usar a seção @scripts da visualização principal para fazer isso e não ter a preocupação parcial sobre scripts.

214
Darin Dimitrov

Essa é uma pergunta bastante popular, então vou postar minha solução.
Eu tive o mesmo problema e, embora não seja o ideal, acho que funciona muito bem e não depende parcial da visão.
Meu cenário era que uma ação era acessível por si só, mas também poderia ser incorporada em uma visão - um mapa do google.

No meu _layout eu tenho: 

@RenderSection("body_scripts", false)

Na minha visualização index eu tenho:

@Html.Partial("Clients")
@section body_scripts
{
    @Html.Partial("Clients_Scripts")
}

Na minha visualização clients eu tenho (todo o mapa e assoc. Html):

@section body_scripts
{
    @Html.Partial("Clients_Scripts")
}

Meu modo de exibição Clients_Scripts contém o javascript para ser renderizado na página

Dessa forma, meu script é isolado e pode ser renderizado na página onde for necessário, com a tag body_scripts sendo renderizada apenas na primeira ocorrência em que o mecanismo de visualização de lâmina o encontra.

Isso me permite ter tudo separado - é uma solução que funciona muito bem para mim, outros podem ter problemas com isso, mas corrige o buraco "por design".

78
dan richardson

A partir das soluções em este thread , eu criei a seguinte solução provavelmente supercomplicada que permite atrasar a renderização de qualquer html (scripts também) dentro de um bloco de uso.

USO

Crie a "seção"

  1. Cenário típico: Em uma visão parcial, inclua apenas o bloco uma vez, não importa quantas vezes a visão parcial seja repetida na página:

    @using (Html.Delayed(isOnlyOne: "some unique name for this section")) {
        <script>
            someInlineScript();
        </script>
    }
    
  2. Em uma visão parcial, inclua o bloco para cada vez que a parcial for usada:

    @using (Html.Delayed()) {
        <b>show me multiple times, @Model.Whatever</b>
    }
    
  3. Em uma visão parcial, inclua apenas o bloco uma vez, não importa quantas vezes a parcial seja repetida, mas depois a renderize especificamente pelo nome when-i-call-you:

    @using (Html.Delayed("when-i-call-you", isOnlyOne: "different unique name")) {
        <b>show me once by name</b>
        <span>@Model.First().Value</span>
    }
    

Renderize as "seções"

(isto é, exibe a seção atrasada em uma exibição pai)

@Html.RenderDelayed(); // writes unnamed sections (#1 and #2, excluding #3)
@Html.RenderDelayed("when-i-call-you", false); // writes the specified block, and ignore the `isOnlyOne` setting so we can dump it again
@Html.RenderDelayed("when-i-call-you"); // render the specified block by name
@Html.RenderDelayed("when-i-call-you"); // since it was "popped" in the last call, won't render anything due to `isOnlyOne` provided in `Html.Delayed`

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


}
36
drzaus

Eu tive esse problema e usei this technique.

É a melhor solução que encontrei, que é muito flexível. 

Também por favor voteaqui para adicionar suporte para declaração de seção cumulativa

15
iBoy

Se você tiver uma necessidade legítima de executar uma js de uma partial, veja como você pode fazer isso, jQuery é obrigatório:

<script type="text/javascript">        
    function scriptToExecute()
    {
        //The script you want to execute when page is ready.           
    }

    function runWhenReady()
    {
        if (window.$)
            scriptToExecute();                                   
        else
            setTimeout(runWhenReady, 100);
    }
    runWhenReady();
</script>
8
Serj Sagan

Seguindo o princípio unobtrusive , não é necessário que "_myPartial" injete conteúdo diretamente na seção de scripts. Você pode adicionar esses scripts de vista parcial ao arquivo .js separado e referenciá-los na seção @scripts da vista pai.

8
archil

Existe uma falha fundamental na maneira como pensamos sobre a web, especialmente ao usar o MVC. A falha é que o JavaScript é, de alguma forma, responsabilidade da exibição. Uma visão é uma visão, JavaScript (comportamental ou não) é JavaScript. No padrão MVVM do Silverlight e do WPF, estamos diante de "view first" ou "model first". No MVC, devemos sempre tentar raciocinar do ponto de vista do modelo e o JavaScript faz parte desse modelo de várias maneiras. 

Eu sugeriria usar o AMD pattern (eu mesmo gosto de RequireJS ). Separe seu JavaScript em módulos, defina sua funcionalidade e conecte-se ao seu html a partir do JavaScript, em vez de confiar em uma visualização para carregar o JavaScript. Isto irá limpar o seu código, separar as suas preocupações e tornar a vida mais fácil, tudo de uma só vez.

4
Mr. Baudin

A primeira solução que consigo pensar é usar o ViewBag para armazenar os valores que devem ser renderizados. 

Onestly eu nunca tentei se este trabalho de uma visão parcial, mas deveria imo.

2
Iridio

Você pode usar estes métodos de extensão : (Salvar como PartialWithScript.cs)

namespace System.Web.Mvc.Html
{
    public static class PartialWithScript
    {
        public static void RenderPartialWithScript(this HtmlHelper htmlHelper, string partialViewName)
        {
            if (htmlHelper.ViewBag.ScriptPartials == null)
            {
                htmlHelper.ViewBag.ScriptPartials = new List<string>();
            }

            if (!htmlHelper.ViewBag.ScriptPartials.Contains(partialViewName))
            {
                htmlHelper.ViewBag.ScriptPartials.Add(partialViewName);
            }

            htmlHelper.ViewBag.ScriptPartialHtml = true;
            htmlHelper.RenderPartial(partialViewName);
        }

        public static void RenderPartialScripts(this HtmlHelper htmlHelper)
        {
            if (htmlHelper.ViewBag.ScriptPartials != null)
            {
                htmlHelper.ViewBag.ScriptPartialHtml = false;
                foreach (string partial in htmlHelper.ViewBag.ScriptPartials)
                {
                    htmlHelper.RenderPartial(partial);
                }
            }
        }
    }
}

Use assim:

Exemplo parcial: (_MyPartial.cshtml) Coloque o html no if, e o js no else.

@if (ViewBag.ScriptPartialHtml ?? true)
    <p>I has htmls</p>
}
else {
    <script type="text/javascript">
        alert('I has javascripts');
    </script>
}

Em seu _Layout.cshtml, ou onde quer que os scripts das partials on sejam renderizados, coloque o seguinte (uma vez): Ele renderizará apenas o javascript de todos os parciais na página atual neste local.

@{ Html.RenderPartialScripts(); }

Então, para usar o seu parcial, basta fazer o seguinte: Ele irá renderizar apenas o html neste local.

@{Html.RenderPartialWithScript("~/Views/MyController/_MyPartial.cshtml");}
1
Lomak

Existe uma maneira de inserir seções em vistas parciais, embora não seja bonita. Você precisa ter acesso a duas variáveis ​​da Vista pai. Como parte do propósito de sua visão parcial é criar essa seção, faz sentido exigir essas variáveis.

Aqui está o que parece para inserir uma seção na visão parcial:

@model KeyValuePair<WebPageBase, HtmlHelper>
@{
    Model.Key.DefineSection("SectionNameGoesHere", () =>
    {
        Model.Value.ViewContext.Writer.Write("Test");
    });
}

E na página, inserindo a visão parcial ...

@Html.Partial(new KeyValuePair<WebPageBase, HtmlHelper>(this, Html))

Você também pode usar essa técnica para definir o conteúdo de uma seção programaticamente em qualquer classe.

Apreciar!

1
Pluto

Isso funcionou para mim, permitindo-me co-localizar javascript e html para visualização parcial no mesmo arquivo. Ajuda com o processo de pensamento para ver o html e a parte relacionada no mesmo arquivo de visão parcial.


In View, que usa a Vista Parcial chamada "_MyPartialView.cshtml"

<div>
    @Html.Partial("_MyPartialView",< model for partial view>,
            new ViewDataDictionary { { "Region", "HTMLSection" } } })
</div>

@section scripts{

    @Html.Partial("_MyPartialView",<model for partial view>, 
                  new ViewDataDictionary { { "Region", "ScriptSection" } })

 }

No arquivo de vista parcial

@model SomeType

@{
    var region = ViewData["Region"] as string;
}

@if (region == "HTMLSection")
{


}

@if (region == "ScriptSection")
{
        <script type="text/javascript">
    </script">
}
1
purvin

Você não pode precisar usar seções na visão parcial.

Inclua na sua Vista Parcial. Executa a função após o jQuery carregado. Você pode alterar a cláusula de condição do seu código.

<script type="text/javascript">    
var time = setInterval(function () {
    if (window.jQuery != undefined) {
        window.clearInterval(time);

        //Begin
        $(document).ready(function () {
           //....
        });
        //End
    };
}, 10); </script>

Julio Spader

1
Julio Spader

Usando Mvc Core você pode criar um TagHelper scripts como visto abaixo. Isso pode facilmente ser transformado em uma tag section, onde você também dá um nome (ou o nome é retirado do tipo derivado). Observe que a injeção de dependência precisa ser configurada para IHttpContextAccessor.

Ao adicionar scripts (por exemplo, em uma parcial)

<scripts>
    <script type="text/javascript">
        //anything here
    </script>
</scripts>

Ao enviar os scripts (por exemplo, em um arquivo de layout)

<scripts render="true"></scripts>

Código

public class ScriptsTagHelper : TagHelper
    {
        private static readonly object ITEMSKEY = new Object();

        private IDictionary<object, object> _items => _httpContextAccessor?.HttpContext?.Items;

        private IHttpContextAccessor _httpContextAccessor;

        public ScriptsTagHelper(IHttpContextAccessor httpContextAccessor)
        {
            _httpContextAccessor = httpContextAccessor;
        }

        public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
        {
            var attribute = (TagHelperAttribute)null;
            context.AllAttributes.TryGetAttribute("render",out attribute);

            var render = false;

            if(attribute != null)
            {
                render = Convert.ToBoolean(attribute.Value.ToString());
            }

            if (render)
            {
                if (_items.ContainsKey(ITEMSKEY))
                {
                    var scripts = _items[ITEMSKEY] as List<HtmlString>;

                    var content = String.Concat(scripts);

                    output.Content.SetHtmlContent(content);
                }
            }
            else
            {
                List<HtmlString> list = null;

                if (!_items.ContainsKey(ITEMSKEY))
                {
                    list = new List<HtmlString>();
                    _items[ITEMSKEY] = list;
                }

                list = _items[ITEMSKEY] as List<HtmlString>;

                var content = await output.GetChildContentAsync();

                list.Add(new HtmlString(content.GetContent()));
            }
        }
    }
0
BlackjacketMack

Eu resolvi isso uma rota completamente diferente (porque eu estava com pressa e não queria implementar um novo HtmlHelper):

Eu embrulhei minha Visão Parcial em uma grande declaração if-else:

@if ((bool)ViewData["ShouldRenderScripts"] == true){
// Scripts
}else{
// Html
}

Então, chamei o Partial duas vezes com um ViewData personalizado:

@Html.Partial("MyPartialView", Model, 
    new ViewDataDictionary { { "ShouldRenderScripts", false } })

@section scripts{
    @Html.Partial("MyPartialView", Model, 
        new ViewDataDictionary { { "ShouldRenderScripts", true } })
}
0
Rick Love

como alternativa, você poderia usar um Folder/index.cshtml como uma masterpage e adicionar scripts de seção. Então, no seu layout você tem:

@RenderSection("scripts", required: false) 

e seu index.cshtml:

@section scripts{
     @Scripts.Render("~/Scripts/file.js")
}

e funcionará sobre todas as suas visões parciais. Funciona para mim

0
RogerEdward

Acabei de adicionar este código na minha visão parcial e resolvi o problema, embora não muito limpo, funciona. Você precisa ter certeza de que os IDs dos objetos que você está processando.

0
luis

Eu tive um problema semelhante, onde eu tinha uma página mestra da seguinte forma:

@section Scripts {
<script>
    $(document).ready(function () {
        ...
    });
</script>
}

...

@Html.Partial("_Charts", Model)

mas a visão parcial dependia de algum JavaScript na seção Scripts. Eu resolvi isso codificando a visão parcial como JSON, carregando-a em uma variável JavaScript e usando isso para preencher uma div, então:

@{
    var partial = Html.Raw(Json.Encode(new { html = Html.Partial("_Charts", Model).ToString() }));
}

@section Scripts {
<script>
    $(document).ready(function () {
        ...
        var partial = @partial;
        $('#partial').html(partial.html);
    });
</script>
}

<div id="partial"></div>
0
John M

A ideia de Plutão de uma maneira melhor:

CustomWebViewPage.cs:

    public abstract class CustomWebViewPage<TModel> : WebViewPage<TModel> {

    public IHtmlString PartialWithScripts(string partialViewName, object model) {
        return Html.Partial(partialViewName: partialViewName, model: model, viewData: new ViewDataDictionary { ["view"] = this, ["html"] = Html });
    }

    public void RenderScriptsInBasePage(HelperResult scripts) {
        var parentView = ViewBag.view as WebPageBase;
        var parentHtml = ViewBag.html as HtmlHelper;
        parentView.DefineSection("scripts", () => {
            parentHtml.ViewContext.Writer.Write(scripts.ToHtmlString());
        });
    }
}

Visualizações\web.config:

<pages pageBaseType="Web.Helpers.CustomWebViewPage">

Visão:

@PartialWithScripts("_BackendSearchForm")

Parcial (_BackendSearchForm.cshtml):

@{ RenderScriptsInBasePage(scripts()); }

@helper scripts() {
<script>
    //code will be rendered in a "scripts" section of the Layout page
</script>
}

Página de layout:

@RenderSection("scripts", required: false)
0
PaulSanS

O objetivo do OP é que ele deseja definir scripts embutidos em sua Visão Parcial, o que eu suponho que este script é específico apenas para a Visão Parcial, e tenha esse bloco incluído em sua seção de script.

Eu entendo que ele quer que a Visão Parcial seja auto contida. A ideia é semelhante aos componentes ao usar o Angular. 

Meu caminho seria apenas manter os scripts dentro da Visão Parcial como está. Agora, o problema é que, ao chamar a Visualização Parcial, ela pode executar o script lá antes de todos os outros scripts (que normalmente é adicionado à parte inferior da página de layout). Nesse caso, você apenas tem o script da Visão Parcial esperando pelos outros scripts. Existem várias maneiras de fazer isso. O mais simples, que usei antes, é usar um evento em body

No meu layout, eu teria algo no fundo assim:

// global scripts
<script src="js/jquery.min.js"></script>
// view scripts
@RenderSection("scripts", false)
// then finally trigger partial view scripts
<script>
  (function(){
    document.querySelector('body').dispatchEvent(new Event('scriptsLoaded'));
  })();
</script>

Então na minha vista parcial (na parte inferior):

<script>
  (function(){
    document.querySelector('body').addEventListener('scriptsLoaded', function() {

      // .. do your thing here

    });
  })();
</script>

Outra solução é usar uma pilha para enviar todos os seus scripts e ligar para cada um deles no final. Outra solução, como já mencionado, é o padrão RequireJS/AMD, que funciona muito bem também.

0
alans