Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Sketcher : Two Of n

0.00/5 (No votes)
14 Aug 2014 2  
Angular.Js / Azure / ASP MVC / SignalR / Bootstrap demo app

Article Series

This article is part of a series of 3 articles

 

Table Of Contents

This is the table of contents for this article only, each of the articles in this series has its own table of contents

Introduction

This is the 2nd part of a proposed 3 part series. Last time we talked about the demo app, saw a few screen shots, and talked about some Angular.js basics. This time we will talk about some of the common infrastructure bits within the actual demo app, and will look at 2 of the actual workflows of the demo app:

  1. Login workflow
  2. Subscription management

 

Where Is The Code

The code for this series is hosted on GitHub and can be found right here:

https://github.com/sachabarber/AngularAzureDemo

 

Prerequisites

You will need a couple of things before you can run the demo application these are as follows:

  1. Visual Studio 2013
  2. Azure SDK v2.3
  3. Azure Emulator v2.3
  4. A will to learn by yourself for some of it
  5. Patience

 

Infrastructure Parts

This article will talk about 2 of the main workflows in the demo app, but before it does I just wanted to talk about some of the main infrastructure points that enable a nice Angular.js / ASP MVC workflow.

The demo app is about an Angular.js / ASP MVC combo, which makes me happy. I want to use all the richness of Angular.js for the client along with its Single Page Application (SPA) capabilities, but I did not want to abandon tools that I have come to know. As such a lot of the sub headings below will be discussing how to get Angular.js / ASP MVC to play nice together.

 

Common Layout

One of things ASP MVC (And ASP .NET for that matter) have done for a very long time, is have the concept of a master layout page, which is used to create all the common elements of the web site. This is something I wanted to use, as such in the demo app you will find a file in the views/shared folder called "_Layout.cshtml" (the standard ASP MVC name and location). Here is the contents of that file

<!DOCTYPE html>
<!--[if lt IE 7]>      <html class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]-->
<!--[if IE 7]>         <html class="no-js lt-ie9 lt-ie8"> <![endif]-->
<!--[if IE 8]>         <html class="no-js lt-ie9"> <![endif]-->
<!--[if gt IE 8]><!-->
<html class="no-js">
<!--<![endif]-->
<head>
    <title>AngularAzureDemo</title>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
    <link href="~/favicon.ico" rel="shortcut icon" type="image/x-icon" />
    <meta name="viewport" content="width=device-width" />
    @Styles.Render("~/Content/bootstrap")
    @Styles.Render("~/Content/css")
    @Scripts.Render("~/bundles/modernizr")
</head>
<body data-ng-app="main" data-ng-controller="RootController">
    <!--[if lt IE 7]>
        <p class="chromeframe">You are using an <strong>outdated</strong> browser. 
            Please <a href="http://browsehappy.com/">upgrade your browser</a> or 
            <a href="http://www.google.com/chromeframe/?redirect=true">activate Google Chrome Frame</a> 
            to improve your experience.</p>
    <![endif]-->
    
    <nav class="navbar navbar-inverse navbar-fixed-top" role="navigation">
        <div class="container-fluid">
            <div class="navbar-header">
                <button type="button" class="navbar-toggle" 
                        data-toggle="collapse" data-target="#bs-example-navbar-collapse-1">
                    <span class="sr-only">Toggle navigation</span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                </button>
                <a class="navbar-brand" href="#">Sketcher</a>
            </div>

            <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
                <ul class="nav navbar-nav">
                    <li data-ng-class="{active : activeViewPath==='/login'}">
                        <a href="#/login">Login/Out</a>
                    </li>
                    <li data-ng-class="{active : activeViewPath==='/sketcheractions'}">
                        <a href="#/sketcheractions">Actions</a>
                    </li>
                </ul>
            </div>
        </div>
    </nav>
    <div class="container">
        @RenderBody()
    </div>
    @Scripts.Render("~/bundles/jquery")
    @Scripts.Render("~/bundles/signalr")
    <!--Reference the autogenerated SignalR hub script. -->
    <script src="/signalr/hubs"></script>
    @Scripts.Render("~/bundles/underscore")
    @Scripts.Render("~/bundles/angular")
    @Scripts.Render("~/bundles/bootstrap")
    @Scripts.Render("~/bundles/toastr")
    @Scripts.Render("~/bundles/app")
    @RenderSection("scripts", required: false)
    
    
    
    <div class="modal" id="waitModal" tabindex="-1" role="dialog" 
         aria-labelledby="waitModalTitle" aria-hidden="true" data-keyboard="false">
        <div class="modal-dialog">
            <div class="modal-content">
                <div class="modal-header">
                    <h4 class="modal-title" id="waitModalTitle">Processing...</h4>
                </div>
                <div class="modal-body">
                    <img src="~/Images/ajax-loader.GIF" />
                </div>
            </div>
        </div>
    </div>
    
    <div class="modal" id="alertModal" tabindex="-1" role="dialog" 
         aria-labelledby="alertModalTitle" aria-hidden="true" data-keyboard="true">
        <div class="modal-dialog">
            <div class="modal-content">
                <div class="modal-header">
                    <button type="button" class="close" data-dismiss="modal">
                        <span aria-hidden="true">&times;</span><span class="sr-only">Close</span>
                    </button>
                    <h4 class="modal-title" id="alertModalTitle">See JS</h4>
                </div>
                <div class="modal-body">
                    <p id="alertModalBody">See JS</p>
                </div>
            </div>
        </div>
    </div>
    
</body>
</html>

It can be seen that this file contains a few things worth pointing out:

  • Emits a few script bundles
  • Contains the Boostrap navigation bar
  • Also contains the container (see the @RenderBody() call) that will hold the body content (the single page essentially)

