Sketcher:N 中的二





5.00/5 (23投票s)
Angular.Js / Azure / ASP MVC / SignalR / Bootstrap 演示应用
文章系列
本文是 3 篇系列文章的一部分
- 文章 1:演示应用介绍 / Angular.js 基础
- 文章 2(本文):登录 / 订阅
- 文章 3:绘制 / 查看 / 评论草图
目录
这是本文的目录,本系列中的每篇文章都有自己的目录
引言
这是拟议的三部分系列文章中的第二部分。上次我们讨论了演示应用,看了一些截图,并谈论了一些 Angular.js 基础知识。这次我们将讨论演示应用中的一些通用基础设施,并深入探讨演示应用的两个实际工作流程。
- 登录工作流程
- 订阅管理
代码在哪里
本系列的源代码托管在 GitHub 上,可以在这里找到:
https://github.com/sachabarber/AngularAzureDemo
必备组件
在运行演示应用程序之前,你需要准备好以下几项:
- Visual Studio 2013
- Azure SDK v2.3
- Azure Emulator v2.3
- 愿意自己学习一部分内容
- 耐心
基础设施部分
本文将介绍演示应用的两个主要工作流程,但在开始之前,我想先介绍一些支持良好的 Angular.js / ASP MVC 工作流程的主要基础设施。
演示应用是 Angular.js / ASP MVC 的组合,这让我很高兴。我想充分利用 Angular.js 的丰富功能及其单页应用程序 (SPA) 功能,同时也不想放弃我已熟悉的工具。因此,下面的许多子标题将讨论如何让 Angular.js / ASP MVC 协同工作。
通用布局
ASP MVC(以及 ASP .NET)长期以来一直有一个主布局页的概念,用于创建网站的所有通用元素。我希望使用此功能,因此在演示应用中,您将在 Views/Shared 文件夹中找到一个名为“_Layout.cshtml”的文件(标准的 ASP MVC 名称和位置)。这是该文件的内容:
<!DOCTYPE html>
<!--[if lt IE 7]> <html class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]-->
<!--[if IE 7]> <html class="no-js lt-ie9 lt-ie8"> <![endif]-->
<!--[if IE 8]> <html class="no-js lt-ie9"> <![endif]-->
<!--[if gt IE 8]><!-->
<html class="no-js">
<!--<![endif]-->
<head>
<title>AngularAzureDemo</title>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<link href="~/favicon.ico" rel="shortcut icon" type="image/x-icon" />
<meta name="viewport" content="width=device-width" />
@Styles.Render("~/Content/bootstrap")
@Styles.Render("~/Content/css")
@Scripts.Render("~/bundles/modernizr")
</head>
<body data-ng-app="main" data-ng-controller="RootController">
<!--[if lt IE 7]>
<p class="chromeframe">You are using an <strong>outdated</strong> browser.
Please <a href="http://browsehappy.com/">upgrade your browser</a> or
<a href="http://www.google.com/chromeframe/?redirect=true">activate Google Chrome Frame</a>
to improve your experience.</p>
<![endif]-->
<nav class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="container-fluid">
<div class="navbar-header">
<button type="button" class="navbar-toggle"
data-toggle="collapse" data-target="#bs-example-navbar-collapse-1">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">Sketcher</a>
</div>
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav">
<li data-ng-class="{active : activeViewPath==='/login'}">
<a href="#/login">Login/Out</a>
</li>
<li data-ng-class="{active : activeViewPath==='/sketcheractions'}">
<a href="#/sketcheractions">Actions</a>
</li>
</ul>
</div>
</div>
</nav>
<div class="container">
@RenderBody()
</div>
@Scripts.Render("~/bundles/jquery")
@Scripts.Render("~/bundles/signalr")
<!--Reference the autogenerated SignalR hub script. -->
<script src="/signalr/hubs"></script>
@Scripts.Render("~/bundles/underscore")
@Scripts.Render("~/bundles/angular")
@Scripts.Render("~/bundles/bootstrap")
@Scripts.Render("~/bundles/toastr")
@Scripts.Render("~/bundles/app")
@RenderSection("scripts", required: false)
<div class="modal" id="waitModal" tabindex="-1" role="dialog"
aria-labelledby="waitModalTitle" aria-hidden="true" data-keyboard="false">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title" id="waitModalTitle">Processing...</h4>
</div>
<div class="modal-body">
<img src="~/Images/ajax-loader.GIF" />
</div>
</div>
</div>
</div>
<div class="modal" id="alertModal" tabindex="-1" role="dialog"
aria-labelledby="alertModalTitle" aria-hidden="true" data-keyboard="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">
<span aria-hidden="true">×</span><span class="sr-only">Close</span>
</button>
<h4 class="modal-title" id="alertModalTitle">See JS</h4>
</div>
<div class="modal-body">
<p id="alertModalBody">See JS</p>
</div>
</div>
</div>
</div>
</body>
</html>
可以看出,该文件包含一些值得注意的地方:
- 输出几个脚本捆绑包
- 包含 Boostrap 导航栏
- 还包含将容纳正文内容(即单页)的容器(请参阅
@RenderBody()
调用)
当 _Layout.cshtml 渲染时,它看起来像这样:
点击查看大图
捆绑包
此演示应用需要大量的 Javascript,因此创建了各种捆绑包来提供和最小化它,这些捆绑包由我们刚刚讨论的 _Layout.cshtml 页面使用。捆绑包配置如下:
using System.Web;
using System.Web.Optimization;
namespace AngularAzureDemo
{
public class BundleConfig
{
// For more information on Bundling, visit http://go.microsoft.com/fwlink/?LinkId=254725
public static void RegisterBundles(BundleCollection bundles)
{
bundles.Add(new ScriptBundle("~/bundles/jquery").Include(
"~/Scripts/jquery-{version}.js"));
bundles.Add(new ScriptBundle("~/bundles/underscore").Include(
"~/Scripts/underscore.js"));
bundles.Add(new StyleBundle("~/bundles/bootstrap").Include(
"~/Scripts/bootstrap.js"));
bundles.Add(new StyleBundle("~/bundles/signalr").Include(
"~/Scripts/jquery.signalR-2.1.1.js"));
bundles.Add(new StyleBundle("~/bundles/toastr").Include(
"~/Scripts/toastr.min.js"));
bundles.Add(new ScriptBundle("~/bundles/angular").Include(
"~/Scripts/angular.js",
"~/Scripts/angular-cookies.js",
"~/Scripts/angular-ng-grid.js",
"~/Scripts/angular-resource.js",
"~/Scripts/angular-animate.js",
"~/Scripts/angular-route.js"));
bundles.Add(new ScriptBundle("~/bundles/app").Include(
"~/Scripts/app/app.js",
"~/Scripts/app/services/*.js",
"~/Scripts/app/factories/*.js",
"~/Scripts/app/directives/*.js",
"~/Scripts/app/modules/*.js",
"~/Scripts/app/controllers/*.js"
));
// Use the development version of Modernizr to develop with and learn from.
//Then, when you're ready for production, use the build tool at
//http://modernizr.com to pick only the tests you need.
bundles.Add(new ScriptBundle("~/bundles/modernizr").Include(
"~/Scripts/modernizr-*"));
bundles.Add(new StyleBundle("~/Content/bootstrap").Include(
"~/Content/bootstrap.css",
"~/Content/bootstrap-responsive.css"
));
bundles.Add(new StyleBundle("~/Content/css").Include(
"~/Content/colorpicker.css",
"~/Content/toastr.min.css",
"~/Content/site.css",
"~/Content/ng-grid.css"));
bundles.Add(new StyleBundle("~/Content/themes/base/css").Include(
"~/Content/themes/base/jquery.ui.core.css",
"~/Content/themes/base/jquery.ui.resizable.css",
"~/Content/themes/base/jquery.ui.selectable.css",
"~/Content/themes/base/jquery.ui.accordion.css",
"~/Content/themes/base/jquery.ui.autocomplete.css",
"~/Content/themes/base/jquery.ui.button.css",
"~/Content/themes/base/jquery.ui.dialog.css",
"~/Content/themes/base/jquery.ui.slider.css",
"~/Content/themes/base/jquery.ui.tabs.css",
"~/Content/themes/base/jquery.ui.datepicker.css",
"~/Content/themes/base/jquery.ui.progressbar.css",
"~/Content/themes/base/jquery.ui.theme.css"));
}
}
}
这是 BundleConfig.cs 的全部内容。
单页
我们已经提到 _layout.cshtml 为“页面”提供了一个占位符,但这如何实现呢?好吧,这是通过 Index.cshtml 文件实现的,该文件具有以下标记。
<!-- Controller that deals with the SignalR real time push notifications-->
<div ng-controller="RealTimeNotificationsController">
</div>
<!--This is where the main views would get loaded for SPA
This page is the container page for all sub views that get loaded.
The sub views that would get loaded into ng-view would not inherit the master page,
and hence would only send relevant html fargments to render specific views.-->
<div ng-view></div>
这里有 2 个主要要点:
- 有一个具有硬编码控制器的 DIV,这意味着每个页面都具有该控制器作为标准。
- 有一个具有 Angular.js ng-view 属性的 DIV。这是“视图”将由 Angular.js 应用的路由配置在“单页”中呈现的地方,我们将在下一步中进行介绍。
Angular 路由 / ASP MVC 路由
Angular.js 附带自己的路由,允许在 ng-view 属性化的容器(在本例中为 DIV)中呈现视图。它还支持 URI 参数,以及路由服务所需的一切。
路由的处理方式通常在 Angular.js 应用程序级别,对于演示应用来说,如下所示:
// Main configuration file. Sets up AngularJS module and routes and any other config objects
var appRoot = angular.module('main',
[ 'ngRoute',
'ngAnimate',
'ngGrid',
'ngResource',
'ngCookies',
'angularAzureDemo.services',
'angularAzureDemo.factories',
'angularAzureDemo.directives',
'colorpicker.module'
]); //Define the main module
appRoot
.config(['$routeProvider', function ($routeProvider) {
//Setup routes to load partial templates from server.
//TemplateUrl is the location for the server view (Razor .cshtml view)
$routeProvider
//home routes
.when('/subscriptions', {
templateUrl: '/home/subscriptions',
controller: 'SubscriptionsController'
})
.when('/create', {
templateUrl: '/home/create',
controller: 'CreateController'
})
.when('/viewall', {
templateUrl: '/home/viewall',
controller: 'ViewAllController'
})
.when('/sketcheractions', {
templateUrl: '/home/sketcheractions',
controller: 'SketcherActionsController'
})
.when('/viewsingleimage/:id',
{
templateUrl: '/home/viewsingleimage',
controller: 'ViewSingleImageController'
}
)
//account routes
.when('/login', {
templateUrl: '/account/login',
controller: 'LoginController'
})
//default
.otherwise({ redirectTo: '/login' });
}])
.controller('RootController', ['$scope', '$route',
'$routeParams', '$location', function ($scope, $route, $routeParams, $location) {
$scope.$on('$routeChangeSuccess', function (e, current, previous) {
$scope.activeViewPath = $location.path();
});
}]);
// grab underscore from window (where it attaches itself)
appRoot.constant('_', window._);
// add on underscore to global scope
appRoot.run(function ($rootScope) {
$rootScope._ = window._;
});
这里有很多事情发生,比仅仅是路由还要多,所以我们逐一处理。
- 声明了一个新的 Angular.js 模块“main”,它接受一系列依赖项, Angular.js 的依赖注入系统会为您处理这些依赖项。
- 然后使用 Angular.js
$routeprovider
服务配置路由。您可以看到标准路由和带有路由额外参数的路由的混合。这些路由中的每一个都将调用一个 ASP MVC 控制器,该控制器将提供视图的模板。 - 我们还将 Underscore.js 辅助库(非常适合处理数组)设置为一个常量,以便在其他 Angular.js 模块中使用。Underscore.js 是一个有趣的库,因为它会附加到 window 对象,所以我们需要从那里获取它作为我们的常量,并将其设置在 angular
$rootScope
上。
所以,让我们回到第二点,我们可以看到有一个路由,如
//home routes
.when('/subscriptions', {
templateUrl: '/home/subscriptions',
controller: 'SubscriptionsController'
})
让我们来谈谈这个路由并跟踪它,看看路由是如何工作的(所有其他路由都与此类似)。有两点需要注意。
- 我们有一个名为“
Home
”的 ASP MVC 控制器,它有一个名为“subscriptions
”的动作,该动作将提供初始视图模板。using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; namespace AngularAzureDemo.Controllers { public class HomeController : Controller { public ActionResult Subscriptions() { return View(); } } }
这将简单地从标准的 MVC subscriptions 文件夹中渲染名为“Subscriptions
”的视图,其中包含视图的初始模板。 - 路由中还有一个控制器,这次不是 ASP MVC 控制器(ASP MVC 控制器仅用于提供 Angular.js 请求的初始模板),也不是 WebApi 控制器(仅用于 REST 数据......有没有感到困惑?!),而是一个 angular 控制器。是的,这是一个用于视图的 Angular.js JavaScript 控制器。因此,我们应该会有一个基于 Angular.js 的 JavaScript
SubscriptionsController
,确实有,这是它的骨架(不用太担心,现在只需要了解路由是如何工作的,当我们继续阅读文章时,其余部分可能会自然而然地出现)。angular.module('main').controller('SubscriptionsController', ['$scope', '$log', '$window', '$location', 'loginService', '$cookieStore', 'userService', 'dialogService', 'userSubscription', function ($scope, $log, $window, $location, loginService, $cookieStore, userService, dialogService, userSubscription) { ....... }]);
所以,这就是 Angular.js 和 ASP MVC 在演示应用中的路由工作方式,明白了,太棒了,我们继续。
有用提示:一位 CodeProject 用户编写了一篇关于如何将 Angular.js 与 ASP MVC 结合使用的非常出色/简单的指南,您可以在这里阅读: https://codeproject.org.cn/Articles/806029/Getting-started-with-AngularJS-and-ASP-NET-MVC-Par 如果您对 Angular.js 不熟悉,这可能会帮助您巩固一些 Angular.js 的知识。
Angular 视图动画
Angular.js 拥有一个名为 ngAnimate
的动画模块,它包含在它丰富的模块集合中,可用于为视图添加进出场动画(如果您回顾一下路由应用程序设置,您会看到演示应用程序的主模块依赖于这个 ngAnimate
模块)。
有了这个模块,您可以创建一些非常酷的动画(演示应用使用的是非常简单的动画,但这里有一些惊人的动画: http://tympanus.net/codrops/2013/05/07/a-collection-of-page-transitions/)。
您只需要这个模块和一些 CSS。这是相关的 CSS:
/* ANIMATIONS
============================================================================= */
.ng-enter {
-webkit-animation: scaleUpCenter .8s ease-out both ;
-moz-animation: scaleUpCenter .8s ease-out both;
animation: scaleUpCenter .8s ease-out both;
z-index: 8888;
}
.ng-leave {
z-index: 9999;
}
/* scale down center */
@-webkit-keyframes scaleDownCenter {
from { }
to { opacity: 0; -webkit-transform: scale(.7); }
}
@-moz-keyframes scaleDownCenter {
from { }
to { opacity: 0; -moz-transform: scale(.7); }
}@keyframes scaleDownCenter {
from { }
to { opacity: 0; transform: scale(.7); transform: scale(.7); }
}
/* scale up center */
@-webkit-keyframes scaleUpCenter {
from { opacity: 0; -webkit-transform: scale(.7); }
}
@-moz-keyframes scaleUpCenter {
from { opacity: 0; -moz-transform: scale(.7); }
}
@keyframes scaleUpCenter {
from { opacity: 0; transform: scale(.7); transform: scale(.7); }
}
为了实现“单页”过渡的动画效果,我们只需要定位以下两个标签:
- .ng-enter
- .ng-leave
Angular.js 会将这些类附加到“单页”视图上。非常简单,不是吗?
DialogService
显示“请稍候”对话框或成功/错误对话框是很常见的任务,因此我决定将其抽象为一个自定义的 angular 服务,它看起来如下,我认为它相当自explanatory:
angularAzureDemoServices.service('dialogService', ['$log', function ($log) {
this.showPleaseWait = function () {
$('#waitModal').modal('show');
}
this.hidePleaseWait = function () {
$('#waitModal').modal('hide');
}
this.showAlert = function (title, content) {
$('#alertModalTitle').text(title);
$('#alertModalBody').text(content);
$('#alertModal').modal('show');
}
}]);
WebApi IOC
为了实现 IOC 到 web api 控制器,我设计了一种安装程序风格的语法(有点像 Castle Windsor,但使用 Unity IOC 容器)。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Microsoft.Practices.Unity;
namespace AngularAzureDemo.IOC
{
public interface IUnityInstaller
{
void Install(IUnityContainer container);
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using AngularAzureDemo.DomainServices;
using Microsoft.Practices.Unity;
namespace AngularAzureDemo.IOC
{
public static class UnityContainerExtensions
{
public static void Install(this IUnityContainer container, IUnityInstaller installer)
{
installer.Install(container);
}
}
}
using Microsoft.Practices.Unity;
using System;
using System.Collections.Generic;
using System.Web.Http.Dependencies;
public class UnityResolver : IDependencyResolver
{
protected IUnityContainer container;
public UnityResolver(IUnityContainer container)
{
if (container == null)
{
throw new ArgumentNullException("container");
}
this.container = container;
}
public object GetService(Type serviceType)
{
try
{
return container.Resolve(serviceType);
}
catch (ResolutionFailedException)
{
return null;
}
}
public IEnumerable<object> GetServices(Type serviceType)
{
try
{
return container.ResolveAll(serviceType);
}
catch (ResolutionFailedException)
{
return new List<object>();
}
}
public IDependencyScope BeginScope()
{
var child = container.CreateChildContainer();
return new UnityResolver(child);
}
public void Dispose()
{
container.Dispose();
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using AngularAzureDemo.DomainServices;
using Microsoft.Practices.Unity;
namespace AngularAzureDemo.IOC
{
public class WebApiInstaller : IUnityInstaller
{
public void Install(IUnityContainer container)
{
container.RegisterType<IUserSubscriptionRepository, UserSubscriptionRepository>(
new HierarchicalLifetimeManager());
container.RegisterType<IImageBlobRepository, ImageBlobRepository>(
new HierarchicalLifetimeManager());
container.RegisterType<IImageBlobCommentRepository, ImageBlobCommentRepository>(
new HierarchicalLifetimeManager());
}
}
}
这一切都在 global.asax.cs 中进行了如下连接:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Http;
using System.Web.Mvc;
using System.Web.Optimization;
using System.Web.Routing;
using AngularAzureDemo.DomainServices;
using AngularAzureDemo.IOC;
using Microsoft.Practices.Unity;
namespace AngularAzureDemo
{
public class MvcApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
.....
.....
.....
//IOC
var container = new UnityContainer();
container.Install(new WebApiInstaller());
//set IOC resolver
GlobalConfiguration.Configuration.DependencyResolver = new UnityResolver(container);
}
}
}
登录工作流程
本节概述了演示应用的 login
工作流程的工作方式及其外观。
点击查看大图
登录工作流程如下:
- 有一个静态用户列表,您必须从中选择一个用户进行登录。该静态列表中的所有其他用户将立即成为您的朋友。
理想情况下,我本来希望使用 Facebook OpenAuthentication 进行登录并获取声明令牌,然后使用 Facebook SDK 为当前声明令牌获取朋友列表,但在工作场所(我在此处撰写部分内容,午餐时间)无法使用 Facebook,因此最终选择了静态用户列表。
ASP MVC 控制器
MVC 控制器没什么可说的,它只是提供登录路由的初始视图模板,该模板使用 Angular.js LoginController
。这是代码:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
namespace AngularAzureDemo.Controllers
{
public class AccountController : Controller
{
public ActionResult Login()
{
return View();
}
}
}
视图模板 / Angular 控制器交互
这是与 Angular.js LoginController
对应的模板,就在后面。
@{
Layout = null;
}
<div class="row">
<div>
<br />
<p>This is a small demo app demonstrating the following technolgies all working together</p>
<br />
<img src="~/Images/banners.png"
class="img-responsive"
alt="Responsive image">
</div>
</div>
<br />
<br />
<div class="well">
<div class="row">
<div class="col-xs-12 col-sm-8 col-md-8" ng-show="!isLoggedIn">
<h2>Login</h2>
<p>Please pick a user to log in as</p>
<div class="row">
<p class="col-xs-12 col-sm-8 col-md-8">
<select class="form-control"
data-ng-options="o.Name for o in usersList"
data-ng-model="selectedPerson"
ng-disabled="isLoggedIn"></select>
</p>
<div class="col-xs-12 col-sm-4 col-md-4">
<button type="button" id="btnLogin"
class="btn btn-primary"
data-ng-click="login()"
ng-disabled="isLoggedIn">LOG IN</button>
</div>
</div>
</div>
<div class="col-xs-12 col-sm-8 col-md-8" ng-show="isLoggedIn">
<h2>Logout</h2>
<p>You are logged in as : <span ng-bind="selectedPerson.Name"></span></p>
<div class="row">
<p class="col-xs-12 col-sm-4 col-md-4">
<button type="button"
id="btnLogout"
class="btn btn-primary"
data-ng-click="logout()"
ng-disabled="!isLoggedIn">LOG OUT</button>
</p>
</div>
</div>
</div>
</div>
Angular 登录控制器
这是 Angular Login
控制器的完整代码:
appRoot.controller('LoginController', ['$scope', '$log', '$location', '$resource',
'$window', 'loginService', 'dialogService','userService', '_',
function ($scope, $log, $location, $resource, $window,
loginService, dialogService, userService, _) {
$scope.usersList = [];
$scope.selectedPerson = null;
$scope.isLoggedIn = false;
$log.log("logged in " + loginService.isLoggedIn());
dialogService.showPleaseWait();
getAllPeople();
$scope.login = function () {
loginService.login($scope.selectedPerson);
$scope.isLoggedIn = true;
$location.path("sketcheractions");
};
$scope.logout = function () {
$scope.selectedPerson = null;
loginService.logout();
$scope.isLoggedIn = false;
};
function getPersonFromList(userName) {
$scope.selectedPerson = _.findWhere($scope.usersList,
{ Name: userName });
}
function getAllPeople() {
userService.getAll()
.success(function (users) {
$scope.usersList = users;
if (loginService.isLoggedIn()) {
getPersonFromList(loginService.currentlyLoggedInUser().Name);
$scope.isLoggedIn = true;
$log.log("selected person " + $scope.selectedPerson.Name);
}
dialogService.hidePleaseWait();
})
.error(function (error) {
dialogService.hidePleaseWait();
$window.alert('Error', 'Unable to load user data: ' + error.message);
});
}
}]);
以下是重要部分:
- 我们使用了之前讨论过的通用对话框服务。
- 我们使用了 UserService。
- 我们接受了几个依赖项,例如:
$log
:Angular.js 日志记录服务(您应该使用它而不是 Console.log)。$location
:允许控制器更改路由。$window
:Angular.js 窗口抽象。loginService
:用于处理登录的自定义服务。dialogService
:用于显示对话框(等待/错误等)的自定义服务。userService
:用于获取用户数据的 Angular.js $resource。- _:这是 underscore 库(用于处理数组的有用函数)。
我想深入研究的两个主要内容是 loginService
和 userService
。
LoginService
这是一个非常简单的身份验证服务(极其粗糙,正如我所说,我理想情况下会使用 Facebook 开放身份验证),它能够存储已登录的用户,并能告诉调用者当前是否有已登录的用户。
以下是相关代码
angularAzureDemoServices.service('loginService', ['$log', function ($log) {
this.loggedInUser = null;
this.login = function (currentUser) {
this.loggedInUser = currentUser;
$log.log('Logged in user ' + this.loggedInUser.name);
}
this.logout = function () {
this.loggedInUser = null;
$log.log('User has been logged out');
}
this.isLoggedIn = function() {
return typeof this.loggedInUser !== 'undefined' && this.loggedInUser != null;
}
this.currentlyLoggedInUser = function () {
return this.loggedInUser;
}
}]);
该服务被广泛使用,通常在 Angular.js 控制器的开头使用,用于检查当前是否有已登录用户,如果没有,控制器将重定向到“Login”路由。
UserService
UserService
是一个自定义的 Angular.js $resource 类服务,它与以下 URL 的 WebApi 控制器通信:/api/User
。这是 UserService
代码。
// http://weblogs.asp.net/dwahlin/using-an-angularjs-factory-to-interact-with-a-restful-service
angularAzureDemoServices.service('userService',
['$http', '$window', function ($http, $window) {
var urlBase = '/api/user';
this.getAll = function () {
return $http.get(urlBase);
};
this.getFriends = function (id) {
return $http.get(urlBase + '/' + id);
};
}]);
这是相关的 WebApiUserController 代码。
using System.Collections.Generic;
using System.Linq;
using System.Web.Http;
using AngularAzureDemo.Models;
namespace AngularAzureDemo.Controllers
{
/// <summary>
/// API controller to manage users
/// </summary>
public class UserController : ApiController
{
private readonly Users users;
public UserController()
{
users = new Users();
}
// GET api/user
public IEnumerable<User> Get()
{
// Return a static list of people
return users;
}
// GET api/user/5
[System.Web.Http.HttpGet]
public IEnumerable<User> Get(int id)
{
//return static list of people which do not include the current person
return users.Where(x => x.Id != id);
}
}
}
它使用了以下辅助类来提供实际的静态用户列表:
using System.Collections.Generic;
namespace AngularAzureDemo.Models
{
public class Users : List<User>
{
public Users()
{
this.Add(new User{Id=1, Name="Sacha Barber"});
this.Add(new User{Id=2, Name="Adam Gril"});
this.Add(new User{Id=3, Name="James Franklin"});
this.Add(new User{Id=4, Name="Vicky Merry" });
this.Add(new User{Id=5, Name="Cena Rego"});
}
}
}
订阅管理工作流程
本节概述了演示应用的 subscriptions
工作流程的工作方式及其外观。
点击查看大图
订阅工作流程如下:
- 有一个静态用户列表,所有这些用户都是您的直系朋友,除了您选择登录的用户。其余用户可供您选择,作为您希望在他们创建新草图时接收实时通知的朋友。
- 订阅可以从 UI 添加/删除,然后将它们存储在 Azure 表存储中。
正如我之前所说,我本来希望使用 Facebook OpenAuthentication 进行登录并获取声明令牌,然后使用 Facebook SDK 为当前声明令牌获取朋友列表,但在工作场所(我在此处撰写部分内容,午餐时间)无法使用 Facebook,因此最终选择了静态用户列表。
ASP MVC 控制器
MVC 控制器没什么可说的,它只是提供 subscriptions 路由的初始视图模板,该模板使用 Angular.js SubscriptionsController
。这是代码:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
namespace AngularAzureDemo.Controllers
{
public class HomeController : Controller
{
public ActionResult Subscriptions()
{
return View();
}
}
}
视图模板 / Angular 控制器交互
这是与 Angular.js SubscriptionsController
对应的模板,就在后面。
@{
Layout = null;
}
<div ng-show="hasSubscriptions">
<br />
<div class="well">
<h2>Subscriptions</h2>
<div class="row">
<div class="col-xs-12 col-sm-8 col-md-8">
<table class="table table-striped table-condensed">
<tr>
<th>Id</th>
<th>Name</th>
<th>Subscription Active</th>
</tr>
<tbody ng:repeat="friendsSubscription in allFriendsSubscriptions">
<tr>
<td>{{friendsSubscription.Id}}</td>
<td>{{friendsSubscription.Name}}</td>
<td><input type="checkbox"
ng-model="friendsSubscription.IsActive"></td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="row">
<div class="col-xs-12 col-sm-4 col-md-4">
<button type="button"
class="btn btn-primary"
data-ng-click="updateSubscriptions()">UPDATE SUBSCRIPTIONS</button>
</div>
</div>
</div>
</div>
Angular 订阅控制器
这是 Angular Subscriptions
控制器的完整代码,没有什么太多可说的,我们获取了完整的的朋友列表和所有已存储的订阅,然后创建一个新的对象投影,该对象将根据您是否为朋友列表中的用户存储了订阅来显示开/关标志。
如果修改了订阅,它们将被存储在 cookie 中,这是通过使用标准的 Angular.js $cookieStore
服务完成的。
angular.module('main').controller('SubscriptionsController',
['$scope', '$log', '$window', '$location', 'loginService', '$cookieStore',
'userService', 'dialogService', 'userSubscription',
function ($scope, $log, $window, $location, loginService, $cookieStore,
userService, dialogService, userSubscription) {
if (!loginService.isLoggedIn()) {
$location.path("login");
}
$scope.storedSubscriptions = [];
$scope.allFriendsSubscriptions = [];
$log.log('Logged in user Id : ', loginService.currentlyLoggedInUser().Id);
$scope.hasSubscriptions = false;
dialogService.showPleaseWait();
getAllFriends(loginService.currentlyLoggedInUser().Id);
function getAllFriends(id) {
userService.getFriends(id)
.success(function (friends) {
$log.log('friends count : ', friends.length);
$scope.storedSubscriptions = [];
for (var i = 0; i < friends.length; i++) {
friends[i].IsActive = false;
$scope.storedSubscriptions.push(friends[i]);
}
//get all actual stored subscriptions
getAllSubscriptions(id);
})
.error(function (error) {
dialogService.hidePleaseWait();
dialogService.showAlert('Error',
'Unable to load friend data: ' + error.message);
});
}
function getAllSubscriptions(id) {
userSubscription.get({ id: id }, function (result) {
var savedSubscriptions = result.Subscriptions;
$log.log('subscription count : ', savedSubscriptions.length);
for (var i = 0; i < savedSubscriptions.length; i++) {
var friendSubscription = _.findWhere($scope.storedSubscriptions,
{
Id: savedSubscriptions[i].FriendId
});
if (typeof friendSubscription !== 'undefined' && friendSubscription != null) {
friendSubscription.IsActive = true;
} else {
$log.log('could not find friend', savedSubscriptions[i].FriendId);
}
}
$scope.allFriendsSubscriptions = $scope.storedSubscriptions;
$cookieStore.put('allFriendsSubscriptions', $scope.allFriendsSubscriptions);
$scope.hasSubscriptions = true;
dialogService.hidePleaseWait();
}, function (error) {
dialogService.hidePleaseWait();
dialogService.showAlert('Error',
'Unable to load subscription data: ' + error.message);
});
}
$scope.updateSubscriptions = function () {
dialogService.showPleaseWait();
$log.log('Updating the subscriptions');
var subscriptionsToSave = [];
for (var i = 0; i < $scope.allFriendsSubscriptions.length; i++) {
subscriptionsToSave.push(
{
"UserId": loginService.currentlyLoggedInUser().Id,
"FriendId": $scope.allFriendsSubscriptions[i].Id,
"IsActive": $scope.allFriendsSubscriptions[i].IsActive
});
}
$log.log('subscriptionsToSave', subscriptionsToSave);
var userSubscriptions = {
Subscriptions : subscriptionsToSave
}
userSubscription.save((userSubscriptions), function (result) {
$log.log('saveSubscriptions result : ', result);
if (result) {
dialogService.hidePleaseWait();
dialogService.showAlert('Success', 'Successfully saved all subscriptions');
} else {
dialogService.hidePleaseWait();
$window.alert('Unable to save subscription data');
dialogService.showAlert('Error', 'Unable to save subscription data');
}
}, function (error) {
dialogService.hidePleaseWait();
dialogService.showAlert('Error',
'Unable to save subscription data: ' + error.message);
});
};
}]);
此控制器还使用了两个自定义的 Angular.js 服务/工厂,即:
- UserService
- UserSubscription Factory
我们将在接下来的部分中介绍它们。
UserService
这是一个 Angular.js 自定义 $resource
,用于与标准 web api 控制器通信。
这是自定义的 Angular.js UserService
的完整代码。
angularAzureDemoServices.service('userService',
['$http', '$window', function ($http, $window) {
var urlBase = '/api/user';
this.getAll = function () {
return $http.get(urlBase);
};
this.getFriends = function (id) {
return $http.get(urlBase + '/' + id);
};
}]);
可以看出,此 UserService
用于与 user
web api 控制器通信,该控制器仅返回所有用户列表,如下所示:
using System.Collections.Generic;
using System.Linq;
using System.Web.Http;
using AngularAzureDemo.Models;
namespace AngularAzureDemo.Controllers
{
/// <summary>
/// API controller to manage users
/// </summary>
public class UserController : ApiController
{
private readonly Users users;
public UserController()
{
users = new Users();
}
// GET api/user
public IEnumerable<User> Get()
{
// Return a static list of people
return users;
}
// GET api/user/5
[System.Web.Http.HttpGet]
public IEnumerable<User> Get(int id)
{
//return static list of people which do not include the current person
return users.Where(x => x.Id != id);
}
}
}
UserSubscription Factory
这是一个 Angular.js 自定义 $resource
,用于与标准 web api 控制器通信。
这是自定义的 Angular.js UserSubscription
工厂的完整代码。
angularAzureDemoFactories.factory('userSubscription', ['$resource', function ($resource) {
var urlBase = '/api/usersubscription/:id';
return $resource(
urlBase,
{ id: "@id" },
{
"save": { method: "POST", isArray: false }
});
}]);
可以看出,此 UserSubscription
用于与 usersubscription
web api 控制器通信,该控制器具有用于通过存储库从 Azure 表存储中保存/检索用户订阅数据的各种方法。这是 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 manage user subscriptions
/// </summary>
public class UserSubscriptionController : ApiController
{
private readonly IUserSubscriptionRepository userSubscriptionRepository;
public UserSubscriptionController(IUserSubscriptionRepository userSubscriptionRepository)
{
this.userSubscriptionRepository = userSubscriptionRepository;
}
// GET api/usersubscription/5
[System.Web.Http.HttpGet]
public async Task<UserSubscriptions> Get(int id)
{
if (id <= 0)
return new UserSubscriptions();
// Return a static list of people
var subscriptions = await userSubscriptionRepository.FetchSubscriptions(id);
UserSubscriptions userSubscriptionsToSave = new UserSubscriptions();
userSubscriptionsToSave.Subscriptions = subscriptions.ToList();
return userSubscriptionsToSave;
}
// POST api/usersubscription/....
[System.Web.Http.HttpPost]
public async Task<bool> Post(UserSubscriptions userSubscriptions)
{
var subscriptions = userSubscriptions.Subscriptions;
if (!subscriptions.Any())
return false;
int id = subscriptions[0].UserId;
if (subscriptions.Any(x => x.UserId != id))
return false;
// remove all subscriptions that user chose to remove
var subscriptionsToDelete = subscriptions.Where(x => !x.IsActive).ToList();
if (subscriptionsToDelete.Any())
{
await userSubscriptionRepository.RemoveSubscriptions(subscriptionsToDelete);
}
// add all subscriptions that user now has active
var subscriptionsToAdd = subscriptions.Where(x => x.IsActive).ToList();
if (subscriptionsToAdd.Any())
{
await userSubscriptionRepository.AddSubscriptions(subscriptionsToAdd);
}
return true;
}
}
}
可以看出,由于这是服务器端代码,我们可以自由使用 async/await(web api v2 完全支持)。要在此代码中注意的唯一其他事项是,我们还使用了 UserSubscriptionRepository
,它通过我们上面看到的 IOC 代码注入到 web api 控制器中。
UserSubscriptionRepository
负责与 Azure 表存储通信。完整的代码如下:
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
using System.Web;
using AngularAzureDemo.Azure.TableStorage;
using AngularAzureDemo.Models;
using Microsoft.WindowsAzure.Storage;
using Microsoft.WindowsAzure.Storage.Table;
using Microsoft.WindowsAzure.Storage.Table.Queryable;
namespace AngularAzureDemo.DomainServices
{
public interface IUserSubscriptionRepository
{
Task<bool> AddSubscriptions(IEnumerable<UserSubscription> subscriptionsToAdd);
Task<IEnumerable<UserSubscription>> FetchSubscriptions(int userId);
Task<bool> RemoveSubscriptions(IEnumerable<UserSubscription> subscriptionsToRemove);
}
public class UserSubscriptionRepository : IUserSubscriptionRepository
{
private readonly string azureStorageConnectionString;
private readonly CloudStorageAccount storageAccount;
public UserSubscriptionRepository()
{
azureStorageConnectionString =
ConfigurationManager.AppSettings["azureStorageConnectionString"];
storageAccount = CloudStorageAccount.Parse(azureStorageConnectionString);
}
public async Task<bool> AddSubscriptions(IEnumerable<UserSubscription> subscriptionsToAdd)
{
CloudTableClient tableClient = storageAccount.CreateCloudTableClient();
CloudTable userSubscriptionsTable =
tableClient.GetTableReference("userSubscriptions");
var tableExists = await userSubscriptionsTable.ExistsAsync();
if (!tableExists)
{
await userSubscriptionsTable.CreateIfNotExistsAsync();
}
TableBatchOperation batchOperation = new TableBatchOperation();
foreach (var subscription in subscriptionsToAdd)
{
UserSubscriptionEntity userSubscriptionEntity =
new UserSubscriptionEntity(subscription.UserId, subscription.FriendId);
batchOperation.InsertOrReplace(userSubscriptionEntity);
}
await userSubscriptionsTable.ExecuteBatchAsync(batchOperation);
return true;
}
public async Task<IEnumerable<UserSubscription>> FetchSubscriptions(int userId)
{
CloudTableClient tableClient = storageAccount.CreateCloudTableClient();
CloudTable userSubscriptionsTable = tableClient.GetTableReference("userSubscriptions");
var tableExists = await userSubscriptionsTable.ExistsAsync();
if (!tableExists)
{
return new List<UserSubscription>();
}
List<UserSubscriptionEntity> activeUserSubscriptionEntities = new List<UserSubscriptionEntity>();
Expression<Func<UserSubscriptionEntity, bool>> filter =
(x) => x.PartitionKey == userId.ToString();
Action<IEnumerable<UserSubscriptionEntity>> processor =
activeUserSubscriptionEntities.AddRange;
await ObtainUserSubscriptionEntities(userSubscriptionsTable, filter, processor);
var userSubscriptions = activeUserSubscriptionEntities.Select(x => new UserSubscription()
{
UserId = int.Parse(x.PartitionKey),
FriendId = int.Parse(x.RowKey),
IsActive = true
}).ToList();
return userSubscriptions;
}
public async Task<bool> RemoveSubscriptions(IEnumerable<UserSubscription> subscriptionsToRemove)
{
CloudTableClient tableClient = storageAccount.CreateCloudTableClient();
CloudTable userSubscriptionsTable = tableClient.GetTableReference("userSubscriptions");
var tableExists = await userSubscriptionsTable.ExistsAsync();
if (!tableExists)
{
return false;
}
List<UserSubscriptionEntity> activeUserSubscriptionEntities = new List<UserSubscriptionEntity>();
Expression<Func<UserSubscriptionEntity, bool>> filter =
(x) => x.PartitionKey == subscriptionsToRemove.First().UserId.ToString();
Action<IEnumerable<UserSubscriptionEntity>> processor = activeUserSubscriptionEntities.AddRange;
await ObtainUserSubscriptionEntities(userSubscriptionsTable, filter, processor);
TableBatchOperation deletionBatchOperation = new TableBatchOperation();
foreach (var userSubscription in subscriptionsToRemove)
{
var entity = activeUserSubscriptionEntities.SingleOrDefault(
x => x.PartitionKey == userSubscription.UserId.ToString() &&
x.RowKey == userSubscription.FriendId.ToString());
if (entity != null)
{
deletionBatchOperation.Add(TableOperation.Delete(entity));
}
}
if (deletionBatchOperation.Any())
{
await userSubscriptionsTable.ExecuteBatchAsync(deletionBatchOperation);
}
return true;
}
private async Task<bool> ObtainUserSubscriptionEntities(
CloudTable userSubscriptionsTable,
Expression<Func<UserSubscriptionEntity, bool>> filter,
Action<IEnumerable<UserSubscriptionEntity>> processor)
{
TableQuerySegment<UserSubscriptionEntity> segment = null;
while (segment == null || segment.ContinuationToken != null)
{
var query = userSubscriptionsTable
.CreateQuery<UserSubscriptionEntity>()
.Where(filter)
.AsTableQuery();
segment = await query.ExecuteSegmentedAsync(segment == null ?
null : segment.ContinuationToken);
processor(segment.Results);
}
return true;
}
}
}
这里有几个要点:
- Azure SDK 支持 async/await,所以我使用了它。
- Azure 表存储支持有限的 LINQ 查询,非常有限,但可以做到,所以我们通过使用自定义的
Func<T,TR>
来实现。 - 我们不知道存储了多少项,所以我选择分批处理,这是通过
TableQuerySegment
类完成的,您可以在上面的代码中看到。 - 当从 Azure 表存储中删除项时,我不想逐个删除,那样太傻了,所以我们使用了
TableBatchOperation
,如上面的代码所示。 - Azure 表存储有一个 upsert 类功能(标准 SQL 中的 MERGE),所以我们使用了它,它是
TableBatchOperation
的一个静态方法,称为TableBatchOperation.InsertOrReplace(..)
。
除了这些点之外,其他都相当标准。
下一步
哇,这篇文章到此结束。松一口气。
在本篇文章中,我们开始剖析演示应用,并研究了一些通用基础设施,讨论了登录和订阅工作流程。
下次,我们将完成对演示应用的剖析,并将重点介绍以下 4 个工作流程:
- 创建草图
- 查看所有草图
- 查看单个草图
- 实时通知(另一个用户在不同的浏览器/会话中创建草图)
就这些
这就是本文我想说的全部。我想您可能会比第一部分更喜欢这一篇,因为它包含了一些实际的代码。
如果你喜欢你所看到的,非常欢迎投票或评论。