This article is part of a series of 3 articles
This is the table of contents for this article only, each of the articles in this series has its own table of contents
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:
- Login workflow
- Subscription management
The code for this series is hosted on GitHub and can be found right here:
https://github.com/sachabarber/AngularAzureDemo
You will need a couple of things before you can run the demo application these are as follows:
- Visual Studio 2013
- Azure SDK v2.3
- Azure Emulator v2.3
- A will to learn by yourself for some of it
- Patience
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.
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>
<!---->
<!---->
<!---->
<!---->
<html class="no-js">
<!---->
<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">
<!---->
<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")
<!---->
<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">×</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
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
{
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"
));
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
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.
<!---->
<div ng-controller="RealTimeNotificationsController">
</div>
<!---->
<div ng-view></div>
There are 2 main points to take in here, which are as follows:
- There is a DIV which has a hard coded controller, this means every page has that controller as standard
- 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.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:
var appRoot = angular.module('main',
[ 'ngRoute',
'ngAnimate',
'ngGrid',
'ngResource',
'ngCookies',
'angularAzureDemo.services',
'angularAzureDemo.factories',
'angularAzureDemo.directives',
'colorpicker.module'
]);
appRoot
.config(['$routeProvider', function ($routeProvider) {
$routeProvider
.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'
}
)
.when('/login', {
templateUrl: '/account/login',
controller: 'LoginController'
})
.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();
});
}]);
appRoot.constant('_', window._);
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:
- 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
- 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
- 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
.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.
- 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
- 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
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
.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;
}
@-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); }
}
@-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:
Which Angular.js will append as classes to "The Single Page" view. Easy peasy no.
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');
}
}]);
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()
{
.....
.....
.....
var container = new UnityContainer();
container.Install(new WebApiInstaller());
GlobalConfiguration.Configuration.DependencyResolver = new UnityResolver(container);
}
}
}
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
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();
}
}
}
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>
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
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
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:
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
{
public class UserController : ApiController
{
private readonly Users users;
public UserController()
{
users = new Users();
}
public IEnumerable<User> Get()
{
return users;
}
[System.Web.Http.HttpGet]
public IEnumerable<User> Get(int id)
{
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"});
}
}
}
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
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();
}
}
}
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>
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 < friends.length; i++) {
friends[i].IsActive = false;
$scope.storedSubscriptions.push(friends[i]);
}
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 < 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 < $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
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
{
public class UserController : ApiController
{
private readonly Users users;
public UserController()
{
users = new Users();
}
public IEnumerable<User> Get()
{
return users;
}
[System.Web.Http.HttpGet]
public IEnumerable<User> Get(int id)
{
return users.Where(x => x.Id != id);
}
}
}
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
{
public class UserSubscriptionController : ApiController
{
private readonly IUserSubscriptionRepository userSubscriptionRepository;
public UserSubscriptionController(IUserSubscriptionRepository userSubscriptionRepository)
{
this.userSubscriptionRepository = userSubscriptionRepository;
}
[System.Web.Http.HttpGet]
public async Task<UserSubscriptions> Get(int id)
{
if (id <= 0)
return new UserSubscriptions();
var subscriptions = await userSubscriptionRepository.FetchSubscriptions(id);
UserSubscriptions userSubscriptionsToSave = new UserSubscriptions();
userSubscriptionsToSave.Subscriptions = subscriptions.ToList();
return userSubscriptionsToSave;
}
[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;
var subscriptionsToDelete = subscriptions.Where(x => !x.IsActive).ToList();
if (subscriptionsToDelete.Any())
{
await userSubscriptionRepository.RemoveSubscriptions(subscriptionsToDelete);
}
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:
- Azure SDK supports async / await, so I use it
- 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>
- 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
- 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
- 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
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 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.