When the _Layout.cshtml renders it looks like this

CLICK FOR BIGGER IMAGE

Bundles

There is a fair amont of Javascript required for this demo app, as such there are various bundles created to supply and minify it, which are used by the _Layout.cshtml page we just discussed. The bundles are configured as follows:


using System.Web;
using System.Web.Optimization;

namespace AngularAzureDemo
{
    public class BundleConfig
    {
        // For more information on Bundling, visit http://go.microsoft.com/fwlink/?LinkId=254725
        public static void RegisterBundles(BundleCollection bundles)
        {
            bundles.Add(new ScriptBundle("~/bundles/jquery").Include(
                        "~/Scripts/jquery-{version}.js"));

            bundles.Add(new ScriptBundle("~/bundles/underscore").Include(
                        "~/Scripts/underscore.js"));

            bundles.Add(new StyleBundle("~/bundles/bootstrap").Include(
                        "~/Scripts/bootstrap.js"));
         
            bundles.Add(new StyleBundle("~/bundles/signalr").Include(
                        "~/Scripts/jquery.signalR-2.1.1.js"));

            bundles.Add(new StyleBundle("~/bundles/toastr").Include(
            "~/Scripts/toastr.min.js"));

            bundles.Add(new ScriptBundle("~/bundles/angular").Include(
                        "~/Scripts/angular.js",
                        "~/Scripts/angular-cookies.js",
                        "~/Scripts/angular-ng-grid.js",
                        "~/Scripts/angular-resource.js",
                        "~/Scripts/angular-animate.js",
                        "~/Scripts/angular-route.js"));

            bundles.Add(new ScriptBundle("~/bundles/app").Include(
                        "~/Scripts/app/app.js",
                        "~/Scripts/app/services/*.js",
                        "~/Scripts/app/factories/*.js",
                        "~/Scripts/app/directives/*.js",
                        "~/Scripts/app/modules/*.js",
                        "~/Scripts/app/controllers/*.js"
                        ));

            // Use the development version of Modernizr to develop with and learn from. 
            //Then, when you're ready for production, use the build tool at 
            //http://modernizr.com to pick only the tests you need.
            bundles.Add(new ScriptBundle("~/bundles/modernizr").Include(
                        "~/Scripts/modernizr-*"));

            bundles.Add(new StyleBundle("~/Content/bootstrap").Include(
                "~/Content/bootstrap.css",
                "~/Content/bootstrap-responsive.css"
                ));


            bundles.Add(new StyleBundle("~/Content/css").Include(
                        "~/Content/colorpicker.css",
                        "~/Content/toastr.min.css",
                        "~/Content/site.css",
                        "~/Content/ng-grid.css"));

            bundles.Add(new StyleBundle("~/Content/themes/base/css").Include(
                        "~/Content/themes/base/jquery.ui.core.css",
                        "~/Content/themes/base/jquery.ui.resizable.css",
                        "~/Content/themes/base/jquery.ui.selectable.css",
                        "~/Content/themes/base/jquery.ui.accordion.css",
                        "~/Content/themes/base/jquery.ui.autocomplete.css",
                        "~/Content/themes/base/jquery.ui.button.css",
                        "~/Content/themes/base/jquery.ui.dialog.css",
                        "~/Content/themes/base/jquery.ui.slider.css",
                        "~/Content/themes/base/jquery.ui.tabs.css",
                        "~/Content/themes/base/jquery.ui.datepicker.css",
                        "~/Content/themes/base/jquery.ui.progressbar.css",
                        "~/Content/themes/base/jquery.ui.theme.css"));
        }
    }
}

This is the entire contents of BundleConfig.cs

 

The Single Page

We already mentioned that the _layout.cshtml had a placeholder for "The Page", but how is this done. Well that is done using the file Index.cshtml, which has this markup.


<!-- Controller that deals with the SignalR real time push notifications-->
<div ng-controller="RealTimeNotificationsController">
</div>
    
<!--This is where the main views would get loaded for SPA
    This page is the container page for all sub views that get loaded.
    The sub views that would get loaded into ng-view would not inherit the master page,
    and hence would only send relevant html fargments to render specific views.-->
<div ng-view></div>

There are 2 main points to take in here, which are as follows:

  1. There is a DIV which has a hard coded controller, this means every page has that controller as standard
  2. That there is a DIV which has the Angular.js ng-view attribute. This is where "The View" will be rendered in "The single page" by the Angular.js apps routing configuration, which we will look at next

 

Angular Routing / ASP MVC Routing

Angular.js comes with its own routing to allow the rendering of the view into the ng-view attributed container (DIV in my case). It also comes with support for uri parameters, and everything you would expect from a routing service.

The way the routes are dealt with is typically at the Angular.js application level, which for the demo app looks like this:

// Main configuration file. Sets up AngularJS module and routes and any other config objects

var appRoot = angular.module('main',
    [   'ngRoute',
        'ngAnimate',
        'ngGrid',
        'ngResource',
        'ngCookies',
        'angularAzureDemo.services',
        'angularAzureDemo.factories',
        'angularAzureDemo.directives',
        'colorpicker.module'
    ]);     //Define the main module

