包括特定于ASP.NET MVC4视图或部分视图的脚本



我已经研究了许多类似于"如何在MVC4中的部分视图中添加脚本?和MVC4部分视图javascript绑定问题,当涉及到特定于视图的脚本时,我仍在努力理解ASP.NET MVC架构。对于那些试图将脚本包含在MVC4部分视图中的其他人来说,答案似乎是将脚本放在更高的级别。但有些脚本无法移动到更高的级别,在那里它将在全球范围内运行。例如,我不想为控件未加载的视图模型运行应用knockout.js数据绑定的脚本。我不想每次加载页面时都为一大堆不活动的视图运行一大堆脚本。

因此,我开始在.vbhtml视图中使用视图特定的@Section Script块来包含视图特定的脚本。然而,正如其他人指出的那样,这并不是片面的。我正在对我们的体系结构进行原型设计,看看我们在这里能做什么,不能做什么。我想,在某些情况下,我可能可以将视图用作局部视图,反之亦然。但是,当将视图拉入以用作局部视图时,@Section Script块不会渲染。我已经成功地全局定义了我的所有视图模型脚本,这样我只需要运行一行代码就可以创建和绑定视图模型,但我仍然需要这一行代码才能在特定视图处于活动状态时运行。在局部视图中,哪里可以适当地添加这行代码?

ko.applyBindings(window.webui.inventoryDetailViewModel(ko, webui.inventorycontext));

我走的路对吗?这是设计MVC应用程序的正确方法吗?

编辑发现这个问题与我的问题密切相关,并包含了我答案的重要部分:你能调用ko.applyBindings来绑定部分视图吗?

以下是我如何构建视图模型和视图:

// ~/scripts/app/viewModels/primaryViewModel.js
var primaryViewModelFactory = (function() {
return { // this gives a singleton object for defining static members and preserving memory
init: init
}
function init(values) {
var model = {
// initialization
secondaryViewModel: secondaryViewModelFactory.init(values);
}
// I've decided to allow root-level view models to call apply bindings directly
ko.applyBindings(model);
}
}());
// ~/scripts/app/viewModels/secondaryViewModel.js
var secondaryViewModelFactory = (function() {
return { 
init: init
}
function init(values, target) {
return = {
// initialize object
};
}        
}());

在我的视图中,我的主模板中确实有一个脚本部分。所以我的观点是这样的:

