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

Posted

技术标签:

【中文标题】包括特定于 ASP.NET MVC4 视图或部分视图的脚本【英文标题】:Including script specific to an ASP.NET MVC4 view or partial view 【发布时间】:2014-02-06 19:39:43 【问题描述】:

我查看了许多类似于How to add a script in a partial view in MVC4? 和MVC4 partial view javascript bundling Issue 的问题,但在涉及特定于视图的脚本时,我仍然难以理解 ASP.NET MVC 架构。对于其他试图在其 MVC4 部分视图中包含脚本的人来说,答案似乎是将脚本置于更高级别。但是某些脚本不能移动到更高的级别,它将在全球范围内运行。例如,我不想为未加载控件的视图模型运行应用 knockout.js 数据绑定的脚本。而且我不想在每次加载页面时为一大堆不活跃的视图运行一大堆脚本。

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

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

我走在正确的道路上吗?这是构建 MVC 应用程序的正确方法吗?

编辑发现这个问题与我的问题密切相关,并且包含了我回答的重要部分:Can you call ko.applyBindings to bind a partial view?

【问题讨论】:

是什么控制了部分的加载?任何控制加载的东西都不能负责应用绑定吗? @Html.Partial(Razor 语法)函数在其父级中包含一个视图作为部分视图。这是一个 .NET/MVC 框架函数,我不希望在已建立标准的情况下使用自定义函数来处理我的代码。 部分是有条件地呈现还是会一直存在?如果它总是在那里,我认为@JotaBe 的解决方案是一个很好的解决方案,如果它是有条件的,你需要动态拉入脚本,这可能会很痛苦,尽管你可能会使用像 RequireJS 这样的 AMD。 是的,我现在倾向于@JotaBe 的解决方案,但也刚刚发现了淘汰模板,它也可以作为替代解决方案或该解决方案的一部分。 (knockoutjs.com/documentation/template-binding.html) 我很好奇你决定朝哪个方向发展。你的决定有进展吗?发现任何陷阱或承诺? 【参考方案1】:

This is the best you can do, 但仍有问题:

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

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

最好的解决方案是让所有脚本在部分被重新渲染时都可用:

在连续页面中加载它们 动态加载它们(这更难)

如果您这样做,您可以在需要时运行这些脚本。但是,您如何仅在部分的所需部分上运行所需的脚本?更简单的方法是使用自定义 data- 属性标记它们。然后您可以“解析”页面,查找您的自定义 data- 属性,并运行适用的脚本:这是不显眼的 javascript。

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

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

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

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

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

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

主页:包含一个“解析器脚本”:这个解析器脚本负责:

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

部分

他们在 DOM 元素上有 data-attributes,以便解析器知道需要哪些脚本 他们有额外的data- 属性来传递额外的数据给脚本

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

了解如何动态下载脚本的好地方是:On-demand JavaScript

有很多解决方案。其他选项:How can I dynamically download and run a javascript script from a javascript console?

您的脚本应该将自己附加到单例,就像您在定义 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- 属性找到其他参数和选项。

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

【讨论】:

所以我认为我已经完成了您在此处讨论的架构的 90% 以上,因为我使用敲除进行所有数据绑定和 Ajax 加载数据,自动敲除拿起。我不知道该怎么办的一行是在问题中。如果我正确阅读了您的答案,我只需要将它也放在顶层并使其从视图和部分视图中获取信息,指示需要应用哪些视图模型? FWIW,我也有明显的 best 可能解决方案的类似实现,我主要使用它来确保我的脚本引用不会被多个局部视图模板复制共享依赖。但是,正如您从我的解决方案中看到的那样,这不是绝对必要的,我也不认为它是您能做的最好的。 @BlueMonkMN 我不太了解您的问题,但我会尽力回答:) 您不必在顶层加载所有内容。但是,您需要那里的“解析器”脚本。您可以使用data- 属性来指示每个部分(或部分中的 div 或表单)需要哪些脚本。而且,当您解析页面(准备好文档)或 ajax 加载的部分(ajax 成功)时,“解析器”脚本可以下载必要的脚本、JSON 数据等等......(续)跨度> 解析器脚本可以在主页管理一个单例对象,您可以在其中存储data-属性所需的脚本。通过足够的命名空间(选择一个好的约定!),您可以避免它们之间的冲突。因此解析器可以负责下载所需的脚本,并在它适用的 DOM 元素中运行它们。使用必要的data- 属性来指示要运行的脚本、获取JSON 数据的url 等等。 (这是一个不显眼的脚本......一旦你习惯了它就很容易实现,而且非常强大)。 @VinneyKelly 我想您指的是单例以避免下载重复的脚本。如果您动态下载它们(请参阅ajaxpatterns.org/On-Demand_Javascript)并将它们存储在单例中,您将拥有一个可以解决所有问题的按需 JavaScript 系统。在我的架构中,解析器决定 data- 属性需要哪个脚本,并检查它是否在单例中可用。如果它不可用下载。最后,脚本运行。当脚本运行时,它可以从其他额外的data- 属性中获取额外的信息。【参考方案2】:

这是我编写视图模型和视图的方式:

// ~/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.cshtml

<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 架构,它确实在我的开发周期中得到了改进。希望这也对您有益。我很乐意回答任何问题。

【讨论】:

感谢编辑!你觉得这有帮助吗?我注意到您最近发现了 ko 模板。对我来说,这是让所有这一切融合在一起的转折点。 在我决定它有多大帮助之前,我仍在努力思考它。这里有很多! 确实如此。如果您想向我提出任何问题,请随时发起聊天。 我认为这有助于证明 ko 模板可以用来回答这个问题。我不确定我是否会走那条路。同时,你能确认一下,你有没有在一个严肃的项目中没有后悔地使用过这个?您是否遇到过这种方法的任何缺点? 我在我当前的项目中使用它,这是一个库存控制系统(非常重要)。正如我之前所说,我已经磨练这种开发模型已经有一段时间了。我确信它还不完美,但它是迄今为止我尝试过的最好的 MVVM/SPA 模型。我还没有仔细研究过的一个潜在痛点是 WebGrease 脚本优化框架。老实说,我还没有做任何最小化,因为我们还没有处于优化阶段。不幸的是,我对此了解的还不够多,还不知道这种架构会遇到什么问题。【参考方案3】:

现有的答案不够详细,所以请允许我用代码提供详细的答案。我主要遵循 JotaBe 的回答的建议,这就是具体方法。

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

App_Code/Helpers.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://***.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 文件,还可以实现按需下载和定义脚本的不显眼机制并查看与淘汰赛绑定相关的模型。

【讨论】:

以上是关于包括特定于 ASP.NET MVC4 视图或部分视图的脚本的主要内容,如果未能解决你的问题,请参考以下文章

ASP.NET MVC4 部分视图

如何访问当前用户 ASP.net MVC4 的 SimpleMemberShipprovider 属性

ASP.NET MVC4 视图中的模型和 ItemModel 绑定

翻译转载官方教程Asp.Net MVC4入门指南:添加一个视图

如何在 Asp.Net MVC4 Razpr 视图中将操作链接作为图像?

关于ASP.NET MVC部分视图渲染问题。