简体   繁体   中英

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.

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; 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. 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 ? What would the Sammy.js route definition look like in users.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 . Now...

The provided Index.cshtml view looks like this:

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

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?

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

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.

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. Although this is a good thing, the original out-of-the box's knockout with binding to show your views is broken.

To apply the fix, first of all it is required to understand the knockouts with binding. In the default home view there is the

<!-- ko with: home-->

statement which should ensure visibility of the home view only when a member home is present. In this case the full name would be app.home

If we inspect this members name, we see it is a computed member defined such as (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.

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 . This is crucial for the knockout's with binding.

Further inspection of both templates shows the better integration with sammy.js . A crucial part of it lies in the 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. Which, again is crucial for the knockout binding.

So, my proposed fix is as follows:

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

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.)

Pseudo _Home.cshtml :

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

Pseudo: 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). The above 'registerView' would replace addViewModel from the default scaffold, of course. All of that, IMO, is trash.

I've been developing SPAs on top of the MVC framework for several years. The MVC5 SPA template is a nice show of interest, but it has problems. 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 . 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.)

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

   navigator = function () {

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



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

and in the index.cshtml I have this:

@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). Beyond the simple approach above, not sure how best that should be done.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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