65.9K
CodeProject 正在变化。 阅读更多。
Home

Sketcher:N 中的三

starIconstarIconstarIconstarIconstarIcon

5.00/5 (22投票s)

2014年8月15日

CPOL

12分钟阅读

viewsIcon

35878

Angular.Js / Azure / ASP MVC / SignalR / Bootstrap 演示应用

文章系列

本文是 3 篇系列文章的一部分

 

目录

这是本文的目录,本系列中的每篇文章都有自己的目录

引言

这是拟议的 3 部分系列文章的第 3 部分。上次我们讨论了演示应用的一些基础设施组件,还讨论了两个主要工作流程,即登录/订阅管理。这次我们将讨论演示应用的最后几个工作流程。

  1. 创建草图工作流程
  2. 查看所有草图工作流程
  3. 查看单个草图工作流程

 

代码在哪里

本系列的源代码托管在 GitHub 上,可以在这里找到:

https://github.com/sachabarber/AngularAzureDemo

 

必备组件

在运行演示应用程序之前,你需要准备好以下几项:

  1. Visual Studio 2013
  2. Azure SDK v2.3
  3. Azure Emulator v2.3
  4. 愿意自己学习一部分内容
  5. 耐心

 

创建工作流程

本节概述了演示应用的 create 工作流程如何工作以及它的样子。

点击查看大图

如果您是创建草图的用户,那么如果您订阅了其他用户,当他们在其系统上创建草图时,您也应该收到弹出通知。然后,您可以单击通知,它将打开他们的草图。这看起来应该像这样(左边的用户订阅了右边的用户,他们都使用了不同的浏览器/HTTP 会话)。

点击查看大图

登录工作流程如下所示。

  • 我们使用 HTML5 canvas,并允许用户使用鼠标进行绘制(这是通过自定义指令完成的)。
  • 我们还允许用户选择笔触粗细和颜色(颜色选择器是我从互联网上获取的一个 Angular.js 模块)。
  • 当用户保存草图时,会发生几件事:
    • 我们将 canvas 数据抓取为 base64 编码字符串,然后将其存储在 Azure blob 存储中。
    • 我们还为草图创建了一个 Azure 表存储行,该行在其 url 字段中仅指向 Azure blob 存储的 url。
    • 我们在服务器端使用 SignalR 来广播新草图已创建。
    • 客户端有一个特殊的(始终可见的)控制器,名为 RealTimeNotificationsController,它使用 SignalR 客户端 JavaScript 代理,该代理将收听关于新草图的广播消息。当它看到一条消息到达时,它会检查消息的 UserId,并查看它是否是您活动订阅之一的用户,如果是,则会显示一个弹出通知,您可以单击该通知来打开其他用户的草图。

 

ASP MVC 控制器

MVC 控制器没什么好说的,它只是为 create 路由提供初始视图模板,该路由使用 Angular.js CreateController。这是它的代码:

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();
        }
    }
}

视图模板 / Angular 控制器交互

这是与 Angular.js CreateController 匹配的模板,就在下面。

@{
    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 绘图指令

视图出奇地简单。有一个 HTML5 canvas 元素,这是视图的核心,但您可以看到 canvas 上有一些额外的属性,例如:

  • stroke-color
  • stroke-width
  • update-controller
  • 绘制。

这些是什么?这些是由于 Angular.js 的一个名为 directives 的神奇功能而存在的,它允许您创建全新的属性,甚至是 DOM 元素。这个指令尤其负责在 canvas 上进行绘制,其代码如下所示。Angular.js 使用一些约定,例如 "-" 被替换为 "",并且文本被转换为驼峰式命名。

奇怪的 "=" / "=?" 作用域文本与您希望如何设置绑定有关,无论是单向、双向还是其他方式。

& 绑定允许指令在特定时间在原始作用域的上下文中触发表达式的求值。

这方面的一个好资源可以在这里找到:

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() });
                });
            }
        }
    };
}]);

此指令的大部分内容都与在指令声明所用的 HTML5 canvas 元素上放置笔触有关。但是,此指令还负责获取 canvas 数据的当前 base64 编码字符串,并将其发送回原始作用域(在这种情况下,CreateController 是当前视图的整体作用域)。它通过一个简单的超时函数来实现这一点,该函数可以在 exportImage() 函数的末尾看到。看看我们如何使用 canvas.toDataURL(),它会获取 base64 编码字符串。您应该再次查看视图,看看指令绑定最终如何调用 CreateControllerAcceptCanvas() 函数。

 

