简体   繁体   English

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

[英]Including script specific to an ASP.NET MVC4 view or partial view

I've looked at a number of questions similar to How to add a script in a partial view in MVC4? 我看过一些类似于如何在MVC4的局部视图中添加脚本的问题 and MVC4 partial view javascript bundling Issue and am still struggling to understand ASP.NET MVC architecture when it comes to view-specific script. MVC4部分视图javascript捆绑问题 ,当涉及特定于视图的脚本时,我仍然在努力理解ASP.NET MVC架构。 It seems the answer to others who have tried to include script in their MVC4 partial views is to put the script at a higher level. 对于试图在其MVC4部分视图中包含脚本的其他人而言,似乎是将脚本置于更高级别的答案。 But some script can't be moved to a higher level where it will run more globally. 但是某些脚本无法移动到更高级别,它将在全局范围内运行更多。 For example, I don't want to run script that applies knockout.js data bindings for a view model whose controls aren't loaded. 例如,我不想运行将knockout.js数据绑定应用于未加载控件的视图模型的脚本。 And I don't want to run a whole bunch of script for a whole bunch of views that aren't active every time I load a page. 而且我不希望为每次加载页面时都不活动的一大堆视图运行一大堆脚本。

So I started using the view-specific @Section Script blocks in my .vbhtml views to include script specific to a view. 所以我开始在我的.vbhtml视图中使用特定于视图的@Section Script块来包含特定于视图的脚本。 However, as pointed out by others, this does not work in a partial view. 但是,正如其他人所指出的,这在局部视图中不起作用。 I am prototyping our architecture to see what we can and can't do here. 我正在构建我们的架构原型,看看我们能做什么,不能做什么。 I'd like to think that I might be able, in some cases, to use a view as a partial view and vice versa. 我想在某些情况下,我可以将视图用作部分视图,反之亦然。 But when you pull in a view to use as a partial view the @Section Script block does not render. 但是当您拉入视图以用作部分视图时, @Section Script块不会呈现。 I have managed to get all my viewmodel script defined globally in a way such that I need only run one line of code to create and bind a view model, but I still need that one line of code to run only when a particular view is active. 我已经设法以一种方式全局定义我的所有viewmodel脚本,这样我只需要运行一行代码来创建和绑定视图模型,但我仍然只需要在特定视图处于活动状态时运行一行代码。 Where can I appropriately add this line of code in a partial view? 我在哪里可以在局部视图中适当添加这行代码?

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

Am I going down the right path here? 我在这里走正路吗? Is this a proper way to architect an MVC application? 这是构建MVC应用程序的正确方法吗?

Edit Found this question very closely related to my problem, and includes a significant part of my answer: Can you call ko.applyBindings to bind a partial view? 编辑发现这个问题与我的问题非常密切相关,并且包含了我的答案的重要部分: 你能调用ko.applyBindings来绑定部分视图吗?

This is the best you can do, but there can be still problems: 这是你能做的最好的,但仍然存在问题:

  • What if your partial view is cached? 如果部分视图被缓存怎么办?
  • What if you render the partial view with Ajax? 如果使用Ajax渲染局部视图怎么办?

So, I also recommend don't doing using this hacky trick. 所以,我也建议不要使用这个hacky技巧。 (Well, Darin Dimitrov's solution is great, but using it it not a good idea). (好吧,Darin Dimitrov的解决方案很棒,但使用它并不是一个好主意)。

