Включение script для просмотра или частичного просмотра ASP.NET MVC4

Я рассмотрел ряд вопросов, подобных Как добавить script в частичном представлении в MVC4? и MVC4 частичное представление javascript, связанное с выпуском, и я все еще пытаюсь понять архитектуру ASP.NET MVC, когда речь заходит о конкретном представлении script. Кажется, ответ другим, которые пытались включить script в свои частичные представления MVC4, - это поставить script на более высокий уровень. Но некоторые script не могут быть перемещены на более высокий уровень, где он будет работать более глобально. Например, я не хочу запускать script, который применяет привязки данных knockout.js для модели представления, элементы управления которой не загружаются. И я не хочу запускать целую кучу script для целой кучи представлений, которые не активны при каждом загрузке страницы.

Итак, я начал использовать блоки @Section Script для просмотра в своих представлениях .vbhtml, чтобы включить script, специфичный для представления. Однако, как отмечают другие, это не работает в частичном представлении. Я прототипирую нашу архитектуру, чтобы понять, что мы можем и чего не можем сделать здесь. Я хотел бы думать, что в некоторых случаях я мог бы использовать представление как частичное представление и наоборот. Но когда вы пытаетесь использовать как частичный вид, блок @Section Script не отображает. Мне удалось получить всю мою модель представления script, определенную глобально, таким образом, что мне нужно только запустить одну строку кода для создания и привязки модели представления, но мне по-прежнему нужно, чтобы одна строка кода запускалась только тогда, когда определенное представление активен. Где я могу соответствующим образом добавить эту строку кода в частичное представление?

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

Я иду по правильному пути здесь? Это правильный способ архивирования приложения MVC?

Изменить. Этот вопрос очень тесно связан с моей проблемой и включает в себя значительную часть моего ответа: Можете ли вы назвать ko.applyBindings для привязки частичного смотреть?

Ответ 1

Существующие ответы были недостаточно подробными, поэтому позвольте мне предоставить подробный ответ с кодом. Я в основном следовал за предложением ответа JotaBe, и вот как именно.

