code4coffee
code4coffee

Reputation: 677

ASP.NET Route config for Backbone Routes with PushState

I have run into an issue recently where we have been told to remove the hash symbols from our Backbone applications. This presents two problems: (a) the ASP.NET routes need to handle any remotely linked URL (currently this is no problem with the hash symbols) so that we're not hitting a 404 error and (b) the proper route needs to be preserved and passed on to the client side (Backbone) application. We're currently using ASP.NET MVC5 and Web API 2 for our backend.

The setup

For an example (and test project), I've created a test project with Backbone - a simple C# ASP.NET MVC5 Web Application. It is pretty simple (here is a copy of the index.cshtml file, please ignore what is commented out as they'll be explained next):

<script type="text/javascript">
    $(document).ready(function(event) {
        Backbone.history.start({
            //pushState: true,
            //root: "/Home/Index/"
        });
        var Route = Backbone.Router.extend({
            routes: {
                "test/:id": function (event) {
                    $(".row").html("Hello, " + event);
                },
                "help": function () {
                    alert("help!");
                }
            }
        });
        var appRouter = new Route();
        //appRouter.navigate("/test/sometext", { trigger: true });
        //appRouter.navigate("/help", { trigger: true });
    });
</script>

<div class="jumbotron">
    <h3>Backbone PushState Test</h3>
</div>

<div class="row"></div>

Now, without pushState enabled I have no issue remote linking to this route, ie http://localhost/Home/Index#test/sometext

The result of which is that the div with a class of .row is now "Hello, sometext".

The problem

Enabling pushState will allow us to replace that pesky # in the URL with a /, ie: http://localhost/Home/Index/test/sometext. We can use the Backbone method of router.navigate("url", true); (as well as other methods) to use adjust the URL manually. However, this does not solve the problem of remote linking. So, when trying to access http://localhost/Home/Index/test/sample you just end up with the typical 404.0 error served by IIS. so, I assume that it is handled in in the RouteConfig.cs file - inside, I add a "CatchAll" route:

routes.MapRoute(
    name: "CatchAll",
    url: "{*clientRoute}",
   defaults: new { controller = "Home", action = "Index" }
);

I also uncomment out the pushState and root attributes in the Backbone.history.start(); method:

        Backbone.history.start({
            pushState: true,
            root: "/Home/Index/"
        });
        var Route = Backbone.Router.extend({
            routes: {
                "test/:id": function (event) {
                    $(".row").html("Hello, " + event);
                },
                "help": function () {
                    alert("help!");
                }
            }
        });
        var appRouter = new Route();
        //appRouter.navigate("/test/sometext", { trigger: true });
        //appRouter.navigate("/help", { trigger: true });

This allows me to at least let get past the 404.0 page when linking to these routes - which is good. However, none of the routes actually "trigger" when I head to them. After attempting to debug them in Chrome, Firefox, and IE11 I notice that none of the events fire. However, if I manually navigate to them using appRouter.navigate("/help", { trigger: true }); the routes are caught and events fired.

I'm at a loss at this point as to where I should start troubleshooting next. I've placed my Javascript inside of the $(document).ready() event as well as the window.onload event also (as well as not inside of an event); none of these correct the issue. Can anyone offer advice on where to look next?

Upvotes: 1

Views: 796

Answers (2)

Curtis Yallop
Curtis Yallop

Reputation: 7319

You simply have to move Backbone.history.start after the "new Route" line.

var Route = Backbone.Router.extend({
    routes: {
        "test/:id": function (event) {
            $(".row").html("Hello, " + event);
        },
        "help": function () {
            alert("help!");
        }
    }
});
var appRouter = new Route();
Backbone.history.start({
    pushState: true,
    root: "/Home/Index/"
});

Make sure you go to ".../Home/Index/help". If it doesn't work, try temporarily removing the root and go to ".../help" to see if the root is the problem.

If you still have troubles, set a js breakpoint in Backbone.History.loadUrl on the "return" line. It is called from the final line of History.start to execute the current browser url on page load. "this.matchRoot()" must pass then, "fragment" is matched against each "route" or regexp string in "this.handlers". You can see why or why not the browser url matches the route regexps.

To set to the js breakpoint, press F12 in the browser to open the dev console, press Ctrl-O or Ctrl-P to open a js file, then type the name of the backbone js file. Then search for "loadUrl:". You can also search for "Router =" to find the start of the router class definition (same as for "View =" and "Model =" to find the backbone view/model implementation code). I find it quite useful to look at the backbone code when I have a question like this. It is surprisingly readable and what better place to get answers?

If your js files happen to be minified/compressed, preferably turn this off. Alternately you can try the browser unminify option. In Chrome this is the "{}" button or "pretty print". Then the js code is not all on 1 line and you can set breakpoints. But the function and variable names may still be mangled.

Upvotes: 1

code4coffee
code4coffee

Reputation: 677

I have solved my own problem using what feels to be "hackish", via the following. If anyone can submit a better response it would be appreciated!

My Solution: I globally override the default Backbone.Router.intilaize method (it is empty) with the following:

$(document).ready(function (event) {
    var _root = "/Home/Index/";
    _.extend(Backbone.Router.prototype, {

        initialize: function () {
            /* check for route & navigate to it */
            var pathName = window.location.pathname;
            var route = pathName.split(_root)[1];
            if (route != undefined && route != "") {
                route = "/" + route;
                this.navigate("", { trigger: false });
                this.navigate(route, { trigger: true });
            }
        }

    });
});

Upvotes: 0

Related Questions