The best solution is to have all the scripts available when the partial is rednered: 最好的解决方案是在部分被删除时使所有脚本可用:

  • loading them in the contianing page 将它们装入连续页面
  • loading them dynamically (that's harder to do) 动态加载它们(这很难做到)

If you do this, you can run the scripts when they are needed. 如果执行此操作,则可以在需要时运行脚本。 But then, how do you only run the desired scripts on the desireds parts of your partials? 但是,你如何只在部分的所需部分上运行所需的脚本? The easier way is to mark them with custom data- attributes. 更简单的方法是使用自定义data-属性标记它们。 Then you can "parse" the page, looking for your custom data- attributes, and running the scripts that apply: that's unobtrusive javascript. 然后,您可以“解析”页面,查找自定义data-属性,并运行适用的脚本:这是不引人注目的javascript。

For example, you can include an script that "parses" the page on jQuery's $(document).ready (when all the page, and all the scripts have finished loading). 例如,您可以在jQuery的$(document).ready包含一个“解析”页面的脚本(当所有页面都已完成加载时)。 This script can look for the elements with the custom data- attributes ($('[data-my-custom-attr]').each( MyCustomSccript(this)); 此脚本可以查找具有自定义data-属性的元素($('[data-my-custom-attr]').each( MyCustomSccript(this));

You can also take into account that the data- attributes can be used to configure your script, ie you can use an attribute to indicate that some kind of script must be run, and extra attributes to configure how the script runs. 您还可以考虑data-属性可用于配置脚本,即您可以使用属性指示必须运行某种脚本,并使用额外属性来配置脚本的运行方式。

And, what about partial views loaded with ajax? 那么,用ajax加载的部分视图呢? No problem. 没问题。 I told you could use $(document).ready , but you also have success callbacks in the functions used to load partial views with ajax, and you can make exactly the same on this callbacks. 我告诉过你可以使用$(document).ready ,但是你在使用ajax加载部分视图的函数中也有success回调,你可以在这个回调中完全相同。 An you can register a global handler for jQuery.Ajax success, so your scripts will be applied to all your ajax loaded partials. 您可以为jQuery.Ajax成功注册一个全局处理程序,因此您的脚本将应用于所有加载了ajax的部分。

And you can even use more powerful techniques, like loading dynamically the scripts needed for your partials, as required for the attributes. 您甚至可以使用更强大的技术,例如根据属性的需要动态加载部分所需的脚本。

Usually, the problem, is that we think that JavaScript should be supplied from the server, but the truth is that JavaScript lives on the browser, and the browser should have more control on it 通常,问题是我们认为应该从服务器提供JavaScript,但事实是JavaScript存在于浏览器上,浏览器应该对它有更多的控制权

Description of architecture with dynamic loading of scripts: 动态加载脚本的体系结构描述:

  • main page: include a "parser script": this parser script is responsible for: 主页面:包含“解析器脚本”:此解析器脚本负责:

    • parsing the page (document ready event) or the ajax downloaded partial (ajax success event) 解析页面(文档就绪事件)或ajax下载部分(ajax成功事件)
    • downloading, and storing the required scripts in a singleton in the page (the required are defined by `data-' attributes) 下载并将所需脚本存储在页面中的单例中(所需的由`data-'属性定义)
    • running the scripts (which are stored in the singleton) 运行脚本(存储在单例中)
  • partials 谐音

    • they have data- attributes on DOM elements so that the parser knows which scripts are required 他们在DOM元素上有data-属性,以便解析器知道需要哪些脚本
    • they have additional data- attributes to pass extra data to the scripts 他们有额外的data-属性来将额外的数据传递给脚本

Obviously, it's very important to follow a good convention to name the scripts and the data- attributes, so that the code is easier to use and debug. 显然,遵循一个好的约定来命名脚本和data-属性是非常重要的,这样代码就更容易使用和调试。

A good place to see how the scripts can be dynamically downloaded is: On-demand JavaScript 查看脚本如何动态下载的好地方是: 按需JavaScript

There are many solutions. 有很多解决方案。 Other option: How can I dynamically download and run a javascript script from a javascript console? 其他选项: 如何从javascript控制台动态下载和运行javascript脚本?

Your script should attach itself to the singleton, just like you do when you define a jQUery plugin. 您的脚本应该将自身附加到单例,就像您定义jquery插件时一样。 the content of a .js would be like this: .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 ... }
}

A little clue on how to implement the parser: 关于如何实现解析器的一点线索:

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

Following the right conventions is absolutely neccessary so that the parser can run the necessary script. 遵循正确的约定是绝对必要的,以便解析器可以运行必要的脚本。

The name of the function to run could be another data- attributes, or be always the same like init . 要运行的函数的名称可以是另一个data-属性,或者始终与init相同。 As this function can acces the DOM element, it can find there other parameters and options using other data- attributes. 由于此函数可以访问DOM元素,因此可以使用其他data-属性找到其他参数和选项。

This can seem hard to implement, but once you have set up a working skeleton you can complete and improve it easily. 这看起来很难实现,但是一旦设置了工作骨架,就可以轻松完成并改进它。

Here's how I've been composing view models and views: 以下是我编写视图模型和视图的方法:

// ~/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
        };
    }        
}());

In my Views, I do have a Script section in my master template. 在我的视图中,我的主模板中有一个脚本部分。 So my view looks like this: 所以我的观点看起来像这样:

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

In fact, the more I write these MVVM apps, the more inclined I am use ajax for loading data and not pass model data into the init function. 事实上,我写这些MVVM应用程序越多,我就越倾向于使用ajax来加载数据而不是将模型数据传递给init函数。 This enables me to move the init call into the factory. 这使我能够将init调用移动到工厂中。 So then you get something like: 那么你会得到类似的东西:

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);
    }
}());

Which reduces my view script to a simple script tag: 这将我的视图脚本简化为一个简单的脚本标记:

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

Lastly, I like to create script templates for vm components inside of partial views like so: 最后,我喜欢在部分视图中为vm组件创建脚本模板,如下所示:

Partial view at ~/Views/Shared/ScriptTemplates/_secondaryViewModelTemplates.cshtml 〜/ 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>

