视图组件中的Javascript

时间:2017-06-13 02:16:30

标签: javascript c# asp.net-mvc razor asp.net-core-mvc

我有一个View组件,其中包含Razor(.cshtml)文件中的一些jQuery。脚本本身非常特定于视图(处理一些第三方库的配置),所以我想将脚本和HTML保存在同一个文件中,以便组织起来。

问题是脚本未在_Layout Scripts部分中呈现。显然,这只是关于View Components的how MVC handles脚本。

我可以通过在Razor文件中使用脚本部分 的脚本来解决这个问题。

但后来我遇到依赖问题 - 因为在引用库之前使用了jQuery(对库的引用位于_Layout文件的底部附近)。

除了将jQuery的引用作为Razor代码的一部分包含在内之外,是否有任何巧妙的解决方案(这将阻止HTML呈现放置组件的位置)?

我目前不在代码的前面,但是如果有机会,如果有人需要查看它以便更好地了解它,我当然可以提供它。

5 个答案:

答案 0 :(得分:15)

我决定写一个ScriptTagHelper,它提供" OnContentLoaded"的属性。如果为true,那么我将使用Javascript函数包装脚本标记的内部内容,以便在文档准备好后执行。这避免了当ViewComponent的脚本触发时jQuery库尚未加载的问题。

[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>

可能有一种比这更好的方法,但到目前为止这对我有用。

答案 1 :(得分:4)

  

部分仅适用于查看,无法在部分视图视图中工作   组件Default.cshtml独立于主ViewResult执行,默认情况下其Layout值为null。)这是设计的。

您可以将render sections用于Layoutpartial view视图声明的view component's。但是,在局部视图和视图组件中定义的部分不会流回渲染视图或其布局。 如果您想在部分视图或View组件中使用jQuery或其他库引用,那么您可以将库拉到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查看组件(您可以提供更多信息here

  

@section脚本不会在ViewComponent视图组件中渲染   应该能够在@section中包含脚本   作为组件的部分视图可以在许多视图中重用   组件负责它自己的功能。

答案 2 :(得分:0)

这是我正在使用的解决方案。它支持外部脚本加载和自定义内联脚本。虽然某些设置代码需要放在您的布局文件中(例如_Layout.cshtml),但所有内容都是在View Component中编排的。

布局文件:

在页面顶部附近添加(在rddPair.aggregateByKey((0, 0, 0, 0))( (iTotal, oisubtotal) => (iTotal._1 + oisubtotal._1, iTotal._2 + oisubtotal._2, iTotal._3 + oisubtotal._3, iTotal._4 + oisubtotal._4 ), (fTotal, iTotal) => (fTotal._1 + iTotal._1, fTotal._2 + iTotal._2, fTotal._3 + iTotal._3, fTotal._4 + iTotal._4) ) 标记内部是一个好地方):

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

查看组件

现在,您的代码中的任何位置(包括View Component)都可以执行此操作,并在页面底部执行脚本:

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



使用外部脚本依赖

但有时这还不够。有时您需要加载外部JavaScript文件,并且只在加载外部文件后才执行自定义脚本。您还希望在View Component中编排所有内容以进行完全封装。为此,我在布局页面(或布局页面加载的外部JavaScript文件)中添加了一些设置代码,以延迟加载外部脚本文件。看起来像这样:

布局文件:

延迟加载脚本

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

查看组件

现在,您可以在View Component中(或其他任何地方)执行此操作:

<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 Component的标签助手(灵感来自@JakeShakesworth的答案):

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

现在在我的View Component中我可以这样做:

[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 Component没有外部文件依赖关系,您也可以省略<script delay="true" after="/my_external_script.js"]"> console.log('This executes after my_external_script.js is loaded.'); </script> 属性。

答案 3 :(得分:0)

此答案适用于.netcore 2.1。简短的答案,注入一个Microsoft.AspNetCore.Mvc.Razor.TagHelpers.ITagHelperComponentManager的实例,该实例将组件UI功能所需的javascript作为body元素的最后一个节点。

长回答我如何到达这里以及对我有什么用。

我有许多我的应用程序使用的小部件,这些小部件具有相同的HTML结构,样式以及UI功能和逻辑,但使用了不同的数据集。

我的应用使用身份声明(角色)来动态控制导航。这些角色还确定了提供小部件的数据集,这些小部件可以组合在一起以在单个页面上提供该数据的不同视图。

我使用Areas来使代码分开。为了促进进一步的关注点分离,我的应用程序需要将小部件代码(数据检索,标记,样式和功能)保持分离,因此我选择使用ViewComponents。

我遇到了同样的问题,因为我的功能取决于我想对shared / _layout文件中的整个应用程序可用的javascript库。

我的脚本渲染是body标签之前_layout文件上的最后一个元素。我的身体渲染是结构中的几个节点,所以我需要以某种方式将需要的javascript附加到body元素的子节点集合中。否则,我的组件将无法创建必要的javascript对象以提供所需的功能。

此解决方案的根源是TagHelperComponentTagHelperComponentManager。我创建了一个类,该类从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标签底部。

在这个简单的代码存根中,现在我可以安全可靠地期望在开始渲染将使用这些库修改我的UI的脚本之前加载所有支持的UI库。在这里,我只是控制台记录从视图组件返回的序列化模型。因此,运行此代码实际上并不能证明其有效,但是,如果要确保将jquery加载到布局文件中,则任何人都可以更改脚本变量以控制台记录jquery的版本。

希望这对某人有帮助。我听说更改即将支持.netcore 3中的部分,但是人们所遇到的那些问题已经在2.1中以本机且优雅的方式解决,如我希望在此处所示。

答案 4 :(得分:0)

假设您有组件AB,则可以在components文件夹中保留_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部分中。

  

我不建议您使用过多的复杂页面   组件。对于很少的组件或其中的名称可能很有用   正在加载的组件是动态的。