Korzystanie z sekcji w szablonach edytora / wyświetlania

104

Chcę zachować cały mój kod JavaScript w jednej sekcji; tuż przed zamknięciembody tagiem na mojej głównej stronie układu i po prostu zastanawiam się, jak najlepiej to zrobić, w stylu MVC.

Na przykład, jeśli utworzę DisplayTemplate\DateTime.cshtmlplik, który używa DateTime Picker interfejsu jQuery UI, to osadziłbym JavaScript bezpośrednio w tym szablonie, ale wtedy będzie renderowany w połowie strony.

W moich normalnych widokach mogę po prostu użyć, @section JavaScript { //js here }a następnie @RenderSection("JavaScript", false)w moim głównym układzie, ale to nie działa w szablonach wyświetlania / edytora - jakieś pomysły?

eth0
źródło
4
dla każdego, kto przyjdzie do tego później - istnieje pakiet nuget
Russ Cam

Odpowiedzi:

189

Możesz kontynuować połączenie dwóch pomocników:

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

a następnie w _Layout.cshtml:

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

i gdzieś w jakimś szablonie:

@Html.Script(
    @<script src="@Url.Content("~/Scripts/jquery-1.4.4.min.js")" type="text/javascript"></script>
)
Darin Dimitrov
źródło
3
Ponieważ słownik jest nieuporządkowany, jak postąpiłbym najpierw na początku? Kolejność generowania jest losowa (prawdopodobnie z powodu Guid) ..
eth0
Być może mógłbyś ustawić statyczne pole liczb całkowitych i użyć Interlocked.Increment () zamiast identyfikatora GUID, aby uzyskać porządek, ale nawet wtedy myślę, że słownik nigdy nie gwarantuje uporządkowania. Po zastanowieniu może się wydawać, że statyczne pole jest podejrzane, ponieważ może pozostać na różnych stronach. Zamiast tego mógłbyś dodać liczbę całkowitą do słownika Items, ale musiałbyś nałożyć wokół niej blokadę.
Mark Adamson
Niedawno zacząłem używać tego rozwiązania, ale nie mogę umieścić dwóch skryptów w jednej linii @ Html.Script (), ponieważ nie jestem pewien, jak działa HelperResult. Czy nie jest możliwe wykonanie 2 bloków skryptu w 1 wywołaniu Html.Script?
Langdon
2
@TimMeers, co masz na myśli? Dla mnie to wszystko zawsze było przestarzałe. W ogóle nie użyłbym tych pomocników. Nigdy nie musiałem włączać żadnych skryptów do moich częściowych widoków. Po prostu trzymałbym się standardowego Razora sections. W MVC4 Bundling rzeczywiście może być użyty, ponieważ pomaga zmniejszyć rozmiar skryptów.
Darin Dimitrov
4
To podejście nie działa, jeśli chcesz umieścić swoje skrypty lub style w headtagu zamiast na końcu bodytagu, ponieważ @Html.RenderScripts()zostanie ono wykonane przed częściowym widokiem, a zatem przed @Html.Script().
Maksim Vi.
41

Zmodyfikowana wersja odpowiedzi Darina w celu zapewnienia zamówienia. Działa również z 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);
}

Możesz dodać zasoby JS i CSS w ten sposób:

@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")

Renderuj zasoby JS i CSS w ten sposób:

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

Możesz sprawdzić ciąg znaków, aby zobaczyć, czy zaczyna się od skryptu / linku, więc nie musisz jawnie definiować, czym jest każdy zasób.