@section scripts {
<script src="~/scripts/app/viewModels/....js"></script>
$(function() {
var vm = primaryViewModel.init(@Html.Raw(Json.Encode(Model)); 
});
}

事实上,我越是编写这些MVVM应用程序,就越倾向于使用ajax加载数据,而不是将模型数据传递到init函数中。这使我能够将init调用移到工厂中。然后你会得到这样的东西:

var primaryViewModelFactory = (function() {
init();        
function init(values) {
var model = {
// initialization
}
model.secondaryViewModel = secondaryViewModelFactory.init(values, model);
// I've decided to allow root-level view models to call apply bindings directly
ko.applyBindings(model);
}
}());

这将我的视图脚本简化为一个简单的脚本标记:

@section scripts {
<script src="~/scripts/app/viewModels/primaryViewModel.js"></script>        
}

最后,我喜欢在部分视图中为vm组件创建脚本模板,比如:

部分视图,位于~/Views/Shared/ScriptTemplates/_secondaryViewModelTemplates.cs.html

<script src="@Url.Content("~/scripts/app/viewModels/secondaryViewModel.js")"></script>
<script id="secondary-view-model-details-readonly-template" type="text/html">...</script>
<script id="secondary-view-model-details-editor-template" type="text/html">...</script>
<script id="secondary-view-model-summary-template" type="text/html">...</script>

这里发生了一些事情。首先,导入关联的脚本。这样可以确保在渲染部分时包含必要的视图模型工厂脚本。这允许主视图对子组件(可能有多个子组件)的脚本需求保持无知。此外,通过在分部文件中而不是在脚本文件中定义模板,我们还能够利用非常有用的HtmlHelper和UrlHelper以及您选择的任何其他服务器端实用程序。

最后,我们在主视图中呈现模板:

@section scripts {
@* primaryViewModel has a dependency on secondaryViewModel so the order does matter *@
@Html.Partial("ScriptTemplates/_secondaryViewModelTemplates.cshtml")
<script src="~/scripts/app/viewModels/primaryViewModel.js"></script>
}
<div data-bind="template: {name: 'secondary-view-model-details-editor-template', with: secondaryViewModel}"></div>

这是很多代码,而且都是用SO编写的,所以可能会有一些错误。在过去的几年里,我一直在发展这种MVVM+MVC架构风格,它确实在我的开发周期中有所改进。希望这对你也有益。我很乐意回答任何问题。

这是你能做的最好的事情,但仍然可能存在问题:

  • 如果您的部分视图被缓存了怎么办
  • 如果使用Ajax渲染局部视图会怎样

所以,我也建议不要使用这个技巧。(Darin Dimitrov的解决方案很棒,但使用它不是一个好主意)。

最好的解决方案是当部分被rednered时,所有的脚本都可用:

  • 在继续页中加载它们
  • 动态加载(这很难做到)

如果执行此操作,则可以在需要时运行脚本。但是,如何只在部分的desireds部分上运行所需的脚本?更简单的方法是用自定义的data-属性标记它们。然后,您可以"解析"页面,查找自定义的data-属性,并运行应用的脚本:这是一个不引人注目的javascript。

例如,您可以在jQuery的$(document).ready上包含一个"解析"页面的脚本(当所有页面和所有脚本都完成加载时)。此脚本可以查找具有自定义data-属性($('[data-my-custom-attr]').each( MyCustomSccript(this));的元素

您还可以考虑data-属性可用于配置脚本,即您可以使用一个属性来指示必须运行某种脚本,并使用额外的属性来配置脚本的运行方式。

那么,加载ajax的部分视图呢?没问题。我告诉过你可以使用$(document).ready,但在用ajax加载部分视图的函数中也有success回调,你可以在这个回调中进行完全相同的调用。您可以为jQuery.Ajax成功注册一个全局处理程序,因此您的脚本将应用于所有加载了ajax的部分。

您甚至可以使用更强大的技术,比如根据属性的需要动态加载部分所需的脚本。

通常,问题是,我们认为JavaScript应该从服务器提供,但事实是JavaScript存在于浏览器上,浏览器应该对它有更多的控制

具有脚本动态加载的架构描述:

  • 主页:包含一个"解析器脚本":这个解析器脚本负责:

    • 解析页面(文档就绪事件)或ajax下载的部分(ajax成功事件)
    • 下载并将所需的脚本存储在页面中的单例中(所需的由"data-"属性定义)
    • 运行脚本(存储在singleton中)
  • 部分

    • 它们在DOM元素上有data-属性,这样解析器就知道需要哪些脚本
    • 它们具有额外的data-属性,用于将额外的数据传递给脚本

显然,遵循一个良好的约定来命名脚本和data-属性非常重要,这样代码更容易使用和调试。

了解如何动态下载脚本的一个好地方是:按需JavaScript

有很多解决方案。其他选项:如何从javascript控制台动态下载并运行javascript脚本?

您的脚本应该将自己附加到singleton,就像您定义jQUery插件时所做的那样。js的内容如下:

if (!MySingleton.MyNamespace) MySingleton.MyNamespe = {};
MySigleton.MyNamespace.ScriptA = {
myFunction: function($element) { 
// check extra data for running from `data-` attrs in $element
// run the script
},
scriptConfig: { opt1: 'x', opt2: 23 ... }
}

关于如何实现解析器的一点线索:

MySingleton = {
parseElement = function(selector) {
$(selector).find(`[data-reqd-script]`).each(
function() {
var reqdScript = $(this).attr('data-reqd-script');
// check if Singleton contains script, if not download
if (!MySingleton.hasOwnProperty(reqdScript)) {
// donwload the script
}
// run the script on $(this) element
MySingleton[reqdScript].myFunction($(this));
});
}
}
// Parse the page !!
$(document).ready(function() {
MySingleton.Parse('body');
}
// You can also subscribe it to parse all downloaded ajax, or call it 
// on demand on the success of ajax donwloands of partial views

遵循正确的约定是绝对必要的,这样解析器才能运行必要的脚本。

要运行的函数的名称可以是另一个data-属性,也可以始终与init相同。由于该函数可以访问DOM元素,因此可以使用其他data-属性找到其他参数和选项。

这似乎很难实现,但一旦你建立了一个工作框架,你就可以很容易地完成和改进它。

现有的答案不够详细,所以请允许我用代码提供详细的答案。我基本上听从了约塔贝的建议,具体做法如下。

首先,我为我将使用的自定义("数据")属性设计了一个方案,并创建了一个助手函数来应用它,以帮助我与ASP.Net绑定兼容。该属性需要提供必要的信息,以便在启用绑定优化时下载单个绑定文件(BundleTable.EnableOptimizations = True),否则下载几个独立文件。您可以在下面代码的注释中看到我为data-model属性确定的格式。这段代码被放入一个名为Helpers.vbhtml的文件中,该文件被添加到我的主项目中的一个新文件夹App_Code中。

应用程序代码/帮助程序.vbhtml

@*
Purpose:       Retrieve a value for the WebUI-specific data-model attribute which will
apply knockout bindings for the current node based on the specified
bundle, factory, and context.
BundleNameUrl: Bundle URL like "~/bundles/inventory"
FactoryName:   Client side factory class of the view model like "inventoryViewModel"
ContextName:   Client side context object that provides methods for retrieving
and updating the data fromt he client, like "inventorycontext"
ForceNew:      If True, a new instance of the view model will always be created;
If False, a previously created instance will be reused when possible.
Output:        In debug mode, the escaped (&quot;) version of a string like
{"bundle": "~/bundles/inventory", "sources": ["/Scripts/app/inventory.datacontext.js",
"/Scripts/app/inventory.model.js","/Scripts/app/inventorydetail.viewmodel.js",
"/Scripts/app/inventory.viewmodel.js"], "factory": "inventoryViewModel",
"context": "inventorycontext", "forceNew": false}
Or in release mode, like
{"bundle": "~/bundles/inventory", "sources": 
["/bundles/inventory?v=YaRZhEhGq-GkPEQDut6enckUI6FH663GEN4u2-0Lo1g1"],
"factory": "inventoryViewModel", "context": "inventorycontext", "forceNew": false}
*@
@Helper GetModel(BundleNameUrl As String, FactoryName As String, ContextName As String, Optional ForceNew As Boolean = False)
@Code
Dim result As New System.Text.StringBuilder()
result.Append("{""bundle"": """ & BundleNameUrl & """, ""sources"": [")
Dim httpCtx As New HttpContextWrapper(HttpContext.Current)
' When EnableOptimizations = True, there will be one script source URL per bundle
' When EnableOptimizations = False, each script in the bundle is delivered separately
If BundleTable.EnableOptimizations Then
result.Append("""" & System.Web.Mvc.UrlHelper.GenerateContentUrl( _
BundleResolver.Current.GetBundleUrl(BundleNameUrl), httpCtx) & """")
Else
Dim first As Boolean = True
For Each bundle In BundleResolver.Current.GetBundleContents(BundleNameUrl)
If first Then first = False Else result.Append(",")
result.Append("""" & System.Web.Mvc.UrlHelper.GenerateContentUrl(bundle, httpCtx) & """")
Next
End If
result.Append("], ""factory"": """ & FactoryName & """, ""context"": """ & ContextName & """")
result.Append(", ""forceNew"": " & If(ForceNew, "true", "false") & "}")
End Code
@<text>@result.ToString()</text>
End Helper

然后,我可以在这样的节点上应用该属性,让它指示它希望如何将敲除绑定应用于自己及其后代,以及在这样做之前需要什么脚本。请注意,我的意图是能够从多个节点引用同一个脚本捆绑包和模型,而不会复制下载或具有重复的模型实例,除非我特别请求使用forceNew的模型的单独实例。最好添加一个容器,将该属性放在一个地方,但我想证明这不是必要的。

Views/Inventory/Details.html

<a href="#" data-bind="click: loadPrevious" data-model="@Helpers.GetModel("~/bundles/inventory", "inventoryDetailViewModel", "inventorycontext")" title="Previous">Previous</a>
<a href="#" data-bind="click: loadNext" data-model="@Helpers.GetModel("~/bundles/inventory", "inventoryDetailViewModel", "inventorycontext")" title="Next">Next</a>
<fieldset data-bind="with: fsItem" data-model="@Helpers.GetModel("~/bundles/inventory", "inventoryDetailViewModel", "inventorycontext")">

最后,我创建了一个在现有捆绑包中引用的javascript文件,该捆绑包总是在_Layout.vbhtml中引入。它具有处理新的"数据模型"属性所需的客户端代码。其思想是在这些特定节点上调用ko.applyBindings,并且只实例化视图模型一次,除非在多个节点上明确请求模型的不同实例。

脚本/app/webui.main.js

// Make sure we have our namespace carved out, and we
// know we're going to put a scriptCache in it.
window.webui = window.webui || { "scriptCache": {} };
// Copied from http://stackoverflow.com/a/691661/78162
// jQuery's getScript uses a mechanism that is not debuggable
// when operating within the domain, so we use this code to
// make sure the code is always a debuggable part of the DOM.
window.webui.getScript = function (url, callback) {
var head = document.getElementsByTagName("head")[0];
var script = document.createElement("script");
script.src = url;
// Handle Script loading
{
var done = false;
// Attach handlers for all browsers
script.onload = script.onreadystatechange = function () {
if (!done && (!this.readyState ||
this.readyState == "loaded" || this.readyState == "complete")) {
done = true;
if (callback)
callback();
// Handle memory leak in IE
script.onload = script.onreadystatechange = null;
}
};
}
head.appendChild(script);
// We handle everything using the script element injection
return undefined;
};
// Call knockout's applyBindings function based on values specified in the
// data-model attribute after the script is done downloading (which is the
// responsibility of the caller).
window.webui.applyBindings = function (cacheObj, forceNew, factory, context, node) {
// Store instantiated view model objects for each factory in
// window.webui.scriptCache[bundleName].models for reuse on other nodes.
cacheObj.models = cacheObj.models || {};
// If an instance of the model doesn't exist yet, create one by calling the
// factory function, which should be implemented in a script in the
// downloaded bundle somewhere. And the context object should have already
// been instantiated when the script was downloaded.
if (forceNew || !cacheObj.models[factory])
cacheObj.models[factory] = window.webui[factory](ko, window.webui[context]);
// Apply bindings only to the node where data-model attribute was applied
ko.applyBindings(cacheObj.models[factory], node);
};
// Callback function when a script specified in the data-model attribute is
// done being downloaded on demand.
window.webui.onModelLoaded = function (cacheObj) {
// Count how many scripts inteh bundle have finished downloading
cacheObj.loadedCount += 1;
// If we have downloaded all scripts in the bundle, call applyBindings
// for all the nodes stored in the onComplete array.
if (cacheObj.loadedCount == cacheObj.totalCount) {
for (var callback in cacheObj.onComplete) {
var onComplete = cacheObj.onComplete[callback];
window.webui.applyBindings(cacheObj, onComplete.forceNew,
onComplete.factory, onComplete.context, onComplete.node);
}
}
};
// Process the data-model attribute of one HTML node by downloading the related bundle
// scripts if they haven't yet been downloaded and then calling applyBindings based on
// the values embedded in the attribute.
window.webui.require = function (modelAttribute, node) {
model = $.parseJSON(modelAttribute);
// Keep a cache of all the bundles that have been downloaded so we don't download the same
// bundle more than once even if multiple nodes refer to it.
window.webui.scriptCache = window.webui.scriptCache || {};
// The cache is keyed by bundle name. All scripts in a bundle are downloaded before
// any bindings are applied.
if (!window.webui.scriptCache[model.bundle]) {
// Store the expectd count and the loaded count so we know when the last
// script in the bundle is done that it's time to apply the bindings.
var cacheObj = {
totalCount: model.sources.length, loadedCount: 0, onComplete:
[{ "factory": model.factory, "context": model.context, "node": node, "forceNew": model.forceNew }]
};
window.webui.scriptCache[model.bundle] = cacheObj;
// For each script in the bundle, start the download, and pass in cacheObj
// so the callback will know if it has downloaded the last script and what
// to do when it has.
for (var script in model.sources) {
window.webui.getScript(model.sources[script], function () {
window.webui.onModelLoaded(cacheObj)
});
}
} else {
// If the bundle referenced already has a space allocated in the cache, that means
// its scripts are already downloaded or are in the process of being downloaded.
var cacheObj = window.webui.scriptCache[model.bundle];
if (cacheObj.totalCount == cacheObj.loadedCount) {
// If the bundle is already completely downloadad, just apply the bindings directly
window.webui.applyBindings(cacheObj, model.forceNew, model.factory, model.context, node);
} else {
// If the bundle is still being downloaded, add work to be done when bindings
// are applied upon completion.
window.webui.scriptCache[model.bundle].onComplete.push({
"factory": model.factory, "context": model.context, "node": node, "forceNew": model.forceNew
});
}
}
};
// When the document is done loading, locate every node with a data-model attribute
// and process the attribute value with the require function above on that node.
$(document).ready(function () {
$('[data-model]').each(function () {
var model = $(this).data("model");
window.webui.require(model, this);
});
});

有了这个解决方案,我可以依靠现有的ASP.NET MVC4绑定框架(我不需要r.js)来优化和组合javascript文件,还可以实现按需下载和一种无结构的机制来定义与敲除绑定相关的脚本和视图模型。

最新更新