A couple of things going on here. 这里发生了一些事情。 First, the associated script is imported. 首先,导入关联的脚本。 This ensures that the necessary view model factory script is included when the partial is rendered. 这可确保在呈现部分时包含必要的视图模型工厂脚本。 This allows the master view to remain ignorant to the script needs of the sub-component (of which it may have multiple). 这允许主视图对子组件(其可能具有多个)的脚本需求保持无知。 Also, by defining the templates in a partial rather than in a script file, we're also able to utilize the wildly helpful HtmlHelper and UrlHelper as well as any other server-side utilities you so chose. 此外,通过在部分文件中而不是在脚本文件中定义模板,我们还可以使用非常有用的HtmlHelper和UrlHelper以及您选择的任何其他服务器端实用程序。

Finally, we render the template in the main view: 最后,我们在主视图中渲染模板:

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

That's a lot of code and it was all written in SO so there could be some errors. 这是很多代码,它都是用SO编写的,所以可能会有一些错误。 I've been evolving this style of MVVM+MVC architecture for the past couple of years and it's really made an improvement in my development cycles. 在过去的几年里,我一直在不断发展这种MVVM + MVC架构,它的开发周期确实得到了改进。 Hopefully this will be beneficial to you as well. 希望这对你也有好处。 I'd be happy to answer any questions. 我很乐意回答任何问题。

The existing answers weren't quite detailed enough, so allow me to provide a detailed answer with code. 现有的答案不够详细,所以请允许我用代码提供详细的答案。 I mostly followed the suggestion of JotaBe's answer, and here's exactly how. 我大多遵循JotaBe的答案的建议,而这正是如何。

First I devised a scheme for what custom ("data") attribute I would use and created a helper function to apply it in a way that would help me be compatible with ASP.Net bundling. 首先,我设计了一个方案,用于我将使用的自定义(“数据”)属性,并创建一个帮助函数,以便以一种有助于我与ASP.Net捆绑兼容的方式应用它。 The attribute needs to provide the necessary information to download a single bundle file when bundling optimizations are turned on ( BundleTable.EnableOptimizations = True ) and several independent files otherwise. 该属性需要提供必要的信息,以便在打开捆绑优化( BundleTable.EnableOptimizations = True )时下载单个捆绑包文件,否则提供若干独立文件。 You can see the format I settled on for a data-model attribute in the comments on the code below. 您可以在下面代码的注释中看到我为data-model属性确定的格式。 This code went into a file called Helpers.vbhtml which was added to a new folder App_Code in my main project. 此代码进入一个名为Helpers.vbhtml的文件,该文件已添加到我的主项目中的新文件夹App_Code中。

App_Code/Helpers.vbhtml 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

Then I can apply that attribute on a node like this to have it indicate how it wants knockout bindings applied to itself and its descendants and what scripts are needed before doing so. 然后我可以在这样的节点上应用该属性,以指示它如何在应用之前将knockout绑定应用于自身及其后代以及需要哪些脚本。 Notice how my intention is to be able to refer to the same script bundle and model from multiple nodes without duplicating the download or having duplicate instances of the model unless I specifically request separate instances of the model with forceNew . 请注意我的意图是如何能够从多个节点引用相同的脚本包和模型,而不会重复下载或具有模型的重复实例,除非我特别使用forceNew请求模型的单独实例。 It would probably be better to add a container to house this attribute in a single place, but I want to demonstrate that it's not necessary. 在一个地方添加一个容器来容纳这个属性可能会更好,但我想证明它没有必要。

Views/Inventory/Details.html 查看/库存/ 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")">

Finally I create a javascript file referenced in an existing bundle that's always pulled in in _Layout.vbhtml . 最后,我创建了一个在现有包中引用的javascript文件,该文件总是在_Layout.vbhtml_Layout.vbhtml It has the client side code necessary for processing the new "data-model" attribute. 它具有处理新“数据模型”属性所需的客户端代码。 The idea is to call ko.applyBindings on these specific nodes, and to only instantiate the view model once unless distinct instances of the model are explicitly requested on multiple nodes. 我们的想法是在这些特定节点上调用ko.applyBindings ,并且只在一次实例化视图模型,除非在多个节点上显式请求模型的不同实例。

Scripts/app/webui.main.js 脚本/应用/ 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);
    });
});

With this solution, I can rely on the existing ASP.NET MVC4 bundling framework (I don't need r.js) to optimize and combine javascript files, but also implement download on demand and an unobstrusive mechanism for defining the scripts and view models related to knockout bindings. 有了这个解决方案,我可以依靠现有的ASP.NET MVC4捆绑框架(我不需要r.js)来优化和组合javascript文件,还可以实现按需下载和一种用于定义脚本和视图模型的不显眼机制与敲除绑定有关。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM