Javascript в компоненте View

У меня есть компонент View, содержащий некоторый jQuery в файле Razor (.cshtml). Сам script весьма специфичен для представления (имеет дело с некоторой конфигурацией сторонних библиотек), поэтому я хотел бы сохранить script и HTML в одном файле для организации ради.

Проблема заключается в том, что script не отображается в разделе _Layout Scripts. По-видимому, это просто как MVC обрабатывает скрипты в отношении View Components.

Я могу обойти это, просто имея скрипты в файле Razor, но не внутри раздела Scripts.

Но затем я сталкиваюсь с проблемами зависимостей - поскольку jQuery используется до ссылки на библиотеку (ссылка на библиотеку находится в нижней части файла _Layout).

Есть ли какое-нибудь умное решение для этого, кроме включения ссылки на jQuery как часть кода Razor (что помешает HTML-рендерингу, где бы он ни находился)?

В настоящее время я не вижу кода, но я могу, конечно, предоставить его, как только у меня появится шанс, если кому-то нужно взглянуть на него, чтобы лучше понять его.

Ответ 1

Я решил написать ScriptTagHelper, который предоставляет атрибут "OnContentLoaded". Если true, то я обертываю внутреннее содержимое тега script функцией Javascript для выполнения, как только документ будет готов. Это позволяет избежать проблемы с библиотекой jQuery, которая еще не загружалась при запуске сценария ViewComponent.

[HtmlTargetElement("script", Attributes = "on-content-loaded")]
public class ScriptTagHelper : TagHelper
{
  /// <summary>
  /// Execute script only once document is loaded.
  /// </summary>
  public bool OnContentLoaded { get; set; } = false;

  public override void Process(TagHelperContext context, TagHelperOutput output)
  {
     if (!OnContentLoaded)
     {
        base.Process(context, output);
     } 
     else
     {
        var content = output.GetChildContentAsync().Result;
        var javascript = content.GetContent();

        var sb = new StringBuilder();
        sb.Append("document.addEventListener('DOMContentLoaded',");
        sb.Append("function() {");
        sb.Append(javascript);
        sb.Append("});");

        output.Content.SetHtmlContent(sb.ToString()); 
     }
  }
}

Пример использования в ViewComponent:

<script type="text/javascript" on-content-loaded="true">
    $('.button-collapse').sideNav();
</script>

Возможно, есть лучший путь, чем этот, но это сработало для меня до сих пор.

Ответ 2

  Разделы работают только в представлении, не работают в частичном представлении или представлении Компонент (Default.cshtml выполняется независимо от основного ViewResult, и его значение Layout по умолчанию равно null.) И это по проекту.

Вы можете использовать его для render sections для Layout, который объявляет представление partial view или view component. Однако разделы, определенные в частичных компонентах и компонентах представления, не возвращаются в представление рендеринга или его макет. Если вы хотите использовать jQuery или другую библиотечную ссылку в вашем частичном представлении или компоненте View, вы можете перетащить свою библиотеку в head, а не в body на своей странице Layout.

Пример:

Просмотр компонента:

public class ContactViewComponent : ViewComponent
{
    public IViewComponentResult Invoke()
    {
        return View();
    }
}

Расположение ViewComponent:

/Views/[CurrentController]/Components/[NameOfComponent]/Default.cshtml
/Views/Shared/Components/[NameOfComponent]/Default.cshtml

Default.cshtml:

@model ViewComponentTest.ViewModels.Contact.ContactViewModel

<form method="post" asp-action="contact" asp-controller="home">
    <fieldset>
        <legend>Contact</legend>
        Email: <input asp-for="Email" /><br />
        Name: <input asp-for="Name" /><br />
        Message: <textarea asp-for="Message"></textarea><br />
        <input type="submit" value="Submit" class="btn btn-default" />
    </fieldset>
</form>

_Layout.cshtml :

    <!DOCTYPE html>
    <html>
    <head>
    ...
        @RenderSection("styles", required: false)
     <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script>
    </head>
    <body>
        @RenderBody()
    ...
    //script move to head
      @RenderSection("scripts", required: false)
    </body>
    </html>

View.cshtml:

