Menggunakan bagian di Editor / Template tampilan

104

Saya ingin menyimpan semua kode JavaScript saya dalam satu bagian; sebelum penutupanbody tag di halaman tata letak master saya dan hanya ingin tahu yang terbaik untuk melakukannya, gaya MVC.

Misalnya, jika saya membuat file DisplayTemplate\DateTime.cshtml file yang menggunakan JQuery UI's DateTime Picker daripada saya akan menyematkan JavaScript langsung ke template itu tetapi kemudian akan merender di tengah halaman.

Dalam tampilan normal saya, saya hanya dapat menggunakan @section JavaScript { //js here }dan kemudian @RenderSection("JavaScript", false)dalam tata letak master saya tetapi ini tampaknya tidak berfungsi di template tampilan / editor - ada ide?

eth0
sumber
4
bagi siapa pun yang datang ke sini nanti - ada paket nuget untuk menangani ini: nuget.org/packages/Forloop.HtmlHelpers
Russ Cam

Jawaban:

189

Anda dapat melanjutkan dengan menggabungkan dua pembantu:

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

dan kemudian di _Layout.cshtml:

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

dan di suatu tempat di beberapa template:

@Html.Script(
    @<script src="@Url.Content("~/Scripts/jquery-1.4.4.min.js")" type="text/javascript"></script>
)
Darin Dimitrov
sumber
3
Karena kamus tidak berurutan, bagaimana saya melakukan first in first out? Urutan yang dikeluarkannya acak (mungkin karena Panduan) ..
eth0
Mungkin Anda bisa menyiapkan bidang integer statis dan menggunakan Interlocked.Increment () sebagai pengganti GUID untuk mendapatkan pengurutan, tetapi meskipun demikian menurut saya kamus tidak pernah menjamin pengurutan. Jika dipikir-pikir, mungkin bidang statis itu cerdik karena mungkin terus melewati tampilan halaman. Sebagai gantinya dapat menambahkan integer ke kamus Item, tetapi Anda harus mengunci di sekitarnya.
Mark Adamson
Saya mulai menggunakan solusi ini baru-baru ini, tetapi saya tidak dapat memasukkan dua skrip dalam satu baris @ Html.Script (), karena saya tidak yakin bagaimana HelperResult bekerja. Apakah tidak mungkin melakukan 2 blok skrip dalam 1 panggilan Html.Script?
Langdon
2
@TimMeers, apa maksudmu? Bagi saya semua ini selalu usang. Saya tidak akan menggunakan pembantu itu sama sekali. Saya tidak pernah memiliki kebutuhan untuk memasukkan skrip apa pun dalam pandangan parsial saya. Saya hanya akan tetap berpegang pada Razor standar sections. Dalam MVC4, Bundling memang bisa digunakan sekaligus membantu mengurangi ukuran skrip.
Darin Dimitrov
4
Pendekatan ini tidak berfungsi jika Anda ingin menempatkan skrip atau gaya Anda di headtag alih-alih di akhir bodytag, karena @Html.RenderScripts()akan dijalankan sebelum tampilan parsial dan sebelumnya @Html.Script().
Maksim Vi.
41

Versi modifikasi dari jawaban Darwin untuk memastikan pemesanan. Juga bekerja dengan 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);
}

Anda dapat menambahkan sumber daya JS dan CSS seperti ini:

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

Dan render sumber daya JS dan CSS seperti ini:

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

Anda dapat melakukan pemeriksaan string untuk melihat apakah itu dimulai dengan skrip / tautan sehingga Anda tidak harus secara eksplisit mendefinisikan apa itu setiap sumber daya.

