Click here to Skip to main content
15,867,488 members
Articles / Hosted Services / Azure

Sketcher : Three Of n

Rate me:
Please Sign up or sign in to vote.
5.00/5 (22 votes)
14 Aug 2014CPOL12 min read 33.2K   28   5
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 3rd part of a proposed 3 part series. Last time we talked about some of the infrastructre peices of the demo app, and we also talked about 2 of the main workflows namely Login/Subscription management. This time we will talk about the final workflows of the demo app:

  1. Creating sketches workflow
  2. Viewing all sketches workflow
  3. Viewing a single sketch workflow

 

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

 

Create Workflow

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

Image 1

CLICK FOR BIGGER IMAGE

If you have the user that is creating the sketch if you list of subscriptions you should also recieve a popup norification when the create a sketch on their system. You can then click on the notification and that will open their new sketch. This should look something like this (user on left is subscriber to user or right, they are both using different browsers/http sessions)

Image 2

CLICK FOR BIGGER IMAGE

The login workflow works like this

  • We use a html 5 canvas, and allow the user to draw with the mouse (this is done using a custom directive)
  • We also allow the user to pick the stroke thickness and color (the color picer is a Angular.js module I grabbed of the internet)
  • When the user saves the sketch a couple of things happen
    • We grab the canva data as a base64 encoded string, which is then store in Azure blob storage
    • We also create a Azure table storage row for the sketch, which simply points to the Azure blob storage url for its url field
    • We use SignalR on the server side to broadcast out that a new sketch has been created
    • The client side has a special (always visible controller called RealTimeNotificationsController which uses a SignalR client side javascript proxy, which will listen for a broadcast message about a new sketch. When it sees a message arrive it examines the message UserId, and sees if it one of the users you have active subscriptions for, if it is a popup notification is shown which you may click on to open the other users sketch

 

ASP MVC Controller

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

C#
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 Create()
        {
            return View();
        }
    }
}

View Template / Angular Controller Interaction

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

HTML
@{
    Layout = null;
}


<br />
<div class="well">
        <h2>Create Actions</h2>
        <div class="row">
        <p class="col-xs-12 col-sm-8 col-md-8">
            <label class="labeller">Stroke Color:</label>
            <input colorpicker type="text" ng-model="strokeColor" />
        </p>
        <p class="col-xs-12 col-sm-8 col-md-8">
            <label class="labeller">Stroke Width:</label>
            <input type="range" min="3" max="30" ng-model="strokeWidth">
        </p>
        <p class="col-xs-12 col-sm-8 col-md-8">
            <label class="labeller">Title:</label>
            <input type="text" maxlength="30" ng-model="title">
        </p>
        <p class="col-xs-12 col-sm-8 col-md-8">
            <canvas class="drawingSurface"
                    stroke-color='strokeColor'
                    stroke-width='strokeWidth'
                    update-controller="acceptCanvas(canvasArg)"
                    ng-model='firstCanvas'
                    draw></canvas>
        </p>
    </div>
</div>

<div class="row">
    <p class="col-xs-12 col-sm-8 col-md-8">
        <button ng-click='firstCanvas = ""' 
                class="btn btn-default">CLEAR</button>
        <button type="button" class="btn btn-primary" 
                data-ng-click="save()">SAVE</button>
    </p>
</div>

Angular Draw Directive

The view is suprisingly simply. There is a html 5 canvas element which is the real guts of the view, but can you see how the canvas has some extra attributes on it such as:

  • stroke-color
  • stroke-width
  • update-controller
  • draw

What are these? These are there thanks to the crazy Angular.js feature called directives, which allow you to create whole new attributes, and even DOM elements. This one in particular is one that does the drawing on the canvas, where the code is as shown below. Angular.js does use some conventions, such as "-" being replaced with "", and the text is converted to camel casing.

The weird "=" / "=?" scope text, are to do with how you want your bindings setup, whether they are one way, two way etc etc

he & binding allows a directive to trigger evaluation of an expression in the context of the original scope, at a specific time

A good resource for this sort of stuff can be found here :

JavaScript
angularAzureDemoDirectives.directive("draw", ['$timeout', function ($timeout) {
    return {
        scope: {
            ngModel: '=',
            strokeColor: '=?',
            strokeWidth: '=?',
            updateController: '&updateController'
        },
        link: function (scope, element, attrs) {
            scope.strokeWidth = scope.strokeWidth || 3;
            scope.strokeColor = scope.strokeColor || '#343536';

            var canvas = element[0];
            var ctx = canvas.getContext('2d');

            // variable that decides if something should be drawn on mousemove
            var drawing = false;

            // the last coordinates before the current move
            var lastX;
            var lastY;

            element.bind('mousedown', function (event) {
                lastX = event.offsetX;
                lastY = event.offsetY;

                // begins new line
                ctx.beginPath();

                drawing = true;
            });

            element.bind('mouseup', function (event) {
                // stop drawing
                drawing = false;
                exportImage();
            });

            element.bind('mousemove', function (event) {
                if (!drawing) {
                    return;
                }

                draw(lastX, lastY, event.offsetX, event.offsetY);

                // set current coordinates to last one
                lastX = event.offsetX;
                lastY = event.offsetY;
            });

            scope.$watch('ngModel', function (newVal, oldVal) {
                if (!newVal && !oldVal) {
                    return;
                }

                if (!newVal && oldVal) {
                    reset();
                }
            });

            // canvas reset
            function reset() {
                element[0].width = element[0].width;
            }

            function draw(lX, lY, cX, cY) {
                // line from
                ctx.moveTo(lX, lY);

                // to
                ctx.lineTo(cX, cY);

                ctx.lineCap = 'round';

                // stroke width
                ctx.lineWidth = scope.strokeWidth;

                // color
                ctx.strokeStyle = scope.strokeColor;

                // draw it
                ctx.stroke();
            }

            function exportImage() {
                $timeout(function () {
                    scope.ngModel = canvas.toDataURL();
                    scope.updateController({ canvasArg: canvas.toDataURL() });
                });
            }
        }
    };
}]);

The bulk of this directive are to do with placing strokes on the html 5 canvas element, which this directive is declared on. However this directive also takes care of grabbing the current base64 encoded string of the canvas data, which it sends back to the original scope (CreateController which is the overall scope for the current view in this case). It does this using a simple timeout function, which can be seen in the exportImage() function right at the end. See how we use the canvas.toDataURL(), that grabs the base 64 encoded string. You should go look at the view again, and see how the directive binding ends up calling the CreateController AcceptCanvas() function

 

Angular Create Controller

Here is the full code for the Angular CreateController

JavaScript
angular.module('main').controller('CreateController',
    ['$scope', '$log', '$window', '$location', 'loginService', 'imageBlob','dialogService',
    function ($scope, $log, $window, $location, loginService, imageBlob, dialogService) {

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

        $scope.changeLog = function () {
            console.log('change');
        };

        $scope.strokeColor = "#a00000";
        $scope.strokeWidth = 5;
        $scope.canvasData = null;
        $scope.title = 'New Image';

        $scope.acceptCanvas = function (newCanvasData) {
            $scope.canvasData = newCanvasData;
        }


        $scope.save = function () {
            if (typeof $scope.canvasData !== 'undefined' && $scope.canvasData != null) {

                var imageBlobToSave = {
                        "UserId": loginService.currentlyLoggedInUser().Id,
                        "UserName": loginService.currentlyLoggedInUser().Name,
                        "CanvasData": $scope.canvasData,
                        "Title": $scope.title,
                        "CreatedOn" : new Date()
                };

                imageBlob.save(imageBlobToSave, function (result) {

                    $log.log('save blobs result : ', result);
                    if (result) {
                        dialogService.showAlert('Success','Sucessfully saved image data');
                    } else {
                        dialogService.showAlert('Error','Unable to save image data');
                    }
                }, function (error) {
                    dialogService.showAlert('Error',
                        'Unable to save image 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)
    • $window : Angular.js window abstraction
    • $location : Allows controllers to change routes
    • loginService : custom service to deal with login
    • dialogService : custom service to show dialogs (wait/error etc etc)
    • imageBlob : Angular.js $resource for obtaining/saving image blob data
    • That the magical (seemlibly unused) AcceptCanvas() function, is actually called by the draw directive. Go have a relook at the draw directive, and also the create view to see how this all binds together

 

ImageBlob service

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

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

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

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

 

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

C#
 
C#
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;
using AngularAzureDemo.SignalR;
using Microsoft.AspNet.SignalR;


namespace AngularAzureDemo.Controllers
{
    /// <summary>
    /// API controller to store user images and their metadata
    /// </summary>
    public class ImageBlobController : ApiController
    {
        private readonly IImageBlobRepository imageBlobRepository;

        public ImageBlobController(IImageBlobRepository imageBlobRepository)
        {
            this.imageBlobRepository = imageBlobRepository;
        }

        // POST api/imageblob/....
        [System.Web.Http.HttpPost]
        public async Task<bool> Post(ImageBlob imageBlob)
        {
            if (imageBlob == null || imageBlob.CanvasData == null)
                return false;

            // add the blob to blob storage/table storage
            var storedImageBlob = await imageBlobRepository.AddBlob(imageBlob);
            if (storedImageBlob != null)
            {
                BlobHub.SendFromWebApi(storedImageBlob);
            }
            return false;
        }
    }
}

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 IImageBlobRepository which is injected into the web api controller using the IOC code we saw above.

It is the IImageBlobRepository that talks to Azure table store, it also the  that deals with converting the html 5 canvas base64 encoded string into a byte[] that will be stored as a image in Azure blb storage. The full code for that is as follows:

C#
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.Blob;
using Microsoft.WindowsAzure.Storage.Table;
using Microsoft.WindowsAzure.Storage.Table.Queryable;

namespace AngularAzureDemo.DomainServices
{
    public interface IImageBlobRepository
    {
        Task<IEnumerable<ImageBlob>> FetchAllBlobs();
        Task<IEnumerable<ImageBlob>> FetchBlobsForUser(int userId);
        Task<IEnumerable<ImageBlob>> FetchBlobForBlobId(Guid id);
        Task<ImageBlob> AddBlob(ImageBlob imageBlobToStore);
    }


    public class ImageBlobRepository : IImageBlobRepository
    {
        private readonly CloudStorageAccount storageAccount;
        private readonly Users users = new Users();
        private const int LIMIT_OF_ITEMS_TO_TAKE = 1000;


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

        public async Task<IEnumerable<ImageBlob>> FetchAllBlobs()
        {
            var tableModel = await AquireTable();
            if (!tableModel.TableExists)
            {
                return new List<ImageBlob>();    
            }

            List<ImageBlob> imageBlobs = new List<ImageBlob>();

            //http://blog.liamcavanagh.com/2011/11/how-to-sort-azure-table-store-results-chronologically/
            string rowKeyToUse = string.Format("{0:D19}", 
                DateTime.MaxValue.Ticks - DateTime.UtcNow.Ticks);

            foreach (var user in users)
            {
                List<ImageBlobEntity> imageBlobEntities = new List<ImageBlobEntity>();
                Expression<Func<ImageBlobEntity, bool>> filter = 
                    (x) =>  x.PartitionKey == user.Id.ToString() &&
                            x.RowKey.CompareTo(rowKeyToUse) > 0;

                Action<IEnumerable<ImageBlobEntity>> processor = imageBlobEntities.AddRange;
                await ObtainImageBlobEntities(tableModel.Table, filter, processor);
                var projectedImages = ProjectToImageBlobs(imageBlobEntities);
                imageBlobs.AddRange(projectedImages);

            }

            var finalImageBlobs = imageBlobs.OrderByDescending(x => x.CreatedOn).ToList();

            return finalImageBlobs;
        }

        public async Task<IEnumerable<ImageBlob>> FetchBlobsForUser(int userId)
        {
            var tableModel = await AquireTable();
            if (!tableModel.TableExists)
            {
                return new List<ImageBlob>();
            }

            //http://blog.liamcavanagh.com/2011/11/how-to-sort-azure-table-store-results-chronologically/
            string rowKeyToUse = string.Format("{0:D19}", DateTime.MaxValue.Ticks - DateTime.UtcNow.Ticks);

            List<ImageBlobEntity> imageBlobEntities = new List<ImageBlobEntity>();
            Expression<Func<ImageBlobEntity, bool>> filter = 
                (x) =>  x.PartitionKey == userId.ToString() &&
                        x.RowKey.CompareTo(rowKeyToUse) > 0;

            Action<IEnumerable<ImageBlobEntity>> processor = imageBlobEntities.AddRange;
            await ObtainImageBlobEntities(tableModel.Table, filter, processor);
            var imageBlobs = ProjectToImageBlobs(imageBlobEntities);
            return imageBlobs;
        }

        public async Task<IEnumerable<ImageBlob>> FetchBlobForBlobId(Guid id)
        {
            var tableModel = await AquireTable();
            if (!tableModel.TableExists)
            {
                return new List<ImageBlob>();
            }

            List<ImageBlobEntity> imageBlobEntities = new List<ImageBlobEntity>();
            Expression<Func<ImageBlobEntity, bool>> filter = (x) => x.Id == id;

            Action<IEnumerable<ImageBlobEntity>> processor = imageBlobEntities.AddRange;
            await ObtainImageBlobEntities(tableModel.Table, filter, processor);
            var imageBlobs = ProjectToImageBlobs(imageBlobEntities);

            return imageBlobs;
        }


        public async Task<ImageBlob> AddBlob(ImageBlob imageBlobToStore)
        {
            BlobStorageResult blobStorageResult = await StoreImageInBlobStorage(imageBlobToStore);
            if (!blobStorageResult.StoredOk)
            {
                return null;
            }
            else
            {
                CloudTableClient tableClient = storageAccount.CreateCloudTableClient();
                CloudTable imageBlobsTable = tableClient.GetTableReference("ImageBlobs");

                var tableExists = await imageBlobsTable.ExistsAsync();
                if (!tableExists)
                {
                    await imageBlobsTable.CreateIfNotExistsAsync();
                }
                
                ImageBlobEntity imageBlobEntity = new ImageBlobEntity(
                        imageBlobToStore.UserId,
                        imageBlobToStore.UserName,
                        Guid.NewGuid(),
                        blobStorageResult.BlobUrl,
                        imageBlobToStore.Title,
                        imageBlobToStore.CreatedOn
                    );

                TableOperation insertOperation = TableOperation.Insert(imageBlobEntity);
                imageBlobsTable.Execute(insertOperation);

                return ProjectToImageBlobs(new List<ImageBlobEntity>() { imageBlobEntity }).First();
            }
        }

        private async Task<ImageBlobCloudModel> AquireTable()
        {
            CloudTableClient tableClient = storageAccount.CreateCloudTableClient();
            CloudTable imageBlobsTable = tableClient.GetTableReference("ImageBlobs");

            var tableExists = await imageBlobsTable.ExistsAsync();
            return new ImageBlobCloudModel
            {
                TableExists = tableExists,
                Table = imageBlobsTable
            };
        }

        private static List<ImageBlob> ProjectToImageBlobs(List<ImageBlobEntity> imageBlobEntities)
        {
            var imageBlobs =
                imageBlobEntities.Select(
                    x =>
                        new ImageBlob()
                        {
                            UserId = int.Parse(x.PartitionKey),
                            UserName = x.UserName,
                            SavedBlobUrl = x.BlobUrl,
                            Id = x.Id,
                            Title = x.Title,
                            CreatedOn = x.CreatedOn,
                            CreatedOnPreFormatted = x.CreatedOn.ToShortDateString(),
                        }).ToList();
            return imageBlobs;
        }

        private async Task<BlobStorageResult> StoreImageInBlobStorage(ImageBlob imageBlobToStore)
        {
           
            CloudBlobClient blobClient = storageAccount.CreateCloudBlobClient();
            CloudBlobContainer container = blobClient.GetContainerReference("images");

            bool created = container.CreateIfNotExists();
            container.SetPermissionsAsync(
                new BlobContainerPermissions
                {
                    PublicAccess = BlobContainerPublicAccessType.Blob
                });


            var blockBlob = container.GetBlockBlobReference(string.Format(@"{0}.image/png", 
                Guid.NewGuid().ToString()));
            string marker = "data:image/png;base64,";
            string dataWithoutJpegMarker = imageBlobToStore.CanvasData.Replace(marker, String.Empty);
            byte[] filebytes = Convert.FromBase64String(dataWithoutJpegMarker);

            blockBlob.UploadFromByteArray(filebytes, 0, filebytes.Length);
            return new BlobStorageResult(true, blockBlob.Uri.ToString());
        }

        private async Task<bool> ObtainImageBlobEntities(
            CloudTable imageBlobsTable,
            Expression<Func<ImageBlobEntity, bool>> filter,
            Action<IEnumerable<ImageBlobEntity>> processor)
        {
            TableQuerySegment<ImageBlobEntity> segment = null;

            while (segment == null || segment.ContinuationToken != null)
            {
                var query = imageBlobsTable
                                .CreateQuery<ImageBlobEntity>()
                                .Where(filter)
                                .Take(LIMIT_OF_ITEMS_TO_TAKE)
                                .AsTableQuery();

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

            return true;
        }
    }
}

Notifications

Real time notifications are done by the web socket (there are more transports) enabled library SignalR. What happens is as follows:

  1. A new blob is stored for the image
  2. A call to the SignalR hub is done from the web api controller, which broadcasts the event to all connected clients
  3. The client recieved this and sees if it is from someone in their suscription list, if it is a notification is shown

The web api controller that initates all this is the line

C#
BlobHub.SendFromWebApi(storedImageBlob);

And here is all the SignalR hub code (yep that's it)

C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Timers;
using System.Web;
using AngularAzureDemo.Models;
using Microsoft.AspNet.SignalR;

namespace AngularAzureDemo.SignalR
{
    public class BlobHub : Hub
    {
        public void Send(ImageBlob latestBlob)
        {
            Clients.All.latestBlobMessage(latestBlob);
        }

        //Called from Web Api controller, so must use GlobalHost context resolution
        public static void SendFromWebApi(ImageBlob imageBlob)
        {
            var hubContext = GlobalHost.ConnectionManager.GetHubContext<BlobHub>();
            hubContext.Clients.All.latestBlobMessage(imageBlob);
        }
    }
}

And here is the code for the Angular.js RealTimeNotificationsController that is visible on every page, so it is always active. It can be seen that this controller is where we start / connect to the server side SignalR hub, and deal with any real time notifications that come up, where we show a toast notification should it come from one of the users we have in our subscription list.

I am using John Papa's Toast library to do the notifications.

JavaScript
angular.module('main').controller('RealTimeNotificationsController',
        ['$rootScope','$scope', '$log', '$window', '$location', '$cookieStore', '_',
    function ($rootScope, $scope, $log, $window, $location, $cookieStore, _) {

        toastr.options = {
            "closeButton": true,
            "debug": false,
            "positionClass": "toast-top-right",
            "onclick": navigate,
            "showDuration": "5000000",
            "hideDuration": "1000",
            "timeOut": "5000000",
            "extendedTimeOut": "1000",
            "showEasing": "swing",
            "hideEasing": "linear",
            "showMethod": "fadeIn",
            "hideMethod": "fadeOut"
        }

        $scope.latestBlobId = '';


        $(function () {

            // Declare a proxy to reference the hub.
            var blobProxy = $.connection.blobHub;

            // Create a function that the hub can call to broadcast messages.
            blobProxy.client.latestBlobMessage = function (latestBlob) {

                //do this here as this may have changed since this controller first started. Its quick lookup
                //so no real harm doing it here
                $scope.allFriendsSubscriptionsCookie = $cookieStore.get('allFriendsSubscriptions');

                var userSubscription = _.findWhere($scope.allFriendsSubscriptionsCookie,
                    { Id: latestBlob.UserId });

                if (userSubscription != undefined) {


                    $scope.latestBlobId = latestBlob.Id;
                    $scope.$apply();

                    //show toast notification
                    var text = latestBlob.UserName + ' has just created a new image called "' +
                        latestBlob.Title + '", click here to view it';
                    toastr['info'](text, "New image added");
                }
            };

            //start the SignalR hub comms
            startHubConnection();

            $.connection.hub.disconnected(function () {
                $log.log('*** BlobHub Disconnected');
                setTimeout(function () {
                    startHubConnection();
                }, 1000); // Restart connection after 1 seconds.
            });
        });


        function startHubConnection() {
            //start the SignalR hub comms
            $.connection.hub.start(
                {
                    transport: ['webSockets', 'longPolling'],
                    waitForPageLoad: false
                });
        }

        function navigate() {
            $rootScope.$apply(function() {
                $location.path("viewsingleimage/" + $scope.latestBlobId);
                $location.replace();
            });
        } 
    }]);

 

View All WorkFlow

The view all workflow is one of the simpler ones, where it really just grabs all the saved image blobs and a count of their comments and shows them in a table, from where the user may click on one of them to see just that image

Image 3

CLICK FOR BIGGER IMAGE

 

ASP MVC Controller

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

C#
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 ViewAll()
        {
            return View();
        }
    }
}

View Template / Angular Controller Interaction

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

HTML
@{
    Layout = null;
}

<div ng-show="hasItems">
    <br />
    <div class="well">
        <h2>Latest Sketches</h2>
        <div class="row">
            <div ng-repeat="tableItem in tableItems" 
					class="col-xs-12 col-sm-3 col-md-2 resultTableItem">
                <a ng-href="#/viewsingleimage/{{tableItem.Blob.Id}}">
                    <div class="resultItem">
                        <div class="resultItemInner">
                            <div class="titleInner">
                                <p><span class="smallText">Title:</span>
                                <span class="smallText">{{tableItem.Blob.Title}}</span></p>
                                <p><span class="smallText">Comments:</span>
                                <span class="smallText">{{tableItem.Comments.length}}</span></p>
                            </div>
                            <div class="imgHolder">
                                <img ng-src="{{tableItem.Blob.SavedBlobUrl}}" 
                                     class="img-responsive resultsImage" alt="Responsive image" />
                            </div>
                        </div>
                    </div>
                </a>
            </div>

        </div>
    </div>
</div>

It can seen that we simply display a list of items from the controller using Angular.js ng-repeat. The only other bit of interest is that when you click on one of the sketches you are redirected to the single image. This is done by using a big html anchor tag around the whole item template, which when clicked will ask Angular.js to perform a redirect

 

Angular View All Controller

Here is the full code for the Angular ViewAllController

JavaScript
angular.module('main').controller('ViewAllController',
    ['$scope', '$log', '$window', '$location', 'loginService', 'imageBlobComment','dialogService',
    function ($scope, $log, $window, $location, loginService, imageBlobComment, dialogService) {

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

        $scope.storedBlobs = [];
        $scope.tableItems = [];
        $scope.hasItems = false;

        dialogService.showPleaseWait();
        getAllBlobs();

        function getAllBlobs() {
            imageBlobComment.get(function (result) {
                $scope.storedBlobs = [];
                if (result.BlobComments.length == 0) {
                    dialogService.hidePleaseWait();
                    dialogService.showAlert('Info',
                        'There are no items stored right now');
                    $scope.hasItems = false;
                } else {
                    $scope.hasItems = true;
                    $scope.tableItems = result.BlobComments;
                    dialogService.hidePleaseWait();
                }
            }, function (error) {
                $scope.hasItems = false;
                dialogService.hidePleaseWait();
                dialogService.showAlert('Error',
                    'Unable to load stored image 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)
    • $window : Angular.js window abstraction
    • $location : Allows controllers to change routes
    • loginService : custom service to deal with login
    • dialogService : custom service to show dialogs (wait/error etc etc)
    • imageBlobComment : Angular.js $resource for obtaining image blob and comment data
    • That the controller provides a list of items for the view to bind to

 

ImageBlobComment service

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

JavaScript
angularAzureDemoFactories.factory('imageBlobComment', ['$resource', '$http', function ($resource, $http) {

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

    var ImageBlobComment = $resource(
        urlBase,
        { id: "@id" },
        {
            "save": { method: "POST", isArray: false }
        });

    ImageBlobComment.fetchSingle = function(id) {
        return $http.get('/api/imageblobcomment/' + id);
    }

    return ImageBlobComment;
}]);

It can be seen this imageBlobComment is used to talk to the ImageBlobController web api controller, which has various methods for retrieving user blob comment data (via a repository) from Azure blob store. Here is the web api controller

C#
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 store user images and their metadata
    /// </summary>
    public class ImageBlobCommentController : ApiController
    {
        private readonly IImageBlobRepository imageBlobRepository;
        private readonly ImageBlobCommentRepository imageBlobCommentRepository;

        public ImageBlobCommentController(IImageBlobRepository imageBlobRepository, 
            ImageBlobCommentRepository imageBlobCommentRepository)
        {
            this.imageBlobRepository = imageBlobRepository;
            this.imageBlobCommentRepository = imageBlobCommentRepository;
        }

        // GET api/imageblobcomment
        [System.Web.Http.HttpGet]
        public async Task<FullImageBlobComments> Get()
        {
            // Return a list of all ImageBlob objects 
            var blobs = await imageBlobRepository.FetchAllBlobs();
            
            //fetch all comments to form richer results
            var fullImageBlobComments = await FetchBlobComments(blobs);

            return fullImageBlobComments;
           
        }


        // GET api/imageblobcomment/4E89064B-D1B1-471C-8B2F-C02B374A9676
        [System.Web.Http.HttpGet]
        public async Task<FullImageBlobComment> Get(Guid id)
        {
            if (Guid.Empty == id)
                return new FullImageBlobComment();

            // Return a blob that matched the Id requested
            var blob = await imageBlobRepository.FetchBlobForBlobId(id);
            //fetch all comments to form richer results
            var fullImageBlobComments = await FetchBlobComments(new List<ImageBlob>() {
                blob.First()
            });

            return fullImageBlobComments.BlobComments.Any() ? 
                fullImageBlobComments.BlobComments.First() : new FullImageBlobComment();
        }


        // POST api/imageblobcomment/....
        [System.Web.Http.HttpPost]
        public async Task<ImageBlobCommentResult> Post(ImageBlobComment imageBlobCommentToSave)
        {
            if (imageBlobCommentToSave == null)
                return new ImageBlobCommentResult() { Comment = null, SuccessfulAdd = false};

            if (string.IsNullOrEmpty(imageBlobCommentToSave.Comment))
                return new ImageBlobCommentResult() { Comment = null, SuccessfulAdd = false };

            // add the imageBlobComment to imageBlobComment storage/table storage
            var insertedComment = await imageBlobCommentRepository.
                AddImageBlobComment(imageBlobCommentToSave);

            return new ImageBlobCommentResult()
            {
                Comment = insertedComment, SuccessfulAdd = true
            };
        }


        private async Task<FullImageBlobComments> FetchBlobComments(IEnumerable<ImageBlob> blobs)
        {
            FullImageBlobComments fullImageBlobComments = new FullImageBlobComments();
            foreach (var blob in blobs)
            {
                var comments = await imageBlobCommentRepository.FetchAllCommentsForBlob(blob.Id);
                fullImageBlobComments.BlobComments.Add(
                    new FullImageBlobComment()
                    {
                        Blob = blob,
                        Comments = comments.ToList()
                    });
            }
            return fullImageBlobComments;
            
        }
    }
}

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 IImageBlobCommentRepository which is injected into the web api controller using the IOC code we saw above. It is the IImageBlobCommentRepository  that deals with gathering the data from Azure table store for blobs and comments. Here is the code for the IImageBlobCommentRepository

C#
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.Blob;
using Microsoft.WindowsAzure.Storage.Table;
using Microsoft.WindowsAzure.Storage.Table.Queryable;

namespace AngularAzureDemo.DomainServices
{
    public interface IImageBlobCommentRepository
    {
        Task<IEnumerable<ImageBlobComment>> FetchAllCommentsForBlob(Guid associatedBlobId);
        Task<ImageBlobComment> AddImageBlobComment(ImageBlobComment imageBlobCommentToStore);
    }


    public class ImageBlobCommentRepository : IImageBlobCommentRepository
    {

        private readonly string azureStorageConnectionString;
        private readonly CloudStorageAccount storageAccount;
        private Users users = new Users();
        private const int LIMIT_OF_ITEMS_TO_TAKE = 1000;


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


        public async Task<IEnumerable<ImageBlobComment>> FetchAllCommentsForBlob(Guid associatedBlobId)
        {
            CloudTableClient tableClient = storageAccount.CreateCloudTableClient();
            CloudTable imageBlobCommentsTable = tableClient.GetTableReference("ImageBlobComments");

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

            //http://blog.liamcavanagh.com/2011/11/how-to-sort-azure-table-store-results-chronologically/
            string rowKeyToUse = string.Format("{0:D19}", 
                DateTime.MaxValue.Ticks - DateTime.UtcNow.Ticks);

            List<ImageBlobCommentEntity> blobCommentEntities = new List<ImageBlobCommentEntity>();
            Expression<Func<ImageBlobCommentEntity, bool>> filter = 
                (x) =>  x.AssociatedBlobId == associatedBlobId &&
                        x.RowKey.CompareTo(rowKeyToUse) > 0;

            Action<IEnumerable<ImageBlobCommentEntity>> processor = blobCommentEntities.AddRange;
            await this.ObtainBlobCommentEntities(imageBlobCommentsTable, filter, processor);
            var imageBlobs = ProjectToBlobComments(blobCommentEntities);


            return imageBlobs;
        }


        public async Task<ImageBlobComment> AddImageBlobComment(ImageBlobComment imageBlobCommentToStore)
        {
            CloudTableClient tableClient = storageAccount.CreateCloudTableClient();
            CloudTable imageBlobCommentsTable = tableClient.GetTableReference("ImageBlobComments");

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

            ImageBlobCommentEntity imageBlobCommentEntity = new ImageBlobCommentEntity(
                    imageBlobCommentToStore.UserId,
                    imageBlobCommentToStore.UserName,
                    Guid.NewGuid(),
                    imageBlobCommentToStore.AssociatedBlobId,
                    imageBlobCommentToStore.Comment,
                    imageBlobCommentToStore.CreatedOn.ToShortDateString()
                );

            TableOperation insertOperation = TableOperation.Insert(imageBlobCommentEntity);
            var result = imageBlobCommentsTable.Execute(insertOperation);


            return ProjectToBlobComments(new List<ImageBlobCommentEntity>() {imageBlobCommentEntity}).First();
        }


        private static List<ImageBlobComment> ProjectToBlobComments(
            List<ImageBlobCommentEntity> blobCommentEntities)
        {
            var blobComments =
                blobCommentEntities.Select(
                    x =>
                        new ImageBlobComment()
                        {
                            Comment = x.Comment,
                            UserName = x.UserName,
                            CreatedOn = DateTime.Parse(x.CreatedOn),
                            CreatedOnPreFormatted = DateTime.Parse(x.CreatedOn).ToShortDateString(),
                            UserId = Int32.Parse(x.PartitionKey),
                            Id = x.Id,
                            AssociatedBlobId = x.AssociatedBlobId
                        }).ToList();
            return blobComments;
        }


        private async Task<bool> ObtainBlobCommentEntities(
            CloudTable imageBlobsTable,
            Expression<Func<ImageBlobCommentEntity, bool>> filter,
            Action<IEnumerable<ImageBlobCommentEntity>> processor)
        {
            TableQuerySegment<ImageBlobCommentEntity> segment = null;

            while (segment == null || segment.ContinuationToken != null)
            {
                var query = imageBlobsTable
                                .CreateQuery<ImageBlobCommentEntity>()
                                .Where(filter)
                                .Take(LIMIT_OF_ITEMS_TO_TAKE)
                                .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

 

View Single WorkFlow

The view single workflow is also fairly straight forward in that  it simply shows a single view and allows users to add comments to the sketch. You may only add comments if you are not the owner of the sketch.

Image 4

CLICK FOR BIGGER IMAGE

 

ASP MVC Controller

There is not much to say about the MVC controller, it simply serves up the initial view template for the viewsingle route, which uses the Angular.js ViewSingleController.

Here is the code for it:

C#
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 ViewSingleImage()
        {
            return View();
        }
    }
}

View Template / Angular Controller Interaction

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

HTML
@{
    Layout = null;
}

<div ng-show="hasItem">
    <br />
    <div class="well">
        <h2>{{storedBlob.Blob.Title}}</h2>
        <span class="smallText">Created By:</span>
        <span class="smallText">{{storedBlob.Blob.UserName}}</span>
        <br/>
        <span class="smallText">Created On:</span>
        <span class="smallText">{{storedBlob.Blob.CreatedOnPreFormatted}}</span>
        <div class="row">
            <br/>
            <div class="col-xs-12 col-sm-12 col-md-12">
                <img ng-src="{{storedBlob.Blob.SavedBlobUrl}}" 
                     class="img-responsive" 
                     alt="Responsive image" />
            </div>

        </div>
        <br />
        <br />
        <div class="row" ng-hide="isTheirOwnImage">
            <div id="newCommentActionRow" class="col-xs-12 col-sm-12 col-md-12">
                <h4>Add A New Comment</h4>
            </div>
            <p class="col-xs-12 col-sm-8 col-md-8">
                <input type="text" class="form-control" ng-model="commentText">
            </p>
            <div class="col-xs-12 col-sm-4 col-md-4">
                <button type="button" class="btn btn-primary" 
                        data-ng-click="saveComment()" 
                        ng-disabled="!hasComment()">ADD</button>
            </div>
            <br />
        </div>        
        
        
        <div class="row" ng-show="storedBlob.Comments.length > 0">
            <br />
            <br />
            <h4 class="commentHeader">Comments</h4>
            <div class="col-xs-12 col-sm-12 col-md-12">
                <table class="table table-striped table-condensed">
                    <tr>
                        <th>Comment Left By</th>
                        <th>Comment Date</th>
                        <th>Comment</th>
                    </tr>
                    <tbody ng:repeat="comment in storedBlob.Comments">
                        <tr>
                            <td width="25%">{{comment.UserName}}</td>
                            <td width="25%">{{comment.CreatedOnPreFormatted}}</td>
                            <td width="50%">{{comment.Comment}}</td>
                        </tr>
                    </tbody>
                </table>
            </div>
        </div>
        

    </div>
</div>

There is not much to say about this view, its all pretty standard stuff actually. A few bindings nothing more.

 

Angular View Single Controller

Here is the full code for the Angular ViewSingleController

C#
angular.module('main').controller('ViewSingleImageController',
    ['$scope', '$log', '$window', '$location', '$routeParams',
        'loginService', 'imageBlobComment', 'dialogService',
    function ($scope, $log, $window, $location, $routeParams,
        loginService, imageBlobComment, dialogService) {

        $scope.currentUserId = 0;
        if (!loginService.isLoggedIn()) {
            $location.path("login");
        } else {
            $scope.currentUserId = loginService.currentlyLoggedInUser().Id;
        }


        $log.log('single controller $scope.currentUserId', $scope.currentUserId);
        $log.log('single controller loginService.currentlyLoggedInUser().Id',
            loginService.currentlyLoggedInUser().Id);


        $scope.id = $routeParams.id;
        $scope.storedBlob = null;
        $scope.hasItem = false;
        $scope.commentText = '';
        $scope.isTheirOwnImage = false;

        $log.log('Route params = ', $routeParams);
        $log.log('ViewSingleImageController id = ', $scope.id);

        dialogService.showPleaseWait();

        getBlob($scope.id);


        $scope.hasComment = function () {
            return typeof $scope.commentText !== 'undefined' &&
                $scope.commentText != null
                && $scope.commentText != '';
        };

       

        $scope.saveComment = function () {
            
            if ($scope.hasComment()) {

                var imageBlobCommentToSave = {
                    "UserId": loginService.currentlyLoggedInUser().Id,
                    "UserName": loginService.currentlyLoggedInUser().Name,
                    "CreatedOn": new Date(),
                    "AssociatedBlobId": $scope.storedBlob.Blob.Id,
                    "Comment" : $scope.commentText
                };

                imageBlobComment.save(imageBlobCommentToSave, function (result) {

                    $log.log('save imageBlobComments result : ', result);
                    if (result.SuccessfulAdd) {
                        $scope.storedBlob.Comments.unshift(result.Comment);
                    } else {
                        dialogService.showAlert('Error', 'Unable to save comment');
                    }
                }, function (error) {
                    $log.log('save imageBlobComments error : ', error);
                    dialogService.showAlert('Error', 'Unable to save comment: ' + error.message);
                });
            }



        };

        function getBlob(id) {

            imageBlobComment.fetchSingle(id)
                .success(function (result) {
                    $scope.hasItem = true;
                    $scope.storedBlob = result;

                    $log.log("SCOPE BLOB", $scope.storedBlob);

                    $scope.isTheirOwnImage = $scope.storedBlob.Blob.UserId == $scope.currentUserId;

                    $log.log('single controller $scope.currentUserId', $scope.currentUserId);
                    $log.log('single controller loginService.currentlyLoggedInUser().Id',
                        loginService.currentlyLoggedInUser().Id);
                    $log.log('single controller $scope.storedBlob.Blob.UserId',
                        $scope.storedBlob.Blob.UserId);


                    dialogService.hidePleaseWait();

                }).error(function (error) {
                    $scope.hasItem = false;
                    dialogService.hidePleaseWait();
                    dialogService.showAlert('Error',
                        'Unable to load stored image 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)
    • $window : Angular.js window abstraction
    • $location : Allows controllers to change routes
    • $routeParams : We use this service to grab the url parameters from the route that we are now rendering, which allows us to load up the correct entity. The source for the viewsingle route was when the user clicked on single sketch in the viewall view
    • loginService : custom service to deal with login
    • dialogService : custom service to show dialogs (wait/error etc etc)
    • imageBlobComment : Angular.js $resource for obtaining image blob and comment data

 

ImageBlobComment service

The imageBlobComment is a custom Angular.js $resource based service which talks to a WebApi controller at the following url /api/ImageBlobComment. We have already seen the code for this above. The web api controller also makes use of the IImageBlobCommentRepository which we also seen the code for above.

 

 

That's All

That is all I wanted to say in this series.I hope you have enjoyed the series, and have learned something along the way. I know I always learn quite a bit when I write an article, its my main learning vehicle.

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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
United Kingdom United Kingdom
I currently hold the following qualifications (amongst others, I also studied Music Technology and Electronics, for my sins)

- MSc (Passed with distinctions), in Information Technology for E-Commerce
- BSc Hons (1st class) in Computer Science & Artificial Intelligence

Both of these at Sussex University UK.

Award(s)

I am lucky enough to have won a few awards for Zany Crazy code articles over the years

  • Microsoft C# MVP 2016
  • Codeproject MVP 2016
  • Microsoft C# MVP 2015
  • Codeproject MVP 2015
  • Microsoft C# MVP 2014
  • Codeproject MVP 2014
  • Microsoft C# MVP 2013
  • Codeproject MVP 2013
  • Microsoft C# MVP 2012
  • Codeproject MVP 2012
  • Microsoft C# MVP 2011
  • Codeproject MVP 2011
  • Microsoft C# MVP 2010
  • Codeproject MVP 2010
  • Microsoft C# MVP 2009
  • Codeproject MVP 2009
  • Microsoft C# MVP 2008
  • Codeproject MVP 2008
  • And numerous codeproject awards which you can see over at my blog

Comments and Discussions

 
GeneralMy vote of 5 Pin
Rajesh Pillai6-Apr-15 19:07
Rajesh Pillai6-Apr-15 19:07 
GeneralMy vote of 5 Pin
D V L20-Feb-15 17:44
professionalD V L20-Feb-15 17:44 
GeneralRe: My vote of 5 Pin
Sacha Barber23-Feb-15 0:27
Sacha Barber23-Feb-15 0:27 
QuestionThat's a beautiful thing. Pin
Pete O'Hanlon14-Aug-14 22:20
subeditorPete O'Hanlon14-Aug-14 22:20 
AnswerRe: That's a beautiful thing. Pin
Sacha Barber14-Aug-14 22:34
Sacha Barber14-Aug-14 22:34 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.