MVC 4, Kendo UI, SPA with Layout, View, Router & MVVM

This will be part three of a six part series of blog posts.

  1. Modern Web Application Layered High Level Architecture with SPA, MVC, Web API, EF, Kendo UI, OData
  2. Generically Implementing the Unit of Work & Repository Pattern with Entity Framework in MVC & Simplifying Entity Graphs
  3. MVC 4, Kendo UI, SPA with Layout, View, Router & MVVM
  4. MVC 4, Web API, OData, EF, Kendo UI, Grid, Datasource (CRUD) with MVVM
  5. MVC 4, Web API, OData, EF, Kendo UI, Binding a Form to Datasource (CRUD) with MVVM
  6. Upgrading to Async with Entity Framework, MVC, OData AsyncEntitySetController, Kendo UI, Glimpse & Generic Unit of Work Repository Framework v2.0

Update: 09/09/2013 – Project has been uploaded to CodePlex: https://genericunitofworkandrepositories.codeplex.com, updated Visual 2013, Twitter Bootstrap, MVC 5, EF6, Kendo UI Bootstrap theme, project redeployed to Windows Azure Website.

Update: 06/20/2013 – Bug fix(es): Fixed View being loaded duplicate times. Enhancement(s): Added View caching. Updated blog, sample app download, and live demo

Wondering how to setup a SPA with MVC 4 project using Kendo UI Web…?! Well let’s cut to the chase and get started. We will start off with a plain old regular ASP.NET MVC 4 Internet project template and convert to a SPA, for the most part this implementation will keep in mind many of us are familiar with MVC 4 so we’ll try to adapt a lot things we are intimate with like the usage and organization of views, layouts, conventions, etc.

This will be part one five of a five part series of blog posts.

  1. Generically Implementing the Unit of Work & Repository Pattern with Entity Framework in MVC & Simplifying Entity Graphs
  2. MVC 4, Kendo UI, SPA with Layout, View, Router & MVVM
  3. MVC 4, Web API, OData, EF, Kendo UI, Grid, Datasource (CRUD) with MVVM
  4. MVC 4, Web API, OData, EF, Kendo UI, Binding a Form to Datasource (CRUD) with MVVM
  5. Upgrading to Async with Entity Framework, MVC, OData AsyncEntitySetController, Kendo UI, Glimpse & Generic Unit of Work Repository Framework v2.0

Taking a look at a high level architecture of this three part series blog: Modern Web Application Layered High Level Architecture with SPA, MVC, Web API, EF, Kendo UI.

Live demo: http://longle.azurewebsites.net.

You’ll need KendoUIWeb NuGet package.

6-18-2013 1-40-51 AM

First let’s create the SPA landing page (index.html), this will be the default page when the site is loaded up and where our Kendo UI Layout object will reside, and where we will swap our different views in.

Index.html


<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>MVC 4 SPA</title>
    <link href="/Content/kendo/2013.1.319/kendo.common.min.css" rel="stylesheet" />
    <link href="/Content/kendo/2013.1.319/kendo.metro.min.css" rel="stylesheet" />
    <link href="/Content/Site.css" rel="stylesheet" />
    <script src="/Scripts/jquery-2.0.2.min.js"></script>
    <script src="/Scripts/kendo/2013.1.319/kendo.web.min.js"></script>
    <script>
        var templateLoader = (function ($, host) {
            return {
                loadExtTemplate: function (name, path) {
                    $.ajax({
                        async: false,
                        url: path,
                        cache: false,
                        success: function (result) {
                            $("body").append(result);
                        },
                        error: function (result) {
                            alert("Error Loading View/Template -- TODO: Better Error Handling");
                        }
                    });
                }
            };
        })(jQuery, document);

        $(function () {
            var views = {};
            templateLoader.loadExtTemplate("layout", "/content/views/layout.html");
            var layout = new kendo.Layout($('#layout').html());
            layout.render($("#app"));

            var router = new kendo.Router();
            var addRoute = function (route, name, path, forceRemoteLoad) {
                forceRemoteLoad = typeof forceRemoteLoad !== "undefined" ? forceRemoteLoad : false;
                router.route(route, function () {
                    kendo.fx($("#body")).slideInRight().reverse().then(function () { // transition, slide view left
                        var isRemotelyLoaded = false;
                        if (views[name] == null || forceRemoteLoad)
                        {   // check if we have already loaded in cache, could store this in browser local storage for larger apps
                            isRemotelyLoaded = true;
                            templateLoader.loadExtTemplate(name, path); // load the view
                            views[name] = new kendo.View(('#' + name)); // add the view to cache
                        }
                        layout.showIn("#body", views[name]); // switch view
                        $(document).trigger("viewSwtichedEvent", { route: route, name: name, path: path, isRemotelyLoaded: isRemotelyLoaded }); // publish event view has been loaded (EventAggregator pattern)
                        kendo.fx($("#body")).slideInRight().play(); // transition, slide view back to the right (center)
                    });
                });
            };

            addRoute("/", "home", "/content/views/home.html");
            addRoute("/about", "about", "/content/views/about.html");
            addRoute("/contact", "contact", "/content/views/contact.html");
            addRoute("/categories", "categories", "/content/views/categories.html");
            addRoute("/customers", "customers", "/content/views/customers.html");
            addRoute("/products", "products", "/content/views/products.html");
            addRoute("/productEdit/:id", "productEdit", "/content/views/productEdit.html");

            router.start();
        });
    </script>