eth0
źródło
Dzięki eth0. Poszedłem na kompromis w tej sprawie, ale będę musiał to sprawdzić.
one.beat.consumer
Wiem to prawie 2 lata temu, ale czy jest sposób, aby sprawdzić, czy plik css / js już istnieje i nie renderować go? Dzięki
CodingSlayer
1
dobrze. Nie jestem pewien, jak efektywne jest to, ale obecnie robię to: var httpTemplates = HtmlHelper.ViewContext.HttpContext.Items [Type] as List <Func <object, HelperResult >>; var prevItem = z q w httpTemplates, gdzie q (null) .ToString () == Template (null) .ToString () select q; if (! prevItem.Any ()) {// Add Template}
CodingSlayer
@imAbhi dzięki, właśnie to, czego potrzebowałem, wygląda jak pętla for 1 pakietów z item.ToString, więc myślę, że powinno być wystarczająco szybkie
Kunukn
35

Napotkałem ten sam problem, ale proponowane tutaj rozwiązania działają dobrze tylko przy dodawaniu odwołania do zasobu i nie są zbyt odpowiednie dla kodu wbudowanego JS. Znalazłem bardzo pomocny artykuł i zapakowałem cały mój wbudowany JS (a także tagi skryptów) w

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

I w widoku _Layout umieszczony @Html.PageScripts() tuż przed zamykającym tagiem „body”. U mnie działa jak urok.


Sami pomocnicy:

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
źródło
3
to jest najlepsza odpowiedź; pozwala też wstrzyknąć prawie wszystko i opóźnić to do końca
drzaus
1
Powinieneś skopiować i wkleić kod z artykułu na wypadek, gdyby kiedykolwiek się zepsuł! To doskonała odpowiedź!
Shaamaan
Jak możemy to zrobić w asp.net core
Ramanmittal
13

Podobało mi się rozwiązanie opublikowane przez @ john-w-harding, więc połączyłem je z odpowiedzią @ darin-dimitrov, aby stworzyć następujące, prawdopodobnie zbyt skomplikowane rozwiązanie, które pozwala opóźnić renderowanie dowolnego kodu HTML (również skryptów) w bloku using.

STOSOWANIE

W powtórzonym częściowym widoku uwzględnij blok tylko raz:

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

W (powtórzonym?) Widoku częściowym uwzględnij blok dla każdego użycia częściowej:

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

W (powtórzonym?) Widoku częściowym włącz blok raz, a później wyrenderuj go specjalnie według nazwy one-time:

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

Aby renderować:

@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

KOD

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
źródło
Dziwne. Nie pamiętam kopiowania odpowiedzi do tego innego wątku , ale zrobiłem tam trochę lepszy zapis ...
drzaus
12

Zainstaluj pakiet NuGet Forloop.HtmlHelpers - dodaje kilka pomocników do zarządzania skryptami w widokach częściowych i szablonach edytorów.

Gdzieś w swoim układzie musisz zadzwonić

@Html.RenderScripts()

Będzie to miejsce, w którym wszelkie pliki skryptów i bloki skryptów zostaną wyprowadzone na stronę, więc zalecam umieszczenie ich po głównych skryptach w układzie i po sekcji skryptów (jeśli taką masz).

Jeśli używasz platformy optymalizacji sieci Web z pakietowaniem, możesz użyć przeciążenia

@Html.RenderScripts(Scripts.Render)

tak aby ta metoda była używana do zapisywania plików skryptów.

Teraz, gdy chcesz dodać pliki skryptów lub bloki w widoku, częściowym widoku lub szablonie, po prostu użyj

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

Pomocnicy zapewniają, że tylko jedno odwołanie do pliku skryptu jest renderowane, jeśli jest dodawane wiele razy, a także zapewnia, że ​​pliki skryptów są renderowane w oczekiwanej kolejności, tj.

  1. Układ
  2. Części i szablony (w kolejności, w jakiej pojawiają się w widoku, od góry do dołu)
Russ Cam
źródło
5

Ten post naprawdę mi pomógł, więc pomyślałem, że opublikuję moją implementację podstawowego pomysłu. Wprowadziłem funkcję pomocniczą, która może zwracać tagi skryptu do użycia w funkcji @ Html.Resource.

Dodałem również prostą klasę statyczną, dzięki czemu mogę używać zmiennych wpisanych do identyfikowania zasobu JS lub 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));
    }
}

I w użyciu

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

Dziękuję @Darin Dimitrov, który udzielił odpowiedzi na moje pytanie tutaj .

Chris
źródło
2

Odpowiedź podana w Populate a Razor Section From a Partial using the RequireScriptHtmlHelper jest zgodna z tym samym wzorcem. Ma również tę zaletę, że sprawdza i eliminuje zduplikowane odniesienia do tego samego adresu URL JavaScript i ma wyraźnypriority parametr, którego można użyć do kontrolowania kolejności.

Rozszerzyłem to rozwiązanie o metody:

// 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) { ... }

Lubię jednak rozwiązania Darin & eth0, ponieważ używają HelperResultszablonu, który pozwala na skrypty i bloki CSS, a nie tylko linki do plików Javascript i CSS.

Martin_W
źródło
1

@Darin Dimitrov i @ eth0 odpowiedzi do użycia przy użyciu rozszerzenia pakietu:

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