Понимание шаблона VS2013 MVC 5 SPA

Я начал играть с шаблоном приложения для одной страницы для MVC 5, который поставляется с Visual Studio 2013. Я больше знаком с Knockout.js, и хотя я не был с Sammy.js, я читал и это не кажется сложным.

То, что я, похоже, не могу оборачивать, - это то, как MVC 5 SPA Template объединяет эти технологии или что команда Visual Studio имела в виду для шаблона в качестве примера; шаблон предоставляет, помимо прочего, файл home.viewModel.js, который должен служить отправной точкой, но я не могу понять, как я могу добавить больше просмотров с маршрутами Sammy.js. Если бы они предоставили второй частичный вид и модель просмотра.

Мой вопрос

Итак, короткий рассказ, мои настоящие вопросы,

  • Как мне показать частичное представление, связанное с маршрутом #users, таким образом, чтобы имитировать предоставленный home.viewModel.js, чтобы я мог перемещаться назад от #home до #users? Каким будет определение маршрута Sammy.js в users.viewModel.js?
  • Нужно ли мне делать что-нибудь особенное, чтобы включить кнопку возврата браузеров или она будет работать, как только я правильно определил свои маршруты?
  • Это он или этот шаблон выглядит наполовину испеченным примером?

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


Некоторый контекст

Предположим, что я создал частичный вид _Users.cshtml, обслуживаемый UserController, который является контроллером MVC, а не контроллером WebAPI, и что я хочу отобразить этот частичный вид с помощью Sammy.js, к концу которого я создал users.viewModel.js. Теперь...

Представленный вид Index.cshtml выглядит следующим образом:

@section SPAViews {
   @Html.Partial("_Home")
}
@section Scripts{
   @Scripts.Render("~/bundles/knockout")
   @Scripts.Render("~/bundles/app")
}

Я предполагаю, что это означает страницу "оболочка" приложения, где остальные частичные представления будут загружены, чтобы заменить содержимое части _Home partial. Проблема в том, что в home.viewModel.js маршрут Sammy инициализируется без передачи в селектор элемента, который будет удерживать содержимое, как этот

