我有一个View组件,其中包含Razor(.cshtml)文件中的一些jQuery。脚本本身非常特定于视图(处理一些第三方库的配置),所以我想将脚本和HTML保存在同一个文件中,以便组织起来。
问题是脚本未在_Layout Scripts部分中呈现。显然,这只是关于View Components的how MVC handles脚本。
我可以通过在Razor文件中使用但脚本部分中 的脚本来解决这个问题。
但后来我遇到依赖问题 - 因为在引用库之前使用了jQuery(对库的引用位于_Layout文件的底部附近)。
除了将jQuery的引用作为Razor代码的一部分包含在内之外,是否有任何巧妙的解决方案(这将阻止HTML呈现放置组件的位置)?
我目前不在代码的前面,但是如果有机会,如果有人需要查看它以便更好地了解它,我当然可以提供它。
答案 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
用于Layout
或partial 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对象以提供所需的功能。
此解决方案的根源是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标签底部。
在这个简单的代码存根中,现在我可以安全可靠地期望在开始渲染将使用这些库修改我的UI的脚本之前加载所有支持的UI库。在这里,我只是控制台记录从视图组件返回的序列化模型。因此,运行此代码实际上并不能证明其有效,但是,如果要确保将jquery加载到布局文件中,则任何人都可以更改脚本变量以控制台记录jquery的版本。
希望这对某人有帮助。我听说更改即将支持.netcore 3中的部分,但是人们所遇到的那些问题已经在2.1中以本机且优雅的方式解决,如我希望在此处所示。
答案 4 :(得分:0)
假设您有组件A
和B
,则可以在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
部分中。
我不建议您使用过多的复杂页面 组件。对于很少的组件或其中的名称可能很有用 正在加载的组件是动态的。