@{
    Layout =  "~/Views/Shared/_Layout.cshtml";     
}
..................
@{Html.RenderPartial("YourPartialView");}
..................
@await Component.InvokeAsync("Contact")
..................
@section Scripts {
    <script>
    //Do somthing
    </script>
}

Просмотр компонентов grahamehorner: (дополнительную информацию вы можете найти здесь)

Скрипты @section не отображаются в ViewComponent, компоненте представления должен иметь возможность включать скрипты в раздел @в отличие Частичные представления в качестве компонентов могут быть повторно использованы во многих представлениях и Компонент отвечает за собственную функциональность.

Ответ 3

Вот решение, которое я использую. Он поддерживает как внешнюю загрузку script, так и пользовательский встроенный script. Хотя некоторый код установки должен идти в вашем файле макета (например, _Layout.cshtml), все организовано из компонента View.

Файл макета:

Добавьте это в начало страницы (внутри тега <head> - хорошее место):

<script>
    MYCOMPANY = window.MYCOMPANY || {};
    MYCOMPANY.delayed = null;
    MYCOMPANY.delay = function (f) { var e = MYCOMPANY.delayed; MYCOMPANY.delayed = e ? function () { e(); f(); } : f; };
</script>

Добавьте это в нижней части страницы (непосредственно перед закрывающим тегом </body>):

<script>MYCOMPANY.delay = function (f) { setTimeout(f, 1) }; if (MYCOMPANY.delayed) { MYCOMPANY.delay(MYCOMPANY.delayed); MYCOMPANY.delayed = null; }</script>

Просмотреть компонент:

Теперь в любом месте вашего кода, включая компонент View Component, вы можете сделать это и выполнить script в нижней части страницы:

<script>
    MYCOMPANY.delay(function() {
        console.log('Hello from the bottom of the page');
    });
</script>




Внешняя зависимость script

Но иногда этого недостаточно. Иногда вам нужно загрузить внешний файл JavaScript и только выполнить свой собственный script после загрузки внешнего файла. Вы также хотите организовать все это в своем компоненте View для полной инкапсуляции. Чтобы сделать это, я добавлю еще немного кода настройки на мою страницу макета (или внешний файл JavaScript, загруженный страницей макета), чтобы ленить загрузку внешнего файла script. Это выглядит так:

Файл макета:

Lazy load script

<script>
    MYCOMPANY.loadScript = (function () {
        var ie = null;
        if (/MSIE ([^;]+)/.test(navigator.userAgent)) {
            var version = parseInt(RegExp['$1'], 10);
            if (version)
                ie = {
                    version: parseInt(version, 10)
                };
        }

        var assets = {};

        return function (url, callback, attributes) {

            attributes || (attributes = {});

            var onload = function (url) {
                assets[url].loaded = true;
                while (assets[url].callbacks.length > 0)
                    assets[url].callbacks.shift()();
            };

            if (assets[url]) {

                if (assets[url].loaded)
                    callback();

                assets[url].callbacks.push(callback);

            } else {

                assets[url] = {
                    loaded: false,
                    callbacks: [callback]
                };

                var script = document.createElement('script');
                script.type = 'text/javascript';
                script.async = true;
                script.src = url;

                for (var attribute in attributes)
                    if (attributes.hasOwnProperty(attribute))
                        script.setAttribute(attribute, attributes[attribute]);

                // can't use feature detection, as script.readyState still exists in IE9
                if (ie && ie.version < 9)
                    script.onreadystatechange = function () {
                        if (/loaded|complete/.test(script.readyState))
                            onload(url);
                    };
                else
                    script.onload = function () {
                        onload(url);
                    };

                document.getElementsByTagName('head')[0].appendChild(script);
            }
        };
    }());
</script>

Просмотреть компонент:

Теперь вы можете сделать это в своем компоненте View (или где-нибудь еще в этом случае):

<script>
    MYCOMPANY.delay(function () {
        MYCOMPANY.loadScript('/my_external_script.js', function () {
            console.log('This executes after my_external_script.js is loaded.');
        });
    });
</script>

Чтобы немного почистить вещи, здесь помощник тега для компонента View (вдохновленный ответом @JakeShakesworth):