</head>
<body>
    <div id="app"></div>
</body>
</html>

I've deliberately left much of the JavaScript code on each of the pages in the interest of easily understanding what's happening on each of the pages or views, obviously you can extract them into their own .js files however you see fit.

TemplateLoader Helper


        var templateLoader = (function ($, host) {
            return {
                loadExtTemplate: function (name, path) {
                    $.ajax({
                        async: false,
                        url: path,
                        cache: false,
                        success: function (result) {
                            $("body").append(result);
                        },
                        error: function (result) {
                            alert("Error Loading View/Template -- TODO: Better Error Handling");
                        }
                    });
                }
            };
        })(jQuery, document);

The templateLoader helper method was used from here, and has been slightly modified so that we can load Kendo Views remotely from the server in our SPA when navigating through the application.

  1. It's taking a path to a file
  2. Grabbing the contents with jQuery Ajax
  3. Appending the contents to the body of our document
  4. Then notifying the application that the template has loaded

Kendo UI Web Router


        $(function () {
            var views = {};
            templateLoader.loadExtTemplate("layout", "/content/views/layout.html");
            var layout = new kendo.Layout($('#layout').html());
            layout.render($("#app"));

            var router = new kendo.Router();
            var addRoute = function (route, name, path, forceRemoteLoad) {
                forceRemoteLoad = typeof forceRemoteLoad !== "undefined" ? forceRemoteLoad : false;
                router.route(route, function () {
                    kendo.fx($("#body")).slideInRight().reverse().then(function () { // transition, slide view left
                        var isRemotelyLoaded = false;
                        if (views[name] == null || forceRemoteLoad)
                        {   // check if we have already loaded in cache, could store this in browser local storage for larger apps
                            isRemotelyLoaded = true;
                            templateLoader.loadExtTemplate(name, path); // load the view
                            views[name] = new kendo.View(('#' + name)); // add the view to cache
                        }
                        layout.showIn("#body", views[name]); // switch view
                        $(document).trigger("viewSwtichedEvent", { route: route, name: name, path: path, isRemotelyLoaded: isRemotelyLoaded }); // publish event view has been loaded (EventAggregator pattern)
                        kendo.fx($("#body")).slideInRight().play(); // transition, slide view back to the right (center)
                    });
                });
            };

            addRoute("/", "home", "/content/views/home.html");
            addRoute("/about", "about", "/content/views/about.html");
            addRoute("/contact", "contact", "/content/views/contact.html");
            addRoute("/categories", "categories", "/content/views/categories.html");
            addRoute("/customers", "customers", "/content/views/customers.html");
            addRoute("/products", "products", "/content/views/products.html");
            addRoute("/productEdit/:id", "productEdit", "/content/views/productEdit.html");

            router.start();
        });

We are also putting the Kendo UI Router to good use so that we can navigate between the different views in our SPA while syncing with the browser history so even though we are never refreshing, and our app never does any post backs, the user is still able to navigate back and forward using their browser buttons.