Angular 创建控制器

这是 Angular CreateController 的完整代码。

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);
                });
            }
        };
    }]);

其中以下是重要部分:

  • 我们使用了之前讨论过的通用对话框服务。
  • 我们使用了 UserService。
  • 我们接受了一些依赖项,例如:
    • $logAngular.js 日志服务(您应该使用它而不是 Console.log)。
    • $windowAngular.js window 抽象。
    • $location:允许控制器更改路由。
    • loginService:处理登录的自定义服务。
    • dialogService:显示对话框(等待/错误等)的自定义服务。
    • imageBlob:用于获取/保存图像 blob 数据的 Angular.js $resource。
    • 看上去(似乎未使用的)AcceptCanvas() 函数实际上是由 draw 指令调用的。请再次查看 draw 指令以及 create 视图,看看这一切是如何结合在一起的。

 

ImageBlob 服务

imageBlob 是一个自定义的 Angular.js $resource 基础服务,它连接到以下 URL 的 WebApi 控制器:/api/ImageBlob。这是 imageBlob 工厂代码:

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

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

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

 

可以看到,imageBlob 用于与 ImageBlobController Web API 控制器通信,该控制器具有通过存储库从 Azure blob 存储保存/检索用户 blob 数据的各种方法。这是 Web API 控制器:

 
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;
        }
    }
}

从上面的代码可以看到,由于现在是服务器端代码,我们可以自由使用 async/await(Web API v2 完全支持)。唯一需要注意的代码是,我们还使用了 IImageBlobRepository,它通过我们在上面看到的 IOC 代码注入到 Web API 控制器中。

IImageBlobRepository 负责与 Azure 表存储通信,它还负责将 HTML5 canvas 的 base64 编码字符串转换为 byte[],该 byte[] 将作为图像存储在 Azure blob 存储中。完整的代码如下:

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

实时通知由支持 WebSocket(还有其他传输)的库 SignalR 完成。情况如下:

  1. 为图像存储了一个新的 blob。
  2. Web API 控制器调用 SignalR hub,该 hub 将事件广播给所有已连接的客户端。
  3. 客户端收到此消息,并查看是否来自其订阅列表中的某人;如果是,则显示通知。

引发这一切的 Web API 控制器是这一行:

BlobHub.SendFromWebApi(storedImageBlob);

这是所有的 SignalR hub 代码(是的,就这些)。

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);
        }
    }
}

这是 Angular.js RealTimeNotificationsController 的代码,它在每个页面上可见,因此始终处于活动状态。可以看到,此控制器是我们启动/连接到服务器端 SignalR hub 并处理任何实时通知的地方,如果我们收到的通知来自我们订阅列表中的用户,则会显示一个 toast 通知。

我正在使用 John Papa 的 Toast 库进行通知。

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();
            });
        } 
    }]);

 

查看所有工作流程

查看所有工作流程是最简单的之一,它只是获取所有保存的图像 blob 及其评论计数,并在表中显示它们,用户可以单击其中一个来仅查看该图像。

点击查看大图

 

ASP MVC 控制器

MVC 控制器没什么好说的,它只是为 viewall 路由提供初始视图模板,该路由使用 Angular.js ViewAllController。这是它的代码:

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();
        }
    }
}

视图模板 / Angular 控制器交互

这是与 Angular.js ViewAllController 匹配的模板,就在下面。

@{
    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>

可以看到,我们只是使用 Angular.js 的 ng-repeat 在视图中显示项目列表。唯一其他有趣的地方是,当您单击草图之一时,您将被重定向到单个图像。这是通过在整个项目模板周围使用一个大的 HTML 锚标记来实现的,当单击该标记时,它会要求 Angular.js 执行重定向。

 

Angular 查看所有控制器

这是 Angular ViewAllController 的完整代码。

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);
            });
        }
    }]);

