简体   繁体   English

了解VS2013 MVC 5 SPA模板

[英]Understanding the VS2013 MVC 5 SPA template

I've started playing with the Single Page Application template for MVC 5 that comes with Visual Studio 2013. I'm more than familiar with Knockout.js , and although I wasn't with Sammy.js I've been reading up on it and it doesn't seem all that complicated. 我已经开始使用Visual Studio 2013附带的MVC 5单页面应用程序模板。我对Knockout.js非常熟悉,虽然我没有和Sammy.js在一起, Sammy.js我一直在阅读它它看起来并不那么复杂。

What I can't seem to wrap my head around is how the MVC 5 SPA Template combines these technologies, or what the Visual Studio team had in mind for the template as an example; 我似乎无法理解的是MVC 5 SPA模板如何结合这些技术,或者Visual Studio团队为模板所考虑的内容作为示例; the template provides, amongst other things, a home.viewModel.js file that's supposed to serve as a starting point, but I can't seem to understand how I can add more views with Sammy.js routes. 该模板提供了一个home.viewModel.js文件,该文件应该作为起点,但我似乎无法理解如何使用Sammy.js路由添加更多视图。 If only they had provided a second partial view and viewmodel. 如果只有他们提供了第二个局部视图和视图模型。

My Question 我的问题

So, short story long, my real questions are, 所以,短篇小说,我真正的问题是,

  1. how do I go about displaying a partial view linked to the route #users in a way that mimics the provided home.viewmodel.js , so that I can navigate back a forth from #home to #users ? 如何以模仿提供的home.viewmodel.js的方式显示链接到路径#users的局部视图,以便我可以从#home向后导航到#users What would the Sammy.js route definition look like in users.viewModel.js ? Sammy.js路由定义在users.viewModel.jsusers.viewModel.js
  2. Do I need to do anything special to enable the browsers back button or will it just work as soon as I have defined my routes properly? 我是否需要做一些特殊的事情来启用浏览器后退按钮,或者只要我正确定义了我的路由它就能正常工作?
  3. It it me or does this template feel as a half-baked example? 它是我还是这个模板感觉是一个半生不熟的例子?

The following code is just for extra reference/context, but it probably not necessary in order for the question to be answered. 以下代码仅用于额外的参考/上下文,但为了回答问题,可能没有必要。


Some context 一些背景

Let's assume I have created a partial view, _Users.cshtml , served by a UserController , which is an MVC controller and not a WebAPI controller, and that I want to display that partial view by means of a Sammy.js route, to which end I've created a users.viewModel.js . 假设我创建了一个部分视图_Users.cshtml ,由UserController ,它是一个MVC控制器,而不是一个WebAPI控制器,我希望通过Sammy.js路由显示该部分视图,为此我创建了一个users.viewModel.js Now... 现在...

The provided Index.cshtml view looks like this: 提供的Index.cshtml视图如下所示:

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

Which I presume is meant as the application "shell" page, where the rest of partial views will be loaded to substitute the contents of the _Home partial. 我认为这意味着应用程序“shell”页面,其中部分视图的其余部分将被加载以替换_Home部分的内容。 The problem is that on the home.viewmodel.js the Sammy route is initialized without passing in a selector for the element that will hold the content, like this 问题是在home.viewmodel.js上初始化Sammy路由而不传入将保存内容的元素的选择器,就像这样

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

instead of, for example 而不是,例如

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

Am I supposed to place the _Users partial alongside _Home from the very beginning so that the Index view looks like this? 我应该放置_Users一起局部_Home从一开始就使得Index视图看起来像这样?

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

This will, of course, display both views at the same time, which is not what we want. 当然,这将同时显示两个视图,这不是我们想要的。

My users.viewmodel.js looks like this: 我的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
});

I've tried using the Sammy.js swap method, but since my _Users view is a partial and Sammy is not set up to act on a specific element the whole page is replaced... and the browser's back button doesn't seem to work. 我已经尝试过使用Sammy.js swap方法,但由于我的_Users视图是部分的,并且Sammy没有设置为对特定元素进行操作,因此整个页面被替换...并且浏览器的后退按钮似乎没有工作。

Sorry for the massive amount of text, and if this is a very trivial question. 对于大量的文本感到抱歉,如果这是一个非常微不足道的问题。 It bothers me that I can't seem to figure it out on my own, even after going through the docs. 令我困扰的是,即使在完成文档之后,我似乎无法自己解决这个问题。

Stumbling upon this myself I managed to apply my own little 'hack' to fix this. 我自己磕磕绊绊地设法应用自己的小“黑客”来解决这个问题。

When comparing the 'older' template with the new one, I noticed Sammy.js is more embedded in the template. 在比较“旧”模板和新模板时,我注意到Sammy.js更多地嵌入模板中。 Although this is a good thing, the original out-of-the box's knockout with binding to show your views is broken. 虽然这是一件好事,但原始开箱即with装订结合显示你的观点已被打破。

To apply the fix, first of all it is required to understand the knockouts with binding. 要应用此修复程序,首先需要了解with绑定的敲除。 In the default home view there is the 在默认的home视图中有

<!-- ko with: home-->

statement which should ensure visibility of the home view only when a member home is present. 只有在成员home存在时才应确保home view可见性的声明。 In this case the full name would be app.home 在这种情况下,全名是app.home