[HtmlTargetElement("script", Attributes = "delay")]
public class ScriptDelayTagHelper : TagHelper
{
    /// <summary>
    /// Delay script execution until the end of the page
    /// </summary>
    public bool Delay { get; set; }

    /// <summary>
    /// Execute only after this external script file is loaded
    /// </summary>
    [HtmlAttributeName("after")]
    public string After { get; set; }

    public override void Process(TagHelperContext context, TagHelperOutput output)
    {
        if (!Delay) base.Process(context, output);

        var content = output.GetChildContentAsync().Result;
        var javascript = content.GetContent();

        var sb = new StringBuilder();
        sb.Append("if (!MYCOMPANY || !MYCOMPANY.delay) throw 'MYCOMPANY.delay missing.';");
        sb.Append("MYCOMPANY.delay(function() {");
        sb.Append(LoadFile(javascript));
        sb.Append("});");

        output.Content.SetHtmlContent(sb.ToString());
    }

    string LoadFile(string javascript)
    {
        if (After == NullOrWhiteSpace)
            return javascript;

        var sb = new StringBuilder();
        sb.Append("if (!MYCOMPANY || !MYCOMPANY.loadScript) throw 'MYCOMPANY.loadScript missing.';");
        sb.Append("MYCOMPANY.loadScript(");
        sb.Append($"'{After}',");
        sb.Append("function() {");
        sb.Append(javascript);
        sb.Append("});");

        return sb.ToString();
    }
}

Теперь в моем компоненте View я могу это сделать:

<script delay="true" after="/my_external_script.js"]">
  console.log('This executes after my_external_script.js is loaded.');
</script>

Вы также можете оставить атрибут after, если компонент просмотра не имеет зависимости внешнего файла.

Ответ 5

Этот ответ для .netcore 2.1. Краткий ответ: введите экземпляр Microsoft.AspNetCore.Mvc.Razor.TagHelpers.ITagHelperComponentManager, который принимает JavaScript, необходимый для функциональности пользовательского интерфейса компонентов, в качестве последнего узла элемента body.

Длинный ответ, как я сюда попал и что у меня работает.

У меня есть много виджетов, которые использует мое приложение, которые имеют одинаковую структуру HTML, стиль и функциональность пользовательского интерфейса & логика, но надоело разные наборы данных.

Мое приложение использует идентификационные утверждения (роли) для динамического управления навигацией. Эти роли также определяют наборы данных, которые питают виджеты, которые можно комбинировать, чтобы обеспечить разные представления этих данных на одной странице.

Я использую Области, чтобы разделить этот код. Чтобы облегчить дальнейшее разделение задач, моему приложению необходимо отделить код виджета (извлечение данных, разметка, стили и функциональность) отдельно, и я решил использовать ViewComponents.

У меня была та же проблема в том, что моя функциональность зависит от библиотек JavaScript, которые я хочу сделать доступными для всего приложения в файле shared/_layout.

Мой сценарий рендеринга является последним элементом в файле _layout перед тегом body. Мой рендер тела состоит из нескольких узлов в структуре, поэтому мне нужно было как-то получить необходимый мне javascript, добавленный к коллекции дочерних узлов в элементе body. В противном случае мои компоненты не смогут создать необходимые объекты JavaScript для обеспечения необходимой функциональности.

В основе этого решения лежит TagHelperComponent и TagHelperComponentManager. Я создал класс, который наследуется от реализации абстрактного базового класса ITagHelperComponent этого файла, и поместил его в. /Pages/Shared/

Вот мой ScriptTagHelper

public class ScriptTagHelper : TagHelperComponent
{
    private readonly string _javascript;
    public ScriptTagHelper(string javascript = "") {
        _javascript = javascript;
    }
    public override Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
    {
        if (string.Equals(context.TagName, "body", StringComparison.Ordinal))
        {
            output.PostContent.AppendHtml($"<script type='text/javascript'>{_javascript}</script>");
        }
        return Task.CompletedTask;
    }
}

Код принимает строку javascript в качестве аргумента конструктора для помощника тега. Задача ProcessAsync переопределяется, чтобы найти тег body и добавить JavaScript.