appRoot
    .config(['$routeProvider', function ($routeProvider) {
        //Setup routes to load partial templates from server. 
        //TemplateUrl is the location for the server view (Razor .cshtml view)
        $routeProvider

            //home routes
            .when('/subscriptions', {
                templateUrl: '/home/subscriptions',
                controller: 'SubscriptionsController'
            })
            .when('/create', {
                templateUrl: '/home/create',
                controller: 'CreateController'
            })
            .when('/viewall', {
                templateUrl: '/home/viewall',
                controller: 'ViewAllController'
            })
            .when('/sketcheractions', {
                templateUrl: '/home/sketcheractions',
                controller: 'SketcherActionsController'
            })
            .when('/viewsingleimage/:id',
                {
                    templateUrl: '/home/viewsingleimage',
                    controller: 'ViewSingleImageController'
                }
            )

            //account routes
            .when('/login', {
                templateUrl: '/account/login',
                controller: 'LoginController'
            })

            //default
            .otherwise({ redirectTo: '/login' });
    }])
    .controller('RootController', ['$scope', '$route',
        '$routeParams', '$location', function ($scope, $route, $routeParams, $location) {
        $scope.$on('$routeChangeSuccess', function (e, current, previous) {
            $scope.activeViewPath = $location.path();
        });
    }]);

// grab underscore from window (where it attaches itself)
appRoot.constant('_', window._);

// add on underscore to global scope
appRoot.run(function ($rootScope) {
    $rootScope._ = window._;
});

 

There are quite a few things going on here, more than just the routing put it that way, so lets tackle them one by one:

  1. There is a new Angular.js module called "main" declared that takes a bunch of dependencies, which the Angular.js dependency injection system deals with for you
  2. Then the routing is configured using the Angular.js $routeprovider service. You can see a mixture of standard routes, and one that takes extra parameters for the route. Each of these will call a ASP MVC controller, which will serve up the template for the view
  3. We also set the Underscore.js helper library (awesome for arrays) as a constant such that is can be used in other Angular.js modules. Underscore.js is a funny beast in that it attaches itself to the window object, so we need to grab it from there for our constant, and also set it on the angular $rootScope

So just going back to point 2 for a minute there, we can see that there is a route like

//home routes
.when('/subscriptions', {
   templateUrl: '/home/subscriptions',
   controller: 'SubscriptionsController'
})

Lets talk about this root and follow it through, to see how the routing works (all other routes are similar to this one). There are 2 things to note there.

  1. We have a ASP MVC controller called "Home" that has an action on it called "subscriptions" that will serve the initial view template
    	using System;
    	using System.Collections.Generic;
    	using System.Linq;
    	using System.Web;
    	using System.Web.Mvc;
    	
    	namespace AngularAzureDemo.Controllers
    	{
    	    public class HomeController : Controller
    	    {
    	
    	        public ActionResult Subscriptions()
    	        {
    	            return View();
    	        }
    	    }
    	}
    	

    This will simple render the view called "Subscriptions" from the standard MVC subscriptions folder, which contains the initial template for the view
  2. Also within the route is a controller, now this is not a ASP MVC controller this time (the ASP MVC controllers are just used to serve the initial templates as requested by Angular.js), or a WebApi controller (which are just used for REST data only......confused yet!!!!!!), but an angular controller. Yes that's right, this is a Angular.js JavaScript controller for the view. So we would expect there to be a Angular.js based JavaScript SubscriptionsController, and there is, here is the skeleton of it  (don't worry about this too much, just try and understand how routing works for now, the rest may fall into place as we carry on into the article)
    	angular.module('main').controller('SubscriptionsController',
        ['$scope', '$log', '$window', '$location', 'loginService', '$cookieStore',
            'userService', 'dialogService', 'userSubscription',
        function ($scope, $log, $window, $location, loginService, $cookieStore,
            userService, dialogService, userSubscription) {
    
         .......
    
        }]);
    	

So yeah that is how routing works into the demo app between Angular.js and ASP MVC, got it, cool lets move on.

USEFUL NOTE : A fellow codeproject user has written a pretty nice/simple guide on getting Angular.js working with ASP MVC, which you can read about right here : http://www.codeproject.com/Articles/806029/Getting-started-with-AngularJS-and-ASP-NET-MVC-Par which may help solidify some of this Angular.js stuff if you are new to it

 

Angular View Animations

One of the really cool things that Angular.js has in its extensive module collection, is an animation module called ngAnimate, which can be used to animate views in and out (if you look back at the routing app setup you will see the demo apps main module takes a dependency on this ngAnimate module)