If we inspect this members name, we see it is a computed member defined such as (app.viewmodel.js): 如果我们检查这个成员名称,我们会看到它是一个定义的计算成员,例如(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];
});

As you can see, it always returns a full initialized view from the Views collection. 如您所见,它始终从Views集合返回完整的初始化视图。

If we compare this with the older template we can see a change here: 如果我们将其与旧模板进行比较,我们可以在此处看到更改:

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

Which returns null if the current view is not the targeted viewItem . 如果当前视图不是目标viewItem则返回null。 This is crucial for the knockout's with binding. 这对于with约束力的淘汰赛至关重要。

Further inspection of both templates shows the better integration with sammy.js . 对这两个模板的进一步检查显示与sammy.js的更好集成。 A crucial part of it lies in the viewmodels (home.viewmodel.js): 它的一个关键部分在于viewmodels(home.viewmodel.js):

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

Since sammy.js is handling the navigation, the earlier mentioned viewItem , encapsulated in app.view() is not set. 由于sammy.js正在处理导航,因此未设置封装在app.view()的前面提到的viewItem Which, again is crucial for the knockout binding. 对于淘汰赛绑定来说,这也是至关重要的。

So, my proposed fix is as follows: 所以,我提出的修复方法如下:

app.viewmodel.js 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];
});

and in every custom viewmodel: 并在每个自定义视图模型中:

home.viewmodel.js home.viewmodel.js

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

Disclaimer: Since I just got this up and running, I didn't have the time to analyze any unwanted side affects. 免责声明:由于我刚刚启动并运行,我没有时间分析任何不必要的副作用。 Besides, altering the default template's core doesn't feel very satisfiable, so better solutions are welcome. 此外,改变默认模板的核心并不是很令人满意,因此欢迎更好的解决方案。

This will, of course, display both views at the same time, which is not what we want. 当然,这将同时显示两个视图,这不是我们想要的。

Actually, in many cases this is exactly what you want (or, rather, you want their presence and to control their visibility.) In addition to a visibility property on the viewmodel and some JS helper methods (or class) to show/hide your views (via the viewmodel references, typically associated with a particular url as well.) 实际上,在许多情况下,这正是您想要的(或者,您希望他们的存在并控制其可见性。)除了viewmodel上的visibility属性和一些JS帮助器方法(或类)来显示/隐藏您的视图(通过视图模型引用,通常也与特定URL相关联。)

Pseudo _Home.cshtml : _Home.cshtml

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

Pseudo: app.viewmanager.js 伪: 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) {/**/};
}

These would handle integrating with History API for routing/deep-linking, and knockout to show/hide DOM elements (via the IsVisible binding). 这些将处理与History API集成以进行路由/深度链接,以及用于显示/隐藏DOM元素的淘汰(通过IsVisible绑定)。 The above 'registerView' would replace addViewModel from the default scaffold, of course. 当然,上面的'registerView'将取代默认支架中的addViewModel All of that, IMO, is trash. 所有这些,IMO,都是垃圾。

I've been developing SPAs on top of the MVC framework for several years. 多年来,我一直在MVC框架之上开发SPA。 The MVC5 SPA template is a nice show of interest, but it has problems. MVC5 SPA模板很受欢迎,但它有问题。 Proper deep-linking, viewmodel initialization and view management are the more obvious issues, but with a bit of elbow grease you can code what you need easily. 正确的深层链接,视图模型初始化和视图管理是更明显的问题,但有一点肘部油脂,您可以轻松编写您需要的代码。

I also find the SPAViews section useless, and prefer to use RenderBody for partial delivery, which requires some modification of _Layout.cshtml . 我还发现SPAViews部分没用,并且更喜欢使用RenderBody进行部分传递,这需要对_Layout.cshtml一些修改。 After all, for a large enough SPA you will wind up delivering almost all of your primary views in a single Page/View anyway (it's rare to see Ajax partials in an SPA, even a large one.) And the only value SPAViews section provides is placement within the _Layout, effectively duplicating the function of RenderBody() (since the body of your SPA is always going to be a collection of invisible views.) 毕竟,对于一个足够大的SPA,你最终会在一个页面/视图中提供几乎所有的主要视图(很少见到SPA中的Ajax部分,甚至是大的。)并且SPAViews部分提供的唯一值是_Layout中的位置,有效地复制了RenderBody()的功能(因为SPA的主体总是一组不可见的视图。)

Yes it's definitely confusing and there doesn't seem to be much in the way of docs. 是的,它确实令人困惑,而且似乎没有太多的文档。 I suspect the number of ways of doing things is so great they had to leave it half baked. 我怀疑做事方式的数量是如此之大,以至于他们不得不将其烘焙一半。 FWIW I have done by simple page nav by adding the following to the app.viewmodel FWIW我通过简单的页面导航完成,将以下内容添加到app.viewmodel

   navigator = function () {

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



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

and in the index.cshtml I have this: 在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")

} }

I think the way they probably expect you to set things up is by having the overall view state somehow bound to 'app' viewmodel (change the view, the view observable somehow rearranges the page). 我认为他们可能期望你设置的方式是让整体视图状态以某种方式绑定到'app'viewmodel(更改视图,视图observable以某种方式重新排列页面)。 Beyond the simple approach above, not sure how best that should be done. 除了上面的简单方法,还不确定应该如何做到最好。

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

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