Строка javascript обернута в тег Html-сценария, и эта полная Html-строка добавляется в конец любого содержимого, уже имеющегося в теле. (это любые фрагменты скрипта, вставленные на страницу скриптом рендеринга, и любой скрипт на странице макета).

Этот помощник по тегам используется путем внедрения зависимостей внутри ViewComponent. Вот пример виджета, подключенного для создания экземпляра ScriptTagHelper.

Это мой виджет распределения использования данных. В конечном итоге он отобразит распределение использования данных на круговой диаграмме, что при перемещении по определенному клину диаграммы также будет отображать больше выделений данных этого конкретного сегмента данных. На данный момент это всего лишь шаблон, который будут использовать все мои виджеты, чтобы помочь мне настроить назначение задач группе для начала создания ресурсов.

Это файл. /Pages/Shared/Components/DataDistribution/Default.cshtml

@model dynamic;
@using Microsoft.AspNetCore.Mvc.Razor.TagHelpers;
@inject ITagHelperComponentManager tagManager;

<div class="col-md-3 grid-margin stretch-card">
    <div class="card">
        <div class="card-body">
            <p class="card-title text-md-center text-xl-left">Data Usage Distribution</p>
            <div class="d-flex flex-wrap justify-content-between justify-content-md-center justify-content-xl-between align-items-center">
                <h3 class="mb-0 mb-md-2 mb-xl-0 order-md-1 order-xl-0">40016</h3>
                <i class="ti-agenda icon-md text-muted mb-0 mb-md-3 mb-xl-0"></i>
            </div>
            <p class="mb-0 mt-2 text-success">10.00%<span class="text-body ml-1"><small>(30 days)</small></span></p>
        </div>
    </div>
</div>


@{
    Layout = null;

    string scriptOptions = Newtonsoft.Json.JsonConvert.SerializeObject(Model);

    string script = @"";

    script += $"console.log({scriptOptions});";

    tagManager.Components.Add(new ScriptTagHelper(javascript: script));
}

Внедряя реализацию ITagHelperComponentManager по умолчанию, мы получаем доступ к коллекции компонентов помощника по тегам и можем добавить реализацию нашего собственного помощника по тегам. Задача помощников по тегам - поместить тег сценария, содержащий наш javascript для этого компонента представления, внизу тега body.

В этой простой заглушке кода теперь я могу безопасно и надежно ожидать загрузки всех моих вспомогательных библиотек пользовательского интерфейса перед тем, как начать рендеринг скрипта, который будет использовать эти библиотеки для изменения моего пользовательского интерфейса. Здесь я просто записываю на консоль мою сериализованную модель, возвращенную из моего компонента представления Таким образом, запуск этого кода на самом деле не докажет, что он работает, но любой может изменить переменную скрипта на консоль, записывающую версию jquery, если вы хотите убедиться, что jquery загружен в ваш файл макета.

Надеюсь, это кому-нибудь поможет. Я слышал, что в поддержку разделов в .netcore 3 приходят изменения, но те проблемы, с которыми сталкиваются люди, уже решены изначально и довольно элегантно в 2.1, как я надеюсь показать здесь.

Ответ 6

Допустим, у вас есть компоненты A и B, вы можете сохранить частичное представление _Scripts внутри папки компонентов:

Views
  Shared
    Components
      A
        Default.cshtml
        _Scripts.cshtml
      B
        Default.cshtml
        _Scripts.cshtml

На своей странице (где вы визуализируете компоненты) вы можете просто сделать это для каждого используемого вами компонента:

@await Component.InvokeAsync("A")
@await Component.InvokeAsync("B")

@section Scripts {
    @await Html.PartialAsync("~/Views/Shared/Components/A/_Scripts.cshtml", Model);
    @await Html.PartialAsync("~/Views/Shared/Components/B/_Scripts.cshtml", Model);
}

Внутри части _Scripts вы помещаете все, что хотите, в раздел Scripts.

Я не рекомендовал бы для сложных страниц, в которых у вас слишком много компоненты. Может быть полезно для нескольких компонентов, или где имя загружаемый компонент является динамическим.