Sammy(function () {
    this.get('#home', function () {
    // more code here
}

вместо, например

Sammy("#content", function () {
    this.get('#home', function () {
    // more code here
}

Предполагаю ли я разместить части _Users вместе с _Home с самого начала так, чтобы вид Index выглядел следующим образом:

@section SPAViews {
   @Html.Partial("_Home")
   @Html.Partial("_Users")
}
@section Scripts{
   @Scripts.Render("~/bundles/knockout")
   @Scripts.Render("~/bundles/app")
}

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

Мой users.viewModel.js выглядит следующим образом:

function UsersViewModel(app, dataModel) {
    var self = this;

    Sammy(function () {
        this.get('#users', function () {
            // the following line only makes sense if _Users is not 
            // called from Index.cshtml
            //this.load(app.dataModel.shoppingCart).swap();
        });
    });

    return self;
}

app.addViewModel({
    name: "Users",
    bindingMemberName: "users",
    factory: UsersViewModel
});

Я пробовал использовать метод Sammy.js swap, но поскольку мое представление _Users является частичным, а Sammy не настроено для действия на конкретном элементе, вся страница заменяется... и кнопка возврата браузера не работает.

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

Ответы

Ответ 1

Наткнувшись на это, мне удалось применить свой собственный "взлом", чтобы исправить это.

При сравнении "старого" шаблона с новым я заметил, что Sammy.js больше встроен в шаблон. Хотя это и хорошо, оригинальная привязка к нокауту with для отображения ваших просмотров нарушена.

Чтобы применить исправление, в первую очередь необходимо понять привязку нокаутов with. В представлении home по умолчанию есть

<!-- ko with: home-->

который должен обеспечивать видимость home view только при наличии элемента home. В этом случае полное имя будет app.home

Если мы проверим имя этого члена, мы увидим, что это вычисляемый член, определенный как (app.viewmodel.js):

// Add binding member to AppViewModel (for example, app.home);
self[options.bindingMemberName] = ko.computed(function () {
    if (!dataModel.getAccessToken()) {
        //omitted for clearity
        if (fragment.access_token) {
            //omitted for clearity
        } else {
            //omitted for clearity      
        }
    }

    return self.Views[options.name];
});

Как вы можете видеть, он всегда возвращает полный инициализированный вид из коллекции Views.

Если мы сравним это со старым шаблоном, мы можем увидеть изменение здесь:

// Add binding member to AppViewModel (for example, app.home);
self[options.bindingMemberName] = ko.computed(function () {
    if (self.view() !== viewItem) {
        return null;
    }

    return new options.factory(self, dataModel);
});

Возвращает null, если текущее представление не является таргетированным viewItem. Это имеет решающее значение для привязки нокаута with.

Дальнейшая проверка обоих шаблонов показывает лучшую интеграцию с Sammy.js. Важнейшая его часть заключается в viewmodels (home.viewmodel.js):

Sammy(function () {
   this.get('#home', function () {
    });
    this.get('/', function () { this.app.runRoute('get', '#home') });
});

Так как Sammy.js обрабатывает навигацию, ранее упомянутый viewItem, инкапсулированный в app.view(), не задан. Что, опять же имеет решающее значение для привязки нокаута.

Итак, мое предложенное исправление выглядит следующим образом:

app.viewmodel.js

// Add binding member to AppViewModel (for example, app.home);
self[options.bindingMemberName] = ko.computed(function () {
    if (!dataModel.getAccessToken()) {
        //omitted for clearity
        if (fragment.access_token) {
            //omitted for clearity
        } else {
            //omitted for clearity      
        }
    }

    ///change start here
    if (self.view() !== viewItem) {
            return null;
        }

    return self.Views[options.name];
});

и в каждой пользовательской модели просмотра:

home.viewmodel.js

Sammy(function () {
    this.get('#home', function () {
         app.view(self);  //this line is added
    });
    this.get('/', function () { this.app.runRoute('get', '#home') });
});

Отказ от ответственности: Поскольку я только что получил это и работал, у меня не было времени, чтобы проанализировать любые нежелательные побочные эффекты. Кроме того, изменение базового шаблона по умолчанию не очень хорошо, поэтому лучше принимать решения.

Ответ 2

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

На самом деле, во многих случаях это именно то, что вы хотите (или, скорее, вы хотите их присутствие и контролировать их видимость.) Помимо свойства видимости на viewmodel и некоторых вспомогательных методах JS (или класса), чтобы показать/скрывать ваши представления (через ссылки на viewmodel, обычно связанные с определенным URL-адресом.)

Pseudo _Home.cshtml:

<!-- ko with: $root.home -->
<div data-bind="visible: isVisible">
    <!-- view markup/etc here -->
</div>
<!-- /ko -->

Псевдо: app.viewmanager.js

MyViewManager = function () {
    this.registerView = function(route, selector, viewmodel) {/**/};
    this.showView = function(selector, callback) {};
    this.cancelView = function(callback) {/**/};
    this.showModal = function(selector, callback) {/**/};
    this.closeModal = function(selector, callback) {/**/};
}

Они могли бы интегрироваться с History API для маршрутизации/глубокой привязки и нокаутом для отображения/скрытия элементов DOM (посредством привязки IsVisible). Вышеупомянутый "registerView" заменил бы addViewModel на стандартный эшафот, конечно. Все это, ИМО, является мусором.

Я уже несколько лет разрабатываю ООР на вершине MVC. Шаблон MVC5 SPA представляет собой интересное проявление интереса, но он имеет проблемы. Правильная глубокая привязка, инициализация и просмотр представлений - более очевидные проблемы, но с небольшим количеством смазки локтя вы можете легко кодировать то, что вам нужно.

Я также считаю раздел SPAViews бесполезным и предпочитаю использовать RenderBody для частичной доставки, что требует некоторой модификации _Layout.cshtml. В конце концов, для достаточно большого SPA вы завершите доставку почти всех ваших основных просмотров в одну страницу/просмотр в любом случае (редко бывает, что части Ajax в SPA, даже большой). И единственное значение SPAViews раздел - это размещение внутри _Layout, эффективно дублируя функцию RenderBody() (поскольку тело вашего SPA всегда будет представлять собой коллекцию невидимых представлений.)

Ответ 3

Да, это определенно сбивает с толку, и, похоже, не так много документов. Я подозреваю, что количество способов делать вещи настолько велико, что им пришлось оставить его наполовину испеченным. FWIW Я сделал простую навигацию по страницам, добавив следующее в app.viewmodel

   navigator = function () {

            self.view(viewItem);   //THIS IS ADDED



            window.location.hash = options.bindingMemberName;
        };

и в index.cshtml у меня есть это:

@section SPAViews {
<!-- ko if: app.view() === app.Views.Login -->
    @Html.Partial("_login")
<!-- /ko -->
<!-- ko if: app.view() === app.Views.MyDashboard -->
    @Html.Partial("_myDashboard")

}

Я думаю, что они, вероятно, ожидают, что вы настроитесь на то, что общее состояние представления каким-то образом связано с моделью просмотра "app" (изменение вида, видимое видимое каким-то образом перегруппировывает страницу). Помимо простого подхода выше, не уверен, как лучше всего это сделать.