eth0
sumber
Terima kasih eth0. Saya telah mengkompromikan masalah ini, tetapi saya harus memeriksanya.
one.beat.consumer
Saya tahu ini hampir 2 tahun yang lalu, tetapi apakah ada cara untuk memeriksa apakah file css / js sudah ada dan tidak merendernya? Terima kasih
CodingSlayer
1
baik. Tidak yakin seberapa efisiennya, tetapi saat ini saya melakukan ini: var httpTemplates = HtmlHelper.ViewContext.HttpContext.Items [Type] as List <Func <object, HelperResult >>; var prevItem = dari q di httpTemplates di mana q (null) .ToString () == Template (null) .ToString () pilih q; if (! prevItem.Any ()) {// Add Template}
CodingSlayer
@imAbhi terima kasih, apa yang saya butuhkan, terlihat seperti bundel 1 lingkaran dengan item. ToString jadi saya akan berpikir itu harus cukup cepat
Kunukn
35

Saya menghadapi masalah yang sama, tetapi solusi yang diusulkan di sini berfungsi dengan baik hanya untuk menambahkan referensi ke sumber daya dan tidak terlalu cocok untuk kode JS sebaris. Saya menemukan artikel yang sangat membantu dan membungkus semua inline JS saya (dan juga tag script)

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

Dan dalam tampilan _Layout ditempatkan @Html.PageScripts() tepat sebelum menutup tag 'body'. Bekerja seperti pesona bagi saya.


Para pembantu itu sendiri:

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
sumber
3
ini adalah jawaban terbaik; itu juga memungkinkan Anda menyuntikkan hampir semua hal dan menundanya sampai akhir
drzaus
1
Anda harus menyalin-tempel kode dari artikel seandainya pernah turun! Ini jawaban yang bagus!
Shaamaan
Bagaimana kita bisa melakukan ini di inti asp.net
ramanmittal
13

Saya menyukai solusi yang diposting oleh @ john-w-harding, jadi saya menggabungkannya dengan jawaban oleh @ darin-dimitrov untuk membuat solusi yang mungkin terlalu rumit berikut yang memungkinkan Anda menunda rendering html (skrip juga) dalam blok penggunaan.

PEMAKAIAN

Dalam tampilan parsial berulang, hanya sertakan blok satu kali:

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

Dalam tampilan parsial (berulang?), Sertakan blok untuk setiap kali parsial digunakan:

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

Dalam tampilan parsial (berulang?), Sertakan blok satu kali, dan kemudian render secara spesifik dengan nama one-time:

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

Untuk merender:

@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

KODE

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
sumber
Aneh. Saya tidak ingat menyalin jawaban ke utas lain ini , tetapi saya melakukan penulisan yang sedikit lebih baik di sana ...
drzaus
12

Instal paket nuget Forloop.HtmlHelpers - ini menambahkan beberapa pembantu untuk mengelola skrip dalam tampilan parsial dan template editor.

Di suatu tempat di tata letak Anda, Anda perlu menelepon

@Html.RenderScripts()

Ini akan menjadi tempat file skrip dan blok skrip akan dikeluarkan di halaman jadi saya akan merekomendasikan meletakkannya setelah skrip utama Anda dalam tata letak dan setelah bagian skrip (jika Anda memilikinya).

Jika Anda menggunakan The Web Optimization Framework dengan bundling, Anda dapat menggunakan kelebihan beban

@Html.RenderScripts(Scripts.Render)

Sehingga metode ini digunakan untuk menulis file script.

Sekarang, kapan pun Anda ingin menambahkan file atau blok skrip dalam tampilan, tampilan parsial, atau templat, cukup gunakan

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

Para pembantu memastikan bahwa hanya satu referensi file skrip yang dirender jika ditambahkan beberapa kali dan itu juga memastikan bahwa file skrip dirender dalam urutan yang diharapkan, yaitu

  1. Tata Letak
  2. Partial dan Template (dalam urutan kemunculannya dalam tampilan, dari atas ke bawah)
Russ Cam
sumber
5

Posting ini sangat membantu saya jadi saya pikir saya akan memposting implementasi saya dari ide dasar. Saya telah memperkenalkan fungsi helper yang dapat mengembalikan tag skrip untuk digunakan dalam fungsi @ Html.Resource.

Saya juga menambahkan kelas statis sederhana sehingga saya dapat menggunakan variabel yang diketik untuk mengidentifikasi sumber daya JS atau 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));
    }
}

Dan sedang digunakan

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

Terima kasih kepada @Darin Dimitrov yang telah memberikan jawaban atas pertanyaan saya di sini .

Chris
sumber
2

Jawaban yang diberikan di Populate a Razor Section From a Partial menggunakan RequireScriptHtmlHelper mengikuti pola yang sama. Manfaatnya juga karena ia memeriksa dan menyembunyikan referensi duplikat ke URL Javascript yang sama, dan memiliki priorityparameter eksplisit yang dapat digunakan untuk mengontrol pengurutan.

Saya memperluas solusi ini dengan menambahkan metode untuk:

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

Saya suka solusi Darin's & eth0 karena mereka menggunakan HelperResulttemplate, yang memungkinkan blok skrip dan CSS, bukan hanya tautan ke file Javascript dan CSS.

Martin_W
sumber
1

@Darin Dimitrov dan @ eth0 jawaban untuk digunakan dengan penggunaan ekstensi bundel:

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