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

时间:2014-01-17 12:52:09

标签: javascript asp.net-mvc vb.net asp.net-mvc-4 knockout.js

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

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

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

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

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

3 个答案:

答案 0 :(得分: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,但是在用于使用ajax加载部分视图的函数中也有success个回调,并且你可以在这个回调中完全相同。您可以为jQuery.Ajax成功注册一个全局处理程序,因此您的脚本将应用于所有加载了ajax的部分。

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

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

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

  • 主页面:包含“解析器脚本”:此解析器脚本负责:

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

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

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

查看脚本如何动态下载的好地方是: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-属性找到其他参数和选项。

这看起来很难实现,但是一旦你设置了一个工作骨架,你就可以轻松完成并改进它。

答案 1 :(得分:1)

以下是我撰写视图模型和视图的方法:

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

答案 2 :(得分:1)

现有的答案还不够详细,所以请允许我提供详细的答案和代码。我大部分都遵循了JotaBe的答案的建议,而这正是如此。

首先,我设计了一个方案,用于我将使用的自定义(&#34; data&#34;)属性,并创建一个帮助函数,以便以一种有助于我与ASP.Net捆绑兼容的方式应用它。该属性需要提供必要的信息,以便在打开捆绑优化(BundleTable.EnableOptimizations = True)时下载单个捆绑包文件,否则需要多个独立文件。您可以在以下代码的注释中查看我为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

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

查看/库存/ 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中引入。它具有处理新的&#34;数据模型所需的客户端代码。属性。我们的想法是在这些特定节点上调用ko.applyBindings,并且只在实例化视图模型一次,除非在多个节点上显式请求模型的不同实例。

脚本/应用/ 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文件,还可以实现按需下载和一种不显眼的定义机制与敲除绑定相关的脚本和视图模型。