With that module you can create some pretty cool animations (the demo app uses pretty simple ones, but there are some crazy ones available here : http://tympanus.net/codrops/2013/05/07/a-collection-of-page-transitions/)

All you need is that module, and some CSS. Here is the relevant CSS

/* ANIMATIONS
============================================================================= */



.ng-enter 		{  
     -webkit-animation: scaleUpCenter .8s ease-out both ;
     -moz-animation: scaleUpCenter .8s ease-out both;
     animation: scaleUpCenter .8s ease-out both;
    z-index: 8888;  
}

.ng-leave 		{  
    z-index: 9999;  
}


/* scale down center */
@-webkit-keyframes scaleDownCenter {
	from { }
	to { opacity: 0; -webkit-transform: scale(.7); }
}
@-moz-keyframes scaleDownCenter {
	from { }
	to { opacity: 0; -moz-transform: scale(.7); }
}@keyframes scaleDownCenter {
	from { }
	to { opacity: 0; transform: scale(.7); transform: scale(.7); }
}

/* scale up center */
@-webkit-keyframes scaleUpCenter {
	from { opacity: 0; -webkit-transform: scale(.7); }
}
@-moz-keyframes scaleUpCenter {
	from { opacity: 0; -moz-transform: scale(.7); }
}
@keyframes scaleUpCenter {
	from { opacity: 0; transform: scale(.7); transform: scale(.7); }
}

All we have to do to make use of animations for our "The Single Page" transitions is target the following 2 tags:

  • .ng-enter
  • .ng-leave

Which Angular.js will append as classes to "The Single Page" view. Easy peasy no.

 

DialogService

Showing a please wait dialog, or success/error dialog are pretty common tasks, as such I decided to abstract that into a custom angular service, here is what it looks like, I think it is pretty self explanatory

angularAzureDemoServices.service('dialogService', ['$log', function ($log) {

    this.showPleaseWait = function () {
        $('#waitModal').modal('show');
    }
    
    this.hidePleaseWait = function () {
        $('#waitModal').modal('hide');
    }

    this.showAlert = function (title, content) {
        $('#alertModalTitle').text(title);
        $('#alertModalBody').text(content);
        $('#alertModal').modal('show');
    }
}]);

WebApi IOC

To do the IOC into the web api controller I have come up with a installer like syntax (somewhat like Castle Windsor but using the Unity IOC container):

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

using Microsoft.Practices.Unity;

namespace AngularAzureDemo.IOC
{
    public interface IUnityInstaller
    {
        void Install(IUnityContainer container);
    }
}




using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

using AngularAzureDemo.DomainServices;

using Microsoft.Practices.Unity;

namespace AngularAzureDemo.IOC
{
    public static class UnityContainerExtensions
    {
        public static void Install(this IUnityContainer container, IUnityInstaller installer)
        {
            installer.Install(container);
        }
    }
}



using Microsoft.Practices.Unity;
using System;
using System.Collections.Generic;
using System.Web.Http.Dependencies;

public class UnityResolver : IDependencyResolver
{
    protected IUnityContainer container;

    public UnityResolver(IUnityContainer container)
    {
        if (container == null)
        {
            throw new ArgumentNullException("container");
        }
        this.container = container;
    }

    public object GetService(Type serviceType)
    {
        try
        {
            return container.Resolve(serviceType);
        }
        catch (ResolutionFailedException)
        {
            return null;
        }
    }

    public IEnumerable<object> GetServices(Type serviceType)
    {
        try
        {
            return container.ResolveAll(serviceType);
        }
        catch (ResolutionFailedException)
        {
            return new List<object>();
        }
    }

    public IDependencyScope BeginScope()
    {
        var child = container.CreateChildContainer();
        return new UnityResolver(child);
    }

    public void Dispose()
    {
        container.Dispose();
    }
}


using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

using AngularAzureDemo.DomainServices;

using Microsoft.Practices.Unity;

namespace AngularAzureDemo.IOC
{
    public class WebApiInstaller : IUnityInstaller
    {
        public void Install(IUnityContainer container)
        {
            container.RegisterType<IUserSubscriptionRepository, UserSubscriptionRepository>(
                new HierarchicalLifetimeManager());
            container.RegisterType<IImageBlobRepository, ImageBlobRepository>(
                new HierarchicalLifetimeManager());
            container.RegisterType<IImageBlobCommentRepository, ImageBlobCommentRepository>(
                new HierarchicalLifetimeManager());
        }
    }
}

Where this is all wired up in the global.asax.cs as follows:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Http;
using System.Web.Mvc;
using System.Web.Optimization;
using System.Web.Routing;

using AngularAzureDemo.DomainServices;
using AngularAzureDemo.IOC;

using Microsoft.Practices.Unity;

namespace AngularAzureDemo
{
    public class MvcApplication : System.Web.HttpApplication
    {
        protected void Application_Start()
        {
            .....
            .....
            .....

            //IOC
            var container = new UnityContainer();
            container.Install(new WebApiInstaller());

            //set IOC resolver
            GlobalConfiguration.Configuration.DependencyResolver = new UnityResolver(container);
        }
    }
}

 

 

Login Workflow

This section outlines how the demo app login workflow works, and how it looks.

CLICK FOR BIGGER IMAGE

The login workflow works like this

  • There is a static list of users from which you must pick a user to login as. All other users in that static list instantly become your friends

Ideally I would have liked to have used Facebook OpenAuthentication for te login and grab a claims token, and then used the Facebook SDK to grab the list of freinds for the current claims token, but I can't use Facebook at work (where I write some of this stuff, at lunch time), so a static list of users it ended up being

ASP MVC Controller

There is not much to say about the MVC controller, it simply serves up the initial view template for the login route, which uses the Angular.js LoginController. Here is the code for it:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;

namespace AngularAzureDemo.Controllers
{
    public class AccountController : Controller
    {
        public ActionResult Login()
        {
            return View();
        }
    }
}

View Template / Angular Controller Interaction

And here is the template that goes with the Angular.js LoginController which is just after this

@{
    Layout = null;
}


<div class="row">
    <div>
        <br />
        <p>This is a small demo app demonstrating the following technolgies all working together</p>
        <br />
        <img src="~/Images/banners.png" 
             class="img-responsive" 
             alt="Responsive image">
    </div>
</div>
<br />
<br />

<div class="well">
    <div class="row">

        <div class="col-xs-12 col-sm-8 col-md-8" ng-show="!isLoggedIn">
            <h2>Login</h2>
            <p>Please pick a user to log in as</p>
            <div class="row">
                <p class="col-xs-12 col-sm-8 col-md-8">
                    <select class="form-control" 
                            data-ng-options="o.Name for o in usersList" 
                            data-ng-model="selectedPerson" 
                            ng-disabled="isLoggedIn"></select>
                </p>
                <div class="col-xs-12 col-sm-4 col-md-4">
                    <button type="button" id="btnLogin" 
                            class="btn btn-primary" 
                            data-ng-click="login()" 
                            ng-disabled="isLoggedIn">LOG IN</button>
                </div>
            </div>

        </div>

        <div class="col-xs-12 col-sm-8 col-md-8" ng-show="isLoggedIn">
            <h2>Logout</h2>
            <p>You are logged in as : <span ng-bind="selectedPerson.Name"></span></p>
            <div class="row">
                <p class="col-xs-12 col-sm-4 col-md-4">
                    <button type="button" 
                            id="btnLogout" 
                            class="btn btn-primary" 
                            data-ng-click="logout()" 
                            ng-disabled="!isLoggedIn">LOG OUT</button>
                </p>
            </div>
        </div>
    </div>

</div>

Angular Login Controller

Here is the full code for the Angular Login controller

appRoot.controller('LoginController', ['$scope', '$log', '$location', '$resource',
    '$window', 'loginService', 'dialogService','userService', '_',
    function ($scope, $log, $location, $resource, $window,
        loginService, dialogService, userService, _) {

        $scope.usersList = [];
        $scope.selectedPerson = null;
        $scope.isLoggedIn = false;

        $log.log("logged in " + loginService.isLoggedIn());

        dialogService.showPleaseWait();

        getAllPeople();

        $scope.login = function () {
            loginService.login($scope.selectedPerson);
            $scope.isLoggedIn = true;
            $location.path("sketcheractions");
        };

        $scope.logout = function () {
            $scope.selectedPerson = null;
            loginService.logout();
            $scope.isLoggedIn = false;
        };

        function getPersonFromList(userName) {
            $scope.selectedPerson = _.findWhere($scope.usersList,
                { Name: userName });
        }

        function getAllPeople() {
            userService.getAll()
                .success(function (users) {
                    $scope.usersList = users;
                    if (loginService.isLoggedIn()) {
                        getPersonFromList(loginService.currentlyLoggedInUser().Name);
                        $scope.isLoggedIn = true;
                        $log.log("selected person " + $scope.selectedPerson.Name);
                    }
                    dialogService.hidePleaseWait();
                })
                .error(function (error) {
                    dialogService.hidePleaseWait();
                    $window.alert('Error', 'Unable to load user data: ' + error.message);
                });
        }
    }]);
    

Where the following are the important bits

  • We make use of a common dialog service which we looked at before
  • That we make use of a UserService
  • That we take a couple of dependencies, such as
    • $log : Angular.js logging services (you should use this instead of Console.log)
    • $location : Allows controllers to change routes
    • $window : Angular.js window abstraction
    • loginService : custom service to deal with login
    • dialogService : custom service to show dialogs (wait/error etc etc)
    • userService : Angular.js $resource for obtaining user data
    • _ : which is the underscore library (useful functions for working with arrays)

 

The 2 main ones I wanted to dig a bit deeper into are the loginService and the userService

LoginService

The is a very simple authentication service (extremely naive, as I say I would ideally liked to have used facebook open authentication), that is able to store a logged in user, and is able to tell callers if there is a current logged in user

Here is the relevant code

angularAzureDemoServices.service('loginService', ['$log', function ($log) {

    this.loggedInUser = null;

    this.login = function (currentUser) {
        this.loggedInUser = currentUser;
        $log.log('Logged in user ' + this.loggedInUser.name);
    }

    this.logout = function () {
        this.loggedInUser = null;
        $log.log('User has been logged out');
    }

    this.isLoggedIn = function() {
        return typeof this.loggedInUser !== 'undefined' && this.loggedInUser != null;
    }

    this.currentlyLoggedInUser = function () {
        return this.loggedInUser;
    }

}]);

This service gets used all over the place, where it is typically used at the start of the Angular.js controller, and is used to see if there is a currently logged in user, if there is not the controller redirects to the "Login" route

UserService

The UserService is a custom Angular.js $resource based service which talks to a WebApi controller at the following url /api/User. Here is the UserService code:

// http://weblogs.asp.net/dwahlin/using-an-angularjs-factory-to-interact-with-a-restful-service
angularAzureDemoServices.service('userService',
    ['$http', '$window', function ($http, $window) {
    
    var urlBase = '/api/user';

    this.getAll = function () {
        return $http.get(urlBase);
    };

    this.getFriends = function (id) {
        return $http.get(urlBase + '/' + id);
    };
}]);

And here is the relevant WebApi UserController code:

using System.Collections.Generic;
using System.Linq;
using System.Web.Http;
using AngularAzureDemo.Models;

namespace AngularAzureDemo.Controllers
{
    /// <summary>
    /// API controller to manage users
    /// </summary>
    public class UserController : ApiController
    {
        private readonly Users users;

        public UserController()
        {
            users = new Users();
        }

        // GET api/user
        public IEnumerable<User> Get()
        {
            // Return a static list of people
            return users;
        }

        // GET api/user/5
        [System.Web.Http.HttpGet]
        public IEnumerable<User> Get(int id)
        {
            //return static list of people which do not include the current person
            return users.Where(x => x.Id != id);
        }
    }
}

This makes use of the following helper class to provide the actual static list of users

using System.Collections.Generic;

namespace AngularAzureDemo.Models
{
    public class Users : List<User>
    {
        public Users()
        {
            this.Add(new User{Id=1, Name="Sacha Barber"});
            this.Add(new User{Id=2, Name="Adam Gril"});
            this.Add(new User{Id=3, Name="James Franklin"});
            this.Add(new User{Id=4, Name="Vicky Merry" });
            this.Add(new User{Id=5, Name="Cena Rego"});
        }
    }
}

Subscription Management Workflow

This section outlines how the demo app subscriptions workflow works, and how it looks.

CLICK FOR BIGGER IMAGE

The subscriptions workflow works like this

  • There is a static list of users all of which are your immediate friends apart from the one you chose to login as. The rest are available for you choose as friends you would like to receive real time notification from should the create a new sketch
  • Subscriptions may be added/removed from the UI, at which point they will be stored in Azure table storage

Like I have stated before I would have liked to have used Facebook OpenAuthentication for te login and grab a claims token, and then used the Facebook SDK to grab the list of freinds for the current claims token, but I can't use Facebook at work (where I write some of this stuff, at lunch time), so a static list of users it ended up being

 

ASP MVC Controller

There is not much to say about the MVC controller, it simply serves up the initial view template for the subscriptions route, which uses the Angular.js Subscriptionsontroller. Here is the code for it:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;

namespace AngularAzureDemo.Controllers
{
    public class HomeController : Controller
    {
        public ActionResult Subscriptions()
        {
            return View();
        }
    }
}

View Template / Angular Controller Interaction

And here is the template that goes with the Angular.js SubscriptionsController which is just after this

@{
    Layout = null;
}


<div ng-show="hasSubscriptions">

    <br />
    <div class="well">
        <h2>Subscriptions</h2>
        <div class="row">
            <div class="col-xs-12 col-sm-8 col-md-8">
                <table class="table table-striped table-condensed">
                    <tr>
                        <th>Id</th>
                        <th>Name</th>
                        <th>Subscription Active</th>
                    </tr>
                    <tbody ng:repeat="friendsSubscription in allFriendsSubscriptions">
                        <tr>
                            <td>{{friendsSubscription.Id}}</td>
                            <td>{{friendsSubscription.Name}}</td>
                            <td><input type="checkbox" 
                                       ng-model="friendsSubscription.IsActive"></td>
                        </tr>
                    </tbody>
                </table>
            </div>
        </div>
        <div class="row">
            <div class="col-xs-12 col-sm-4 col-md-4">
                <button type="button" 
                        class="btn btn-primary" 
                        data-ng-click="updateSubscriptions()">UPDATE SUBSCRIPTIONS</button>
            </div>
        </div>
    </div>
</div>

Angular Subscriptions Controller

Here is the full code for the Angular Subscriptions controller, not too much to talk about we get the full friends list, and all the stored subscriptions, and just create a new projection of object which will have the on/off flag depending on whether you have a subscription stored for the user in the friends list

Should the subscriptons be modified they will be stored in a cookie, which is done by using the standard Angular.js $cookieStore service.

angular.module('main').controller('SubscriptionsController',
    ['$scope', '$log', '$window', '$location', 'loginService', '$cookieStore',
        'userService', 'dialogService', 'userSubscription',
    function ($scope, $log, $window, $location, loginService, $cookieStore,
        userService, dialogService, userSubscription) {

        if (!loginService.isLoggedIn()) {
            $location.path("login");
        }

            
        $scope.storedSubscriptions = [];
        $scope.allFriendsSubscriptions = [];

        $log.log('Logged in user Id : ',  loginService.currentlyLoggedInUser().Id);


        $scope.hasSubscriptions = false;


        dialogService.showPleaseWait();
        getAllFriends(loginService.currentlyLoggedInUser().Id);
     

        function getAllFriends(id) {
            userService.getFriends(id)
                .success(function (friends) {
                    $log.log('friends count : ', friends.length);

                    $scope.storedSubscriptions = [];

                    for (var i = 0; i &lt; friends.length; i++) {
                        friends[i].IsActive = false;
                        $scope.storedSubscriptions.push(friends[i]);
                    }

                    //get all actual stored subscriptions
                    getAllSubscriptions(id);
                        
                })
                .error(function (error) {
                    dialogService.hidePleaseWait();
                    dialogService.showAlert('Error',
                        'Unable to load friend data: ' + error.message);
                });
        }


        function getAllSubscriptions(id) {

            userSubscription.get({ id: id }, function (result) {

                var savedSubscriptions = result.Subscriptions;

                $log.log('subscription count : ', savedSubscriptions.length);

                for (var i = 0; i &lt; savedSubscriptions.length; i++) {
                    var friendSubscription = _.findWhere($scope.storedSubscriptions,
                    {
                         Id: savedSubscriptions[i].FriendId
                    });

                    if (typeof friendSubscription !== 'undefined' && friendSubscription != null) {
                        friendSubscription.IsActive = true;
                    } else {
                        $log.log('could not find friend', savedSubscriptions[i].FriendId);
                    }
                }

                $scope.allFriendsSubscriptions = $scope.storedSubscriptions;

                $cookieStore.put('allFriendsSubscriptions', $scope.allFriendsSubscriptions);

                $scope.hasSubscriptions = true;
                dialogService.hidePleaseWait();
            }, function (error) {
                dialogService.hidePleaseWait();
                dialogService.showAlert('Error',
                    'Unable to load subscription data: ' + error.message);

            });
        }


        $scope.updateSubscriptions = function () {

            dialogService.showPleaseWait();

            $log.log('Updating the subscriptions');

            var subscriptionsToSave = [];
            for (var i = 0; i &lt; $scope.allFriendsSubscriptions.length; i++) {
                subscriptionsToSave.push(
                {
                    "UserId": loginService.currentlyLoggedInUser().Id,
                    "FriendId": $scope.allFriendsSubscriptions[i].Id,
                    "IsActive": $scope.allFriendsSubscriptions[i].IsActive
                });
            }

            $log.log('subscriptionsToSave', subscriptionsToSave);
            
            var userSubscriptions = {
                Subscriptions : subscriptionsToSave
            }

            userSubscription.save((userSubscriptions), function (result) {
                $log.log('saveSubscriptions result : ', result);
                if (result) {
                    dialogService.hidePleaseWait();
                    dialogService.showAlert('Success', 'Successfully saved all subscriptions');
                } else {
                    dialogService.hidePleaseWait();
                    $window.alert('Unable to save subscription data');
                    dialogService.showAlert('Error', 'Unable to save subscription data');
                }
            }, function (error) {
                dialogService.hidePleaseWait();
                dialogService.showAlert('Error',
                    'Unable to save subscription data: ' + error.message);
            });
        };

    }]);

This controller also makes use of 2 custom Angular.js services/factories, namely

  • UserService
  • UserSubscription Factory

Which will be looking at next

 

UserService

This is a Angular.js custom $resource that is used to communicate with a standard web api controller.

Here is the full code for the custom Angular.js UserService

angularAzureDemoServices.service('userService',
    ['$http', '$window', function ($http, $window) {
    
    var urlBase = '/api/user';

    this.getAll = function () {
        return $http.get(urlBase);
    };

    this.getFriends = function (id) {
        return $http.get(urlBase + '/' + id);
    };
}]);

It can be seen this UserService is used to talk to the user web api controller, which simply returns the list of all users, which is as follows:

using System.Collections.Generic;
using System.Linq;
using System.Web.Http;
using AngularAzureDemo.Models;

namespace AngularAzureDemo.Controllers
{
    /// <summary>
    /// API controller to manage users
    /// </summary>
    public class UserController : ApiController
    {
        private readonly Users users;

        public UserController()
        {
            users = new Users();
        }

        // GET api/user
        public IEnumerable<User> Get()
        {
            // Return a static list of people
            return users;
        }

        // GET api/user/5
        [System.Web.Http.HttpGet]
        public IEnumerable<User> Get(int id)
        {
            //return static list of people which do not include the current person
            return users.Where(x => x.Id != id);
        }
    }
}

 

UserSubscription Factory

This is a Angular.js custom $resource that is used to communicate with a standard web api controller.

Here is the full code for the custom Angular.js UserSubscription factory

angularAzureDemoFactories.factory('userSubscription', ['$resource', function ($resource) {

    var urlBase = '/api/usersubscription/:id';

    return $resource(
        urlBase,
        { id: "@id" },
        {
            "save": { method: "POST", isArray: false }
        });
}]);

It can be seen this UserSubscription is used to talk to the usersubscription web api controller, which has various methods for saving/retrieving user subscription data (via a repository) from Azure table store. Here is the web api controller

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Web.Http;
using System.Web.Mvc;

using AngularAzureDemo.DomainServices;
using AngularAzureDemo.Models;


namespace AngularAzureDemo.Controllers
{
    /// <summary>
    /// API controller to manage user subscriptions
    /// </summary>
    public class UserSubscriptionController : ApiController
    {
        private readonly IUserSubscriptionRepository userSubscriptionRepository;

        public UserSubscriptionController(IUserSubscriptionRepository userSubscriptionRepository)
        {
            this.userSubscriptionRepository = userSubscriptionRepository;
        }

        // GET api/usersubscription/5
        [System.Web.Http.HttpGet]
        public async Task<UserSubscriptions> Get(int id)
        {
            
            if (id <= 0)
                return new UserSubscriptions();

            // Return a static list of people
            var subscriptions = await userSubscriptionRepository.FetchSubscriptions(id);
            UserSubscriptions userSubscriptionsToSave = new UserSubscriptions();
            userSubscriptionsToSave.Subscriptions = subscriptions.ToList();
            return userSubscriptionsToSave;
        }

        // POST api/usersubscription/....
        [System.Web.Http.HttpPost]
        public async Task<bool> Post(UserSubscriptions userSubscriptions)
        {
            var subscriptions = userSubscriptions.Subscriptions;

            if (!subscriptions.Any())
                return false;

            int id = subscriptions[0].UserId;

            if (subscriptions.Any(x => x.UserId != id))
                return false;

            // remove all subscriptions that user chose to remove
            var subscriptionsToDelete = subscriptions.Where(x => !x.IsActive).ToList();
            if (subscriptionsToDelete.Any())
            {
                await userSubscriptionRepository.RemoveSubscriptions(subscriptionsToDelete);
            }

            // add all subscriptions that user now has active
            var subscriptionsToAdd = subscriptions.Where(x => x.IsActive).ToList();
            if (subscriptionsToAdd.Any())
            {
                await userSubscriptionRepository.AddSubscriptions(subscriptionsToAdd);
            }
            return true;
        }
    }
}

It can be seen above that since this is now server side code, we are free to use async/await (which is fully supported by the web api v2). The only other thing to note in this code is that we also make use of a UserSubscriptionRepository which is injected into the web api controller using the IOC code we saw above.

 It is the UserSubscriptionRepository that talks to Azure table store. The full code for that is as follows:

using System;
using System.Collections.Generic;
using System.Configuration;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
using System.Web;

using AngularAzureDemo.Azure.TableStorage;
using AngularAzureDemo.Models;

using Microsoft.WindowsAzure.Storage;
using Microsoft.WindowsAzure.Storage.Table;
using Microsoft.WindowsAzure.Storage.Table.Queryable;

namespace AngularAzureDemo.DomainServices
{
    public interface IUserSubscriptionRepository
    {
        Task<bool> AddSubscriptions(IEnumerable<UserSubscription> subscriptionsToAdd);
        Task<IEnumerable<UserSubscription>> FetchSubscriptions(int userId);
        Task<bool> RemoveSubscriptions(IEnumerable<UserSubscription> subscriptionsToRemove);
    }


    public class UserSubscriptionRepository : IUserSubscriptionRepository
    {

        private readonly string azureStorageConnectionString;
        private readonly CloudStorageAccount storageAccount;

        public UserSubscriptionRepository()
        {
            azureStorageConnectionString = 
                ConfigurationManager.AppSettings["azureStorageConnectionString"];
            storageAccount = CloudStorageAccount.Parse(azureStorageConnectionString);
        }



        public async Task<bool> AddSubscriptions(IEnumerable<UserSubscription> subscriptionsToAdd)
        {
            CloudTableClient tableClient = storageAccount.CreateCloudTableClient();
            CloudTable userSubscriptionsTable = 
                tableClient.GetTableReference("userSubscriptions");

            var tableExists = await userSubscriptionsTable.ExistsAsync();
            if (!tableExists)
            {
                await userSubscriptionsTable.CreateIfNotExistsAsync();
            }

            TableBatchOperation batchOperation = new TableBatchOperation();
            foreach (var subscription in subscriptionsToAdd)
            {
                UserSubscriptionEntity userSubscriptionEntity =
                    new UserSubscriptionEntity(subscription.UserId, subscription.FriendId);
                batchOperation.InsertOrReplace(userSubscriptionEntity);
            }
            await userSubscriptionsTable.ExecuteBatchAsync(batchOperation);

            return true;
        }

        public async Task<IEnumerable<UserSubscription>> FetchSubscriptions(int userId)
        {
            CloudTableClient tableClient = storageAccount.CreateCloudTableClient();
            CloudTable userSubscriptionsTable = tableClient.GetTableReference("userSubscriptions");

            var tableExists = await userSubscriptionsTable.ExistsAsync();
            if (!tableExists)
            {
                return new List<UserSubscription>();
            }

            List<UserSubscriptionEntity> activeUserSubscriptionEntities = new List<UserSubscriptionEntity>();
            Expression<Func<UserSubscriptionEntity, bool>> filter = 
                (x) => x.PartitionKey == userId.ToString();
            
            Action<IEnumerable<UserSubscriptionEntity>> processor = 
                activeUserSubscriptionEntities.AddRange;

            await ObtainUserSubscriptionEntities(userSubscriptionsTable, filter, processor);

            var userSubscriptions = activeUserSubscriptionEntities.Select(x => new UserSubscription()
            {
                UserId = int.Parse(x.PartitionKey),
                FriendId = int.Parse(x.RowKey),
                IsActive = true
            }).ToList();
          

            return userSubscriptions;
        }

        public async Task<bool> RemoveSubscriptions(IEnumerable<UserSubscription> subscriptionsToRemove)
        {
            CloudTableClient tableClient = storageAccount.CreateCloudTableClient();
            CloudTable userSubscriptionsTable = tableClient.GetTableReference("userSubscriptions");

            var tableExists = await userSubscriptionsTable.ExistsAsync();
            if (!tableExists)
            {
                return false;
            }

            List<UserSubscriptionEntity> activeUserSubscriptionEntities = new List<UserSubscriptionEntity>();
            Expression<Func<UserSubscriptionEntity, bool>> filter = 
                (x) => x.PartitionKey == subscriptionsToRemove.First().UserId.ToString();

            Action<IEnumerable<UserSubscriptionEntity>> processor = activeUserSubscriptionEntities.AddRange;

            await ObtainUserSubscriptionEntities(userSubscriptionsTable, filter, processor);

            TableBatchOperation deletionBatchOperation = new TableBatchOperation();
            foreach (var userSubscription in subscriptionsToRemove)
            {
                var entity = activeUserSubscriptionEntities.SingleOrDefault(
                    x => x.PartitionKey == userSubscription.UserId.ToString() && 
                    x.RowKey == userSubscription.FriendId.ToString());

                if (entity != null)
                {
                    deletionBatchOperation.Add(TableOperation.Delete(entity));
                }
            }

            if (deletionBatchOperation.Any())
            {
                await userSubscriptionsTable.ExecuteBatchAsync(deletionBatchOperation);
            }
            return true;


        }


        private async Task<bool> ObtainUserSubscriptionEntities(
            CloudTable userSubscriptionsTable,
            Expression<Func<UserSubscriptionEntity, bool>> filter,
            Action<IEnumerable<UserSubscriptionEntity>> processor)
        {
            TableQuerySegment<UserSubscriptionEntity> segment = null;

            while (segment == null || segment.ContinuationToken != null)
            {
                var query = userSubscriptionsTable
                                .CreateQuery<UserSubscriptionEntity>()
                                .Where(filter)
                                .AsTableQuery();

                segment = await query.ExecuteSegmentedAsync(segment == null ? 
                    null : segment.ContinuationToken);
                processor(segment.Results);
            }

            return true;
        }

    }
}

There are a couple of take away points from here:

  1. Azure SDK supports async / await, so I use it
  2. Azure table storage supports limited LINQ queries, very limited, but it can be done, so we do that by using a custom Func<T,TR>
  3. We don't know how many items are stored, so I opted for doing it in batches, which is done using the TableQuerySegment class, which you can see in the code above
  4. When we delete items from the Azure table store I don't want to do it one by one that would be pretty silly, so we use the TableBatchOperation as demonstrated in the code above
  5. Azure table storage has a upsert like feature (MERGE in standard SQL), so we use that, it is a static method of TableBatchOperation called TableBatchOperation.InsertOrReplace(..)

Other than those points, it is all pretty standard stuff

 

 

 

What's Next

Phew, we are done for this article. Sigh of relief.

In this article we started to disect the demo app, and we looked at some of the common infrastructure pieces and talked about Login and Subscription workflows

Next time we will finish up disecting the demo app, and will be looking at the following 4 workflows, those will be:

  • Create Sketch
  • View All Sketches
  • View Single Sketch
  • Real Time Notification (another user creating a Sketch in different browser/session)

 

That's All

That is all I wanted to say in this article. I guess you may like this one a bit better than part 1 as it has some actual code in it.

If you like what you have seen, a vote or comment is most welcome.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here