Сначала я разработал схему для того, какой пользовательский ( "data" ) атрибут я использовал бы и создал вспомогательную функцию, чтобы применить ее таким образом, чтобы помочь мне быть совместимым с комплектом 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 (") 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

Затем я могу применить этот атрибут на node, как это, чтобы указать, как он хочет, чтобы привязки нокаута применялись к себе и его потомкам и какие скрипты нужны, прежде чем это делать. Обратите внимание, как мое намерение состоит в том, чтобы иметь возможность ссылаться на один и тот же набор и модель из нескольких узлов без дублирования загрузки или наличия дубликатов экземпляров модели, если я специально не запрашиваю отдельные экземпляры модели с помощью forceNew. Вероятно, было бы лучше добавить контейнер для размещения этого атрибута в одном месте, но я хочу показать, что это не нужно.

Views/Инвентарь/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 на этих конкретных узлах и только создать экземпляр модели представления один раз, если отдельные экземпляры модели явно не запрашиваются на нескольких узлах.

Сценарии/приложение/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 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 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 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, а также осуществлять загрузку по требованию и ненавязчивый механизм для определения сценариев и просматривать модели, связанные с привязками к нокауту.

Ответ 2

Это лучшее, что вы можете сделать,, но могут быть проблемы:

  • Что делать, если ваш частичный вид кэшируется?
  • Что делать, если вы обрабатываете частичный вид с помощью Ajax?

Итак, я также рекомендую не делать этого, используя этот хакерский трюк. (Ну, решение Дарина Димитрова здорово, но с его помощью это не очень хорошая идея).

Лучшее решение состоит в том, чтобы все скрипты были доступны, когда частичное обновление было красным:

  • Загрузка их на странице contianing
  • загружать их динамически (что сложнее сделать)

Если вы это сделаете, вы можете запустить сценарии, когда они понадобятся. Но тогда, как вы запускаете только нужные сценарии в желаемых частях ваших партикулов? Более простой способ - пометить их пользовательскими атрибутами data-. Затем вы можете "разобрать" страницу, искать свои настраиваемые атрибуты data- и запускать скрипты, которые применяются: этот ненавязчивый javascript.

Например, вы можете включить script, который "анализирует" страницу в jQuery $(document).ready (когда все страницы и все сценарии завершили загрузку). Этот script может искать элементы с пользовательскими атрибутами data- ($('[data-my-custom-attr]').each( MyCustomSccript(this));

Вы также можете учесть, что атрибуты data- могут использоваться для настройки вашего script, то есть вы можете использовать атрибут, чтобы указать, что должен выполняться какой-то script, и дополнительные атрибуты для настройки способа запускается script.

А как насчет частичных представлений, загруженных с помощью ajax? Нет проблем. Я сказал, что вы можете использовать $(document).ready, но вы также имеете обратные вызовы success в функциях, используемых для загрузки частичных представлений с помощью ajax, и вы можете сделать то же самое в этих обратных вызовах. Вы можете зарегистрировать глобальный обработчик для успеха jQuery.Ajax, поэтому ваши скрипты будут применены ко всем вашим загруженным частицам ajax.

И вы даже можете использовать более мощные методы, такие как динамическая загрузка скриптов, необходимых для ваших партитур, как это требуется для атрибутов.

Обычно проблема заключается в том, что мы думаем, что JavaScript должен быть предоставлен с сервера, но правда в том, что JavaScript живет в браузере, и браузер должен иметь больше контроля над ним.

Описание архитектуры с динамической загрузкой скриптов:

  • главная страница: включить "парсер script": этот анализатор script отвечает за:

    • разбор страницы (событие готовности документа) или частично загруженное частичное (ajax) событие ajax
    • загрузка и сохранение необходимых сценариев в одноэлементной странице (требуемые определяются атрибутами `data- ')
    • запуск сценариев (которые хранятся в singleton)
  • обертоны

    • у них есть атрибуты data- на элементах DOM, так что синтаксический анализатор знает, какие скрипты необходимы
    • у них есть дополнительные атрибуты data- для передачи дополнительных данных в скрипты

Очевидно, очень важно следовать хорошему соглашению, чтобы назвать скрипты и атрибуты data-, чтобы код был проще в использовании и отладке.

Хорошим местом для просмотра динамических загрузок сценариев является: JavaScript по требованию

Существует много решений. Другой вариант: Как я могу динамически загружать и запускать javascript script с консоли javascript?

Ваш script должен присоединяться к синглтону, точно так же, как вы это делаете, когда вы определяете плагин 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

После правильных соглашений абсолютно необходимо, чтобы синтаксический анализатор мог запустить необходимый script.

Имя запускаемой функции может быть другим атрибутом data- или всегда быть таким же, как init. Поскольку эта функция может присоединиться к элементу DOM, он может найти там другие параметры и параметры, используя другие атрибуты data-.

Это может показаться сложным для реализации, но как только вы настроите рабочий скелет, вы можете легко его завершить и улучшить.

Ответ 3

Вот как я составлял модели и представления вида:

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

В моем представлении у меня есть раздел Script в моем основном шаблоне. Поэтому мой взгляд выглядит следующим образом:

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

На самом деле, чем больше я пишу эти приложения MVVM, тем более склонен я использую ajax для загрузки данных и не передаю данные модели в функцию init. Это позволяет мне переместить вызов init в factory. Итак, вы получите что-то вроде:

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

Что уменьшает мой вид Script до простого тега Script:

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

Наконец, мне нравится создавать шаблоны Script для компонентов vm внутри частичных представлений:

Частичное представление в ~/Просмотры/Общие/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>

Несколько вещей происходит здесь. Сначала импортируется связанный Script. Это гарантирует, что при визуализации частичной части будет отображаться необходимая модель представления factory Script. Это позволяет мастер-представлению оставаться неосведомленным к потребностям Script подкомпонента (из которого он может иметь несколько). Кроме того, определяя шаблоны в частичном, а не в файле 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-архитектуры в течение последних нескольких лет, и это действительно улучшило мои циклы разработки. Надеюсь, это будет полезно и вам. Я был бы рад ответить на любые вопросы.