We can see that we are also using the Kendo FX API, this is to polish the UX in our SPA by adding slide transitions when swapping views. We are simply remotely loading the requested View (if it hasn't already been loaded for the first time) from the server, sliding the #body div to the left, swapping the View with the requested one, sliding the #body div back it's place (you can click on the live demo link to see this in action). If the view has already been loaded we cache the view in a dictionary, the next time this View is navigated to, we will check cache it it already exist then will load it from there, if not, we'll load it from the server. Yes, we could have done the transitions this with a jQuery plug-in or some other tool or framework, however the intent was here was to accomplish this (along with Part 2 and 3 of this blog series) with the use of only framework, Kendo UI Web.

addRoute Helper


            var addRoute = function (route, name, path, forceRemoteLoad) {
                forceRemoteLoad = typeof forceRemoteLoad !== "undefined" ? forceRemoteLoad : false;
                router.route(route, function () {
                    kendo.fx($("#body")).slideInRight().reverse().then(function () { // transition, slide view left
                        var isRemotelyLoaded = false;
                        if (views[name] == null || forceRemoteLoad)
                        {   // check if we have already loaded in cache, could store this in browser local storage for larger apps
                            isRemotelyLoaded = true;
                            templateLoader.loadExtTemplate(name, path); // load the view
                            views[name] = new kendo.View(('#' + name)); // add the view to cache
                        }
                        layout.showIn("#body", views[name]); // switch view
                        $(document).trigger("viewSwtichedEvent", { route: route, name: name, path: path, isRemotelyLoaded: isRemotelyLoaded }); // publish event view has been loaded (EventAggregator pattern)
                        kendo.fx($("#body")).slideInRight().play(); // transition, slide view back to the right (center)
                    });
                });
            };

The addRoute is simply a helper method to register our routes and the with the Router along with the delegate we want invoke when navigating between our Views. Our delegate simply handles the animation transitions, switching out the Views and caching of our Views.

Now our next step is just converting the regular ASP.NET views to pure html which is pretty straight forward. For now, since MVC ignores routes for anything under /Content folder, we'll just put our views in Spa/Content/views/, you can place them anywhere you see fit, and just have the MVC runtime ignore that path using the routes.IgnoreRoute([path-goes-here]) method in your RoutConfig.cs.

6-17-2013 10-41-19 PM

Spa/Content/views/layout.html


<script type="text/x-kendo-template" id="layout" highlight="31">
    <header>
        <div class="content-wrapper">
            <div class="float-left">
                <p class="site-title"><a href="/">CBRE Architecture & Design Team</a></p>
            </div>
            <div class="float-right">
                <section id="login">
                    <ul>
                        <li><a href="/Account/Register" id="registerLink">Register</a></li>
                        <li><a href="/Account/Login" id="loginLink">Log in</a></li>
                    </ul>
                </section>
                <nav>
                    <ul id="menu">
                        <li><a href="#/" >Home</a></li>
                        <li><a href="#/about" >About</a></li>
                        <li><a href="#/contact">Contact</a></li>
                    </ul>
                </nav>
                <nav>
                    <ul id="menu">
                        <li><a href="#/customers" >Customers</a></li>
                        <li><a href="#/categories" >Categories</a></li>
                        <li><a href="#/products" >Products</a></li>
                    </ul>
                </nav>
            </div>
        </div>
    </header>
    <div id="body">
    </div>
    <footer>
        <div class="content-wrapper">
            <div class="float-left">
                <p>&copy; 2013 - My ASP.NET MVC Application</p>
            </div>
        </div>
    </footer>
</script>

The layout.html View is loaded first, this is pretty much the layout of the application, all the Views will be loaded into the div:

<div d="body">

Spa/Content/views/home.html


<script type="text/x-kendo-template" id="home">
<section class="featured">
    <div class="content-wrapper">
        <hgroup class="title">
            <h1>Home Page.</h1>
            <h2>Modify this template to jump-start your ASP.NET MVC application.</h2>
        </hgroup>
        <p>
            To learn more about ASP.NET MVC visit
                <a href="http://asp.net/mvc" title="ASP.NET MVC Website">http://asp.net/mvc</a>.
                The page features <mark>videos, tutorials, and samples</mark> to help you get the most from ASP.NET MVC.
                If you have any questions about ASP.NET MVC visit
                <a href="http://forums.asp.net/1146.aspx/1?MVC" title="ASP.NET MVC Forum">our forums</a>.
        </p>
    </div>
</section>

<section class="content-wrapper main-content clear-fix">

    <h3>We suggest the following:</h3>
    <ol class="round">
        <li class="one">
            <h5>Getting Started</h5>
            ASP.NET MVC gives you a powerful, patterns-based way to build dynamic websites that
        enables a clean separation of concerns and that gives you full control over markup
        for enjoyable, agile development. ASP.NET MVC includes many features that enable
        fast, TDD-friendly development for creating sophisticated applications that use
        the latest web standards.
        <a href="http://go.microsoft.com/fwlink/?LinkId=245151">Learn more…</a>
        </li>

        <li class="two">
            <h5>Add NuGet packages and jump-start your coding</h5>
            NuGet makes it easy to install and update free libraries and tools.
        <a href="http://go.microsoft.com/fwlink/?LinkId=245153">Learn more…</a>
        </li>

        <li class="three">
            <h5>Find Web Hosting</h5>
            You can easily find a web hosting company that offers the right mix of features
        and price for your applications.
        <a href="http://go.microsoft.com/fwlink/?LinkId=245157">Learn more…</a>
        </li>
    </ol>

</section>
</script>
<script>
    var homeModel = new kendo.data.ObservableObject({});
</script>

Spa/Content/views/contact.html


<script type="text/x-kendo-template" id="contact">
<section class="content-wrapper main-content clear-fix">
    <hgroup class="title">
        <h1>Contact.</h1>
        <h2>Your contact page.</h2>
    </hgroup>

    <section class="contact">
        <header>
            <h3>Phone</h3>
        </header>
        <p>
            <span class="label">Main:</span>
            <span>425.555.0100</span>
        </p>
        <p>
            <span class="label">After Hours:</span>
            <span>425.555.0199</span>
        </p>
    </section>

    <section class="contact">
        <header>
            <h3>Email</h3>
        </header>
        <p>
            <span class="label">Support:</span>
            <span><a href="mailto:Support@example.com">Support@example.com</a></span>
        </p>
        <p>
            <span class="label">Marketing:</span>
            <span><a href="mailto:Marketing@example.com">Marketing@example.com</a></span>
        </p>
        <p>
            <span class="label">General:</span>
            <span><a href="mailto:General@example.com">General@example.com</a></span>
        </p>
    </section>

    <section class="contact">
        <header>
            <h3>Address</h3>
        </header>
        <p>
            One Microsoft Way<br />
            Redmond, WA 98052-6399
        </p>
    </section>
</section>
</script>
<script>
    var contactModel = new kendo.data.ObservableObject({});
</script>

Spa/Content/views/about.html


<script type="text/x-kendo-template" id="about">
<section class="content-wrapper main-content clear-fix">
    <hgroup class="title">
        <h1>About.</h1>
        <h2>Your app description page.</h2>
    </hgroup>

    <article>
        <p>
            Use this area to provide additional information.
        </p>

        <p>
            Use this area to provide additional information.
        </p>

        <p>
            Use this area to provide additional information.
        </p>
    </article>

    <aside>
        <h3>Aside Title</h3>
        <p>
            Use this area to provide additional information.
        </p>
        <ul>
            <li><a href="/">Home</a></li>
            <li><a href="/Home/About">About</a></li>
            <li><a href="/Home/Contact">Contact</a></li>
        </ul>
    </aside>
</section>
</script>
<script>
    var aboutModel = new kendo.data.ObservableObject({});
</script>

We see that each and every View is responsible for the Model that it will use and bind to, so every view will need to have a Model. The convention we are using here is name of View and viewModel, e.g. so if your View is name Product then it should have a model named ProductModel. The code in the SPA/Index.html will handle binding them together, we just need to follow the convention and everything else is handled.

For live demo: http://longle.azurewebsites.net

Download sample application: https://genericunitofworkandrepositories.codeplex.com

Stay tune for Part 2 and 3...

Part 2 – MVC 4, Web API, OData, EF, Kendo UI, Grid, Datasource with MVVM
Part 3 – MVC 4, Web API, OData, EF, Kendo UI, Binding a Form to Datasource (CRUD) with MVVM