其中以下是重要部分:

  • 我们使用了之前讨论过的通用对话框服务。
  • 我们使用了 UserService。
  • 我们接受了一些依赖项,例如:
    • $logAngular.js 日志服务(您应该使用它而不是 Console.log)。
    • $windowAngular.js window 抽象。
    • $location:允许控制器更改路由。
    • loginService:处理登录的自定义服务。
    • dialogService:显示对话框(等待/错误等)的自定义服务。
    • imageBlobComment:用于获取图像 blob 和评论数据的 Angular.js $resource。
    • 控制器为视图提供了一个项目列表进行绑定。

 

ImageBlobComment 服务

imageBlobComment 是一个自定义的 Angular.js $resource 基础服务,它连接到以下 URL 的 WebApi 控制器:/api/ImageBlobComment。这是 imageBlobComment 工厂代码:

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;
}]);

可以看到,imageBlobComment 用于与 ImageBlobController Web API 控制器通信,该控制器具有通过存储库从 Azure blob 存储检索用户 blob 评论数据的各种方法。这是 Web API 控制器:

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;
            
        }
    }
}

从上面的代码可以看到,由于现在是服务器端代码,我们可以自由使用 async/await(Web API v2 完全支持)。唯一需要注意的代码是,我们还使用了 IImageBlobCommentRepository,它通过我们在上面看到的 IOC 代码注入到 Web API 控制器中。IImageBlobCommentRepository 负责从 Azure 表存储收集 blob 和评论的数据。这是 IImageBlobCommentRepository 的代码:

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;
        }
    }
}

这里有几点可以借鉴:

  1. Azure SDK 支持 async/await,所以我使用了它。
  2. Azure 表存储支持有限的 LINQ 查询,非常有限,但可以实现,所以我们通过使用自定义的 Func<T,TR> 来实现。
  3. 我们不知道存储了多少项,所以我选择了分批处理,这是通过 TableQuerySegment 类完成的,您可以在上面的代码中看到。
  4. 当我们在 Azure 表存储中删除项目时,我不想一个一个地删除,那会很愚蠢,所以我们使用了 TableBatchOperation,如上面的代码所示。
  5. Azure 表存储有一个 upsert 功能(标准 SQL 中的 MERGE),所以我们使用了它,它是 TableBatchOperation 的一个静态方法,名为 TableBatchOperation.InsertOrReplace(..)

除此之外,其他都是相当标准的。

 

查看单个工作流程

查看单个工作流程也相当直接,它只是显示一个视图并允许用户向草图添加评论。只有当您不是草图的所有者时,才能添加评论。

点击查看大图

 

ASP MVC 控制器

MVC 控制器没什么好说的,它只是为 viewsingle 路由提供初始视图模板,该路由使用 Angular.js ViewSingleController

这是它的代码:

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();
        }
    }
}

视图模板 / Angular 控制器交互

这是与 Angular.js ViewAllController 匹配的模板,就在下面。

@{
    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>

这个视图没什么好说的,实际上都是相当标准的。一些绑定而已。

 

Angular 查看单个控制器

这是 Angular ViewSingleController 的完整代码。

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);
                });
        }

    }]);


其中以下是重要部分:

  • 我们使用了之前讨论过的通用对话框服务。
  • 我们使用了 UserService。
  • 我们接受了一些依赖项,例如:
    • $logAngular.js 日志服务(您应该使用它而不是 Console.log)。
    • $windowAngular.js window 抽象。
    • $location:允许控制器更改路由。
    • $routeParams:我们使用此服务来抓取当前正在渲染的路由中的 URL 参数,这使我们能够加载正确的实体。viewsingle 路由的来源是用户在 viewall 视图中单击单个草图时。
    • loginService:处理登录的自定义服务。
    • dialogService:显示对话框(等待/错误等)的自定义服务。
    • imageBlobComment:用于获取图像 blob 和评论数据的 Angular.js $resource。

 

ImageBlobComment 服务

imageBlobComment 是一个自定义的 Angular.js $resource 基础服务,它连接到以下 URL 的 WebApi 控制器:/api/ImageBlobComment。我们已经在上面看到过它的代码。WebApi 控制器还使用了 IImageBlobCommentRepository,我们也已经在上面看到过它的代码。

 

 

就这些

这就是本系列我想说的全部内容。希望您喜欢本系列,并在学习过程中有所收获。我知道当我写文章时,我总是学到很多东西,这是我主要的学习途径。

如果你喜欢你所看到的,非常欢迎投票或评论。

© . All rights reserved.