LiteDispatch - 云端物流解决方案





5.00/5 (10投票s)
使用 SQL-CE、Azure SQL、移动服务、SignalR、EF、WebAPI 并集成必应地图的 Azure 网站和 W8 商店应用
- 挑战 5 - 包含 Windows 应用商店应用 - 需要必应地图 SDK 和 Windows 8
- 挑战 4 - LiteDispatch 源代码 - 4.5 MB
- Azure 网站比赛: http://litedispatch.azurewebsites.net/
用户:codeproject
密码:litedispatch
介绍
如今,为 Microsoft Azure 平台开发应用程序变得越来越容易。从人力和财力方面来看,开发团队在此类环境中开发应用程序的成本相对较低;启动成本在过去几年中不断下降,现在可以非常直接地构建一个功能齐全的原型应用程序,部署成本几乎为零或很低。此外,像与 GitHub 的集成这样的新集成功能为团队提供了一套强大的工具;配置自动化构建和部署到公共网站等任务只需几分钟。
本文讨论了 Azure 平台为开发团队带来的好处,涵盖了与 GitHub 的集成、自动构建和部署等主题;从使用 SQL-CE、SQL Express 和 SQL Azure 并结合 EF 或/和 Nhibernate 的数据持久性选项;Azure Websites 和 Azure Mobile Services 的开发。
Windows Azure 挑战
每个挑战阶段均提供的当前部分
第一次挑战 | 入门 | 4月15日 - 5月3日 |
第二次挑战 | 构建网站 | 4月29日 - 5月12日 |
第二次挑战 - 特别奖 | LiteDispatch 复活节彩蛋 | 4月29日 - 5月12日 |
第三次挑战 | 在 Azure 上使用 SQL | 5月13日 - 5月26日 |
第四次挑战 | 虚拟机 | 5月27日 - 6月9日 |
第五次挑战 - 最后一项 | 移动访问 | 6月10日 - 6月24日 |
观看 LiteTracker 视频
背景
我最近为一个客户完成了一个概念验证项目,这可能对你们中的许多人来说是一个非常常见的情况。该客户负责管理一个非常大的食品购物中心的调度;他们每天都会收到许多不同承运商的大量货物。这些货物然后被运送到各个商店,并在每天结束时,为每个商店生成最终的配送单,以便支付每日运费。
目前,客户只有用于会计目的的财务软件包系统,但其余流程均通过纸质完成。客户正在寻找一个系统来帮助他们处理运输事宜,该系统将改进商店送货单的生成,并自动馈送到其财务系统。
选择云端应用程序的几个有利方面包括:
- 承运商应生成每日送货清单,Web 应用程序将方便承运商自己输入这些清单。
- 平板电脑将分配给员工,用于验证清单和签发商店送货单。再次,一个在线应用程序比安装无线网络来支持移动用户更受欢迎。
Windows Azure 平台似乎是一个更合适的解决方案且成本效益更高。它消除了客户获得新专业知识的需要,并降低了项目的初始成本。Azure 提供非常可靠的服务,并与项目开发团队使用的工具很好地集成。为了演示目的,在很短的时间内成功开发了一个原型,没有任何成本。此外,开发团队不在同一个国家,客户也不同,因此基于云的平台在改进和修复方面非常有成效。
本文的示例应用程序 LiteDispatch 基于此项目。
路线图
我计划在本文的执行过程中进行以下阶段,以符合挑战计划:
- 构建网站原型
在此阶段,应该有一个 ASP MVC 应用程序,具有一些基本功能,以演示承运商输入调度详细信息的流程;它将是一个原型,所以此时我们专注于使用 Azure 中的网站设置环境,集成到 GitHub 存储库,并实现一些 UI 功能。它还将讨论如何使用 EF 和 SQL-CE 实现简单的身份验证。 - 集成到 SQL Azure
此阶段将讨论设置业务域并使用 EF 开发持久性层。首先,它将涵盖与 SQL-CE 的集成,并在文章结尾展示如何将数据库迁移到 SQL Azure。 - 虚拟机
这是我在 Azure 中从未用过的新方面。有几个想法,例如安装 SQL-Express,然后查看它与 Web 应用的集成情况。我还对生成 PDF 格式的报告感兴趣,这可能是使用虚拟机解决的理想问题。 - 移动访问
在此阶段需要涵盖两个方面;首先,确保 Web 应用程序在平板电脑上正常工作。第二个方面是每次承运商创建新的送货清单时,集成服务器电子邮件通知。
解决方案工件及更多
LiteDispatch 已部署在以下 Azure 网站上
http://litedispatch.azurewebsites.net/
要使用该网站,您需要登录屏幕具有以下凭据
用户: codeproject
密码: litedispatch
源代码可以在 LiteDispatch GitHub 存储库 找到,最新源代码可在 此处 获取
构建网站
网站基础设施
创建 GitHub 存储库
在 GitHub 上创建一个帐户,然后安装 GitHub for Windows
然后,从 GitHub 客户端添加一个新的存储库并发布它
现在,您只需将项目复制到本地存储库,本文附带的源代码可用于测试目的。在本地运行应用程序后,将更改提交到 GitHub
创建与 GitHub 集成的 Azure 网站
您需要一个 Azure 帐户,如果您还没有,可以查看 免费试用。登录门户后,通过选择 New>WebSite>QuickCreate 来创建新网站
现在,您需要输入您的网站名称和首选 Azure 区域,例如
网站创建后,返回主网站页面并选择“Set up deployment from source code control”,然后选择 GitHub,它将要求您的 GitHub 凭据
然后选择要部署到 Azure 网站的存储库和分支
此时,Azure 会自动部署最新的源代码。如果一切顺利,您应该在部署选项卡下看到类似以下内容
现在您应该可以在浏览器中打开网站了
此时,如果您进行任何代码更改并将其推送到 GitHub,您可以在几分钟内看到最新版本在 Azure 中部署。不算太糟,创建免费试用帐户并运行您的网站可能只需要不到 10 分钟。
已部署技术
此时 LiteDispatch 使用以下技术
- ASP MVC 4
- 使用简单成员资格和角色提供程序的表单身份验证
- 已配置为连接到 SQL-CE 的 Entity Framework
ASP MVC4 和 Azure
如截图所示,我使用 Visual Studio MVC4 模板创建了 LiteDispatch 项目,此模板中的所有内容在 Azure 上部署时似乎都能正常工作。如前所述,该解决方案启用了 NuGet 包还原,它在 Azure 中无需任何额外配置即可正常工作,这是一个优点,因为它极大地减少了部署时间和存储库的大小。
身份验证 - 简单成员资格
为了进行身份验证,我在 MVC 4 中使用了简单成员资格。这样,承运商在获得网站访问权限之前需要获得用户名和密码。还为这类用户创建了一个承运商角色。
Visual Studio 中的默认 MVC4 模板默认设置为使用简单成员资格,web.config 配置如下,设置了角色和成员资格管理器:
...
<roleManager enabled="true" defaultProvider="SimpleRoleProvider">
<providers>
<clear />
<add name="SimpleRoleProvider" type="WebMatrix.WebData.SimpleRoleProvider, WebMatrix.WebData" />
</providers>
</roleManager>
<membership defaultProvider="SimpleMembershipProvider">
<providers>
<clear />
<add name="SimpleMembershipProvider" type="WebMatrix.WebData.SimpleMembershipProvider, WebMatrix.WebData" />
</providers>
</membership>
</system.web>
在 web.config 中,身份验证设置为 Forms。
<authentication mode="Forms">
<forms loginUrl="~/Account/Login" timeout="2880" />
</authentication>
在 Account
控制器中,登录逻辑非常直接
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public ActionResult Login(LoginModel model, string returnUrl)
{
if (ModelState.IsValid && WebSecurity.Login(model.UserName, model.Password, persistCookie: model.RememberMe))
{
return RedirectToLocal(returnUrl);
}
// If we got this far, something failed, redisplay form
ModelState.AddModelError("", "The user name or password provided is incorrect.");
return View(model);
}
WebSecurity
类属于 WebMatrix.WebData
程序集,该程序集管理简单成员资格。
需要注意的一个方面是应用于控制器的过滤器:InitializeSimpleMembership
。它确保 EF 上下文已初始化
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public sealed class InitializeSimpleMembershipAttribute : ActionFilterAttribute
{
private static SimpleMembershipInitializer _initializer;
private static object _initializerLock = new object();
private static bool _isInitialized;
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
// Ensure ASP.NET Simple Membership is initialized only once per app start
LazyInitializer.EnsureInitialized(ref _initializer, ref _isInitialized, ref _initializerLock);
}
private class SimpleMembershipInitializer
{
public SimpleMembershipInitializer()
{
Database.SetInitializer<UsersContext>(null);
try
{
using (var context = new UsersContext())
{
if (!context.Database.Exists())
{
// Create the SimpleMembership database without Entity Framework migration schema
((IObjectContextAdapter)context).ObjectContext.CreateDatabase();
}
}
WebSecurity.InitializeDatabaseConnection("SecurityDb", "UserProfile", "UserId", "UserName", autoCreateTables: true);
}
catch (Exception ex)
{
...
}
}
}
}
Entity Framework 和 SQL-CE
目前只有身份验证实体被持久化到后端数据库,其余业务实例尚未就位,屏幕上使用的是模拟的 MVC Models 实例。
该解决方案使用 EF Code First 来管理 UserProfile
实体的持久化和 CRUD 操作,该实体在 AccountModel.cs
文件中声明。
[Table("UserProfile")]
public class UserProfile
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int UserId { get; set; }
public string UserName { get; set; }
public string EmailAddress { get; set; }
}
EmailAddress
字段是默认模型的一个扩展属性,因此您可以看到如何根据需要扩展模型。例如,可以添加一个属性来提供用户与他工作的承运商之间的链接。
在此文件中还声明了一个 EF DbContext
。
public class UsersContext : DbContext
{
public UsersContext()
: base("LiteDispatch.Security")
{
}
public DbSet<UserProfile> UserProfiles { get; set; }
}
请注意,传递的连接字符串与 web.config 中声明的字符串匹配。
<connectionStrings>
<add name="SecurityDb" providerName="System.Data.SqlServerCe.4.0" connectionString="Data Source=|DataDirectory|\LiteDispatch.Security.sdf;default lock timeout=60000" />
</connectionStrings>
除了 Entity Framework 的默认包外,还向解决方案添加了以下包,以便可以使用 SQL-CE 数据库:
安装上述包后,web.config 将修改为如下与 SQL-CE 配合使用:
<entityFramework>
<defaultConnectionFactory type="System.Data.Entity.Infrastructure.SqlCeConnectionFactory, EntityFramework">
<parameters>
<parameter value="System.Data.SqlServerCe.4.0" />
</parameters>
</defaultConnectionFactory>
</entityFramework>
<system.data>
<DbProviderFactories>
<remove invariant="System.Data.SqlServerCe.4.0" />
<add name="Microsoft SQL Server Compact Data Provider 4.0" invariant="System.Data.SqlServerCe.4.0" description=".NET Framework Data Provider for Microsoft SQL Server Compact" type="System.Data.SqlServerCe.SqlCeProviderFactory, System.Data.SqlServerCe, Version=4.0.0.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91" />
</DbProviderFactories>
</system.data>
您可能还想安装 Visual Studio SQL-CE Toolbox 扩展。
我们将在下一篇文章中详细介绍应用程序的持久化方面,但值得一提的是,EF 迁移已启用,并且数据库预填充了两个用户:admin 和 codeproject。
LiteDispatch 功能
此时可用的功能如下:
- 用户身份验证
- 新调度单
- 调度查询
- 调度详情视图
- 调度详情打印
- 发布说明
用户身份验证
我们已经在上一节中涵盖了这一点,但有一点需要提及:如何从头开始生成数据库。该项目已启用 EF 迁移设置,我们可以在程序包管理器控制台中运行 Update-Database -force
命令。
这对于创建数据库和表模式很有用,它还执行数据库种子,您可以对其进行自定义。在 LiteDispatch 的情况下,调用 Update 命令时使用 Configuration
类,它创建两个角色:haulier 和 administrator,以及两个用户:admin 和 codeproject。
internal sealed class Configuration : DbMigrationsConfiguration<UsersContext>
{
public Configuration()
{
AutomaticMigrationsEnabled = true;
}
protected override void Seed(UsersContext context)
{
WebSecurity.InitializeDatabaseConnection(
"SecurityDb",
"UserProfile",
"UserId",
"UserName", autoCreateTables: true);
CreateAdminUser();
CreateHaulierUser();
}
private void CreateAdminUser()
{
if (!Roles.RoleExists("Administrator")) Roles.CreateRole("Administrator");
if (!WebSecurity.UserExists("admin"))
{
WebSecurity.CreateUserAndAccount(
"admin",
"password",
new { EmailAddress = "admin.staff@lite.dispatch.com" });
}
if (!Roles.GetRolesForUser("admin").Contains("Administrator"))
{
Roles.AddUsersToRoles(new[] { "admin" }, new[] { "Administrator" });
}
}
private void CreateHaulierUser()
{
if (!Roles.RoleExists("Haulier")) Roles.CreateRole("Haulier");
if (!WebSecurity.UserExists("codeproject"))
{
WebSecurity.CreateUserAndAccount(
"codeproject",
"litedispatch",
new { EmailAddress = "bluewhale.staff@lite.dispatch.com" });
}
if (!Roles.GetRolesForUser("codeproject").Contains("Haulier"))
{
Roles.AddUsersToRoles(new[] { "codeproject" }, new[] { "Haulier" });
}
}
}
新调度单
在此屏幕中,用户通过上传符合给定格式的 Excel 文件来输入新的调度。此文件的模板可在屏幕上的“下载模板”链接处找到。目前文件内容根本未检查,因为数据是模拟的,但应用程序仍然检查文件类型是否正确,因此只有在本地计算机识别 XLSX 文件时,此屏幕才能正常工作。
上传文件后,将显示一个屏幕,供用户验证导入的详细信息。
如果调度单已确认,用户将被带到活动调度屏幕。
在此屏幕上,用户可以向下钻取查看调度详细信息或打印它。
挑战 2 特别奖:复活节彩蛋
在 登录 页面找到 LiteDispatch 复活节彩蛋,点击 CodeProject Logo 即可看到彩球出现,点击越多,彩球越多。
我使用了 HTML5 Canvas
功能并结合 KineticJS 库。这是我第一次使用它,文档很好,动画工作起来并非易事,但我对结果很满意。
代码可以在 login.cshtml 文件中找到,以下是使动画工作的 JavaScript 代码:
<script defer="defer" type="text/javascript">
var stage = new Kinetic.Stage({
container: 'kinectContainer'
});
var layer = new Kinetic.Layer();
stage.add(layer);
function addBob() {
$('#kinectContainer')
.css("position", "absolute")
.css("bottom", "0")
.css("top", "0")
.css("left", "0")
.css("right", "0");
stage.setWidth ($('#kinectContainer').width());
stage.setHeight($('#kinectContainer').width());
var imageObj = new Image();
imageObj.src = '@Url.Content("../../Images/bob.png")';
var period = 8000;
var randX = Math.random() * 2 - 1;
var randY = Math.random() * 2 - 1;
var centerX = stage.getWidth() / 2;
var amplitudeX = stage.getWidth() / 2 * randX;
var centerY = stage.getHeight() / 2;
var amplitudeY = stage.getHeight() / 2 * randY;
imageObj.onload = function () {
var bob = new Kinetic.Image({
x: 0,
y: stage.getHeight() / 2,
image: imageObj,
width: 64,
height: 88,
name: 'bob'
});
layer.add(bob);
var rotDeg = Math.random();
var animBob = new Kinetic.Animation(function (frame) {
bob.setX(amplitudeX * Math.sin(frame.time * 2 * Math.PI / period) + centerX);
bob.setY(amplitudeY * Math.sin(frame.time * 2 * Math.PI / period) + centerY);
bob.rotateDeg(rotDeg);
}, layer);
animBob.start();
};
}
$('#moreBobs').click(addBob);
一个比我预想的更麻烦的问题是设置 Canvas 的 div 为全屏模式。我不确定为什么 CSS 对我不起作用,最终我不得不使用 jQuery 修改 div,如上面的代码所示。
挑战 3 - 在 Azure 上使用 SQL
在挑战 2 结束时,应用程序的原型已在 Azure 网站上运行,它有几个工作屏幕,并且可以使用简单成员资格组件进行安全保护。它甚至有一个基本的流程,允许承运商上传调度单文件。但在后端,所有这些东西都被存储在会话实例中,没有任何内置的持久性功能。在本节中,我们将讨论应用程序的持久性组件以及在 Azure 中让它们正常工作所需的条件。它讨论了两种不同的实现:SQL-CE 内联和 Azure SQL。
在这个阶段,在 EF 类之上开发了一套持久性组件,这样域实体就可以无缝地与持久性后端交互,同时又与 EF 分离。在新持久性组件中,本文的这一部分详细讨论了其中的两个:工作单元和通用存储库。
在 ORM 映射方面,EF Code First 方法越来越好,对于像本项目这样简单的模型,它只需最少的精力就能完成工作。我仍然是一个相当标准的 NHibernate 开发者,并且有点怀疑 EF 能否在中/大型项目中使用。话虽如此,我近四年来一直在关注 EF,看到它们不断改进是很好的。有一篇很好的文章讨论了这两种 ORM 实现,我推荐阅读,它以以下陈述结束:
然而,EF 的 RAD 方面不容忽视,对于以 SQL Server 为标准的短期项目来说,它很重要。对于那些项目,我会全心全意推荐 EF。但是对于大型系统,特别是基于领域驱动设计的系统,如果不能使用 NoSQL 解决方案,NHibernate 仍然是行业翘楚。
我有一个想实现的功能,我想知道是否可以同时使用两个 EF 上下文,一个使用 SQL-CE,另一个使用 Azure SQL。因此,我将安全模型设置为使用自己的上下文,然后域实体使用自己的上下文。我担心配置起来会很麻烦。但令我惊讶的是,一旦数据库到位,并没有遇到太多麻烦。我必须以某种手动方式创建和更新数据库才能使其正常运行,但正如我所说,一旦就位,我发现连接和数据提供程序设置非常出色且出乎意料地好用。关于 Azure SQL 的安装,新门户非常易于使用,并且使任务非常容易完成。因此,与几年前相比,在 Azure 中创建数据库要容易得多。本文的这一部分讨论了在 Azure 中运行数据库所需的步骤。
不幸的是,我在两个数据库上下文上遇到了一些麻烦。当我尝试通过 EF 迁移来部署数据库模式时,我发现的问题比预期的要多。我将在本节后面详细介绍我如何解决这些问题,这可能会帮助有类似需求的人。
总而言之,这是一次不错的挑战,看到让两个上下文同时工作的需求是值得的;这并非我每天都会遇到的情况,但总有人会因为某种原因需要连接到一个以上的数据库。
本节内容
本节讨论以下主题
域实体
创建了三个域实体来持久化当前网站页面使用的数据:Haulier、DespatchNote 和 DispatchLine。
为了适应新的域和持久性需求,在解决方案中添加了两个新项目:Domain 和 EF。第三个项目 (LiteDispatch.DbContext) 在后期创建,以解决我在迁移中遇到的问题。
域项目包含域实体、模型、转换、持久性接口和基本实现。EF 项目包含 Entity Framework 持久性实现。域实体遵循一些设计指南,这些指南在 WCF 示例文章 系列中有详细描述。该系列基于具有 WPF 客户端的 Tier 架构,该 Tier 架构与 WCF 服务器组件通信。服务器端组件的设计方式可以轻松地用于其他项目,例如 MVC 项目。此持久性层设计为通用形式,因此可以轻松适应不同的持久性实现;该系列目前涵盖四种实现:EF、NHibernate、RavenDB 和 InMemory。
关于域实体使用的一些基本模式,实体定义为不允许瞬态实例,集合不直接暴露,所有属性都具有私有设置器。这样,实体状态只能通过调用实体实例上的公共方法来修改。这是一种有助于解决方案持久化和维护的模式。上述系列更详细地涵盖了这种设计的理由和实现。
模型和实体之间的映射使用 AutoMapper 轻松实现;这是一个我发现非常有益且强烈推荐的辅助库,如果您以前没有使用过,请尝试一下,我相信您不会后悔。我就是喜欢它流畅的语法和内置的测试功能。在此项目中,它在两个静态类中使用。
public static class EntityToModel
{
public static void Install()
{
Mapper.CreateMap<DispatchLine, DispatchLineModel>();
Mapper.CreateMap<DispatchNote, DispatchNoteModel>()
.ForMember(d => d.HaulierId, m => m.Ignore())
.ForMember(d => d.HaulierName, m => m.UseValue("Bluewhale"))
.ForMember(d => d.Lines, m => m.MapFrom(s => s.DispatchLines()));
Mapper.CreateMap<Haulier, HaulierModel>();
}
}
public static class ModelToEntity
{
public static void Install()
{
Mapper.CreateMap<DispatchLineModel, DispatchLine>();
Mapper.CreateMap<DispatchNoteModel, DispatchNote>()
.ForMember(d => d.Haulier, m => m.Ignore());
Mapper.CreateMap<HaulierModel, Haulier>();
}
}
持久性设计与工作单元
提供了一个包含 CRUD 操作和 IQuerable
方法的通用存储库,因此不需要为每个实体创建单独的存储库。为确保事务得到妥善管理,存储库的访问通过存储库定位器提供。存储库定位器由事务管理器(此解决方案中的工作单元组件)创建。通过工厂类访问事务管理器。UoW 组件的典型用法如下:
public DispatchNoteModel GetDispathNoteById(long id)
{
return ExecuteCommand(locator => GetDispathNoteByIdImpl(locator, id));
}
private DispatchNoteModel GetDispathNoteByIdImpl(IRepositoryLocator locator, long id)
{
var instance = locator.GetById<DispatchNote>(id);
return Mapper.Map<DispatchNote, DispatchNoteModel>(instance);
}
private TResult ExecuteCommand<TResult>(Func<IRepositoryLocator, TResult> command)
where TResult : class
{
using (var transManager = Container.GlobalContext.TransFactory.CreateManager())
{
return transManager.ExecuteCommand(command);
}
}
上面的示例来自 DispatchAdapter
类,控制器引用了适配器并公开了按 Id 检索 DispatchNote 的方法。ExecuteCommand 是一个辅助方法,它获取 UoW(TransManager
)的新实例并执行 GetDispathNoteByIdImpl
方法中的逻辑。最后这个方法需要一个由 UoW 提供的定位器实例。在上述系列中可以找到关于此设计模式的更多详细信息,此外,如果您有机会,请运行提供的源代码以查看其工作原理。
Entity Framework 实现
EF 项目是 Entity Framework 的存储库和事务管理器实现。 WCF by Example - Chapter XVI - EF 5 & SQL CE - AzureWebSites Deployment 详细讨论了这种实现的各个方面,但值得注意的是以下几个方面:LiteDispatchDbContext
、ModelCreator
和内部映射类。
数据库上下文
Entity Framework 需要声明一个继承自 DbContext
类的类,在 Code First 中这一点非常重要,因为它用于构建数据库模式,因为模型没有定义。然后,这个 LiteDispatchDbContext
类声明了我们模型中的所有聚合根,并指示了要调用的自定义 EntityTypeConfiguration
实现。为此,将 DbModelBuilder
的实例传递给 OnModelCreating
方法。基构造函数将 web.config 文件中声明的连接字符串传递给使用,在这种情况下是:DomainDb。
模型创建器
Model Creator 在 Domain 项目中声明。此类用于以流畅的方式声明域实体和数据库表之间的映射。EF 可以从实体类本身推断出很多信息,但在某些情况下,需要额外的信息,因此属性和带 EntityTypeConfiguration
实例支持的模型创建器可以提供所有必需的帮助来正确创建模型。这是一个非常直接的类,它声明了我们模型聚合的根以及任何具有特殊映射的类。在本例中,它声明了 Haulier
实体,因为它是模型中的根(它还包含一个映射);然后声明了 DispatchNote
类,因为它包含一个映射类,所以 Model Creator 需要了解它。但是,不需要 DispatchLine
实体,因为它的映射不需要任何额外的配置,作为 DispatchNote
的子项,EF 会自动包含它。
public class ModelCreator : IModelCreator
{
#region Implementation of IModelCreator
public void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
modelBuilder.Configurations
.Add(new Haulier.Mapping())
.Add(new DispatchNote.Mapping());
}
#endregion
}
映射类
为了导航子实体,由于解决方案使用 EF Code First 方法,因此解决方案必须包含某种模型声明,当类本身无法提供有关模式模型足够信息时。因此,一些实体声明了一个内部类,该类由 ModelCreator 类使用;然后,当实例化 EF 数据库上下文时,ModelCreator 会被使用。例如,Haulier 类声明它包含一个调度单集合,并且每个 DispatchNote 都有一个指向自己的引用,即 Haulier。
部署选项
对于部署在 Azure 网站上的小型应用程序,使用 SQL-CE 数据库进行持久性非常有趣;它是一个单一文件,易于部署和维护,性能卓越,并且如果您使用的是 Entity Framework 或 NHibernate 等 ORM 框架,则可以轻松与您的域集成。开发周期短,并且不需要访问完整的 SQL Server。最后但并非最不重要的一点是,与 Azure SQL 选项相比,它是一个免费或非常便宜的选择。
但是,如果您正在从事中型或大型项目,您可能希望使用 Azure SQL。这种方法的一个优点是它可以被 Azure 内外的多个应用程序访问。使用 SQL-CE,只有部署在 Azure 网站上的应用程序才能与数据库“通信”。此外,Azure SQL 提供了出色的弹性和冗余功能,您的项目可能需要这些功能。
SQL-CE 配置与迁移
当我们描述如何在本文开头使简单成员资格与 SQL-CE 一起工作时,我们配置了 EF 以便它可以连接到 SQL-CE 数据库:那时配置了一个新的 SQL-CE 提供程序。如前所述,作为此阶段比赛的一个额外挑战,我正在为域实体创建一个第二个数据库,仅仅是为了证明我可以让 EF 应用程序连接到具有不同提供程序的多个数据库。使用 SQL-CE 时,域的连接字符串如下:
<add
name="DomainDb"
providerName="System.Data.SqlServerCe.4.0"
connectionString="Data Source=|DataDirectory|\LiteDispatch.Domain.sdf;default lock timeout=60000" />
上面连接设置中有几点需要注意;连接的名称(DomainDb)很重要,因为它在 LiteDispatchDbContext
类中使用。
并且数据库将位于 MVC 项目中的 App_Data
文件夹下。
设置迁移
Entity Framework 提供了 Migration 功能,该功能允许管理数据库的增量模式修改。它通过在数据库中持久化版本信息来实现此功能。简而言之,使用迁移有三个基本步骤:Enable、Add Migration、Update Database。
在此项目中,我们有两个上下文:Security 和 Domain。第一个声明在 Web 项目中,并以标准方式使用 EF,因此您可能会发现您的项目需要非常相似的步骤才能启用迁移。第二个,如我所发现的,并不那么容易正确设置。
简而言之,以下是启用迁移并首次创建数据库的步骤:
- 启用迁移
- 添加迁移
- 更新数据库,如果这是第一次,则创建数据库
一旦数据库到位,这些步骤在 Azure SQL 中是完全相同的。使用 SQL-CE 的另一个优点是,迁移组件会在数据库文件不存在时创建它。所有必需的步骤都在程序包管理器控制台中,您可以在 VS 菜单下的 Tools>Library Package Manager>Package Manager Console 中找到它们。我发现以下网页与此主题相关:
EF 迁移命令参考 | 这是一篇一年多前的文章,但大多数方面仍然适用。它涵盖了我在设置迁移时用于本文的所有命令。 |
开始使用 Entity Framework Code First 迁移 | 本文讨论了使迁移正常工作的步骤。您可能会发现它与我的步骤非常相似。它不包含像本文这样的 2 个上下文,但它指出了我必须采取的解决方法:一个不同的项目。 |
Microsoft Code First 迁移官方文档 | Microsoft 文档讨论了如何使用代码迁移来升级和降级数据库。 |
安全上下文的迁移
首先,在处理安全上下文的迁移时,将“默认项目”设置为 LiteDispatch.Web
项目。
启用迁移
执行“Enable-Migrations”命令,它会创建 Migrations 文件夹和 Configuration
类(点击缩略图展开图像)。
![]() |
|
然后,此时我们想修改 Configuration
类中的 Seed
方法,以便在更新数据库时调用 SecurityDbManager
,确保默认角色和用户在不存在时被创建。
public class SecurityDbManager
{
public void InitialiseDatabase()
{
WebSecurity.InitializeDatabaseConnection(
"SecurityDb",
"UserProfile",
"UserId",
"UserName", autoCreateTables: true);
CreateAdminUser();
CreateHaulierUser();
}
private void CreateAdminUser()
{
...
}
private void CreateHaulierUser()
{
...
}
}
然后,我只需要在 Migration 类中添加以下几行代码:
需要注意的是,如果您再次执行 Enable-Migration 命令,Configuration
类将被覆盖,因此您需要再次放置那几行代码。这就是为什么我首先创建 SecurityDbManager
类,而不是将所有逻辑都放在 Configuration
类中。如果您使用此方法,请确保您的逻辑检查是否需要创建记录,因为每次调用数据库更新时都会执行此逻辑。
添加迁移
此时,您要创建第一个迁移,执行 Add-Migration
命令,后跟一个您可以轻松识别的名称,我用的是:'Add-Migration InitialModel'。
此命令创建一个与调用命令时迁移名称相同的新类。
namespace LiteDispatch.Web.Migrations
{
using System;
using System.Data.Entity.Migrations;
public partial class InitialModel : DbMigration
{
public override void Up()
{
CreateTable(
"dbo.UserProfile",
c => new
{
UserId = c.Int(nullable: false, identity: true),
UserName = c.String(maxLength: 4000),
EmailAddress = c.String(maxLength: 4000),
HaulierName = c.String(maxLength: 4000),
})
.PrimaryKey(t => t.UserId);
}
public override void Down()
{
DropTable("dbo.UserProfile");
}
}
}
这是我们第一次创建模型,所以迁移类包含我们模型中的所有类。对于 SecurityDbContext
,我们只有一个类/表需要创建。
更新数据库
最后一步是执行迁移,它会创建 SQL-CE 文件,运行迁移类,然后调用我们在 SecurityDbManager
帮助类中声明的种子逻辑。要执行的命令是:'Update-Database'。
您可以在 App_Data
文件夹下找到数据库文件,此时应将数据库文件包含在项目中。尝试打开数据库以检查表是否正确创建,我使用了 SQL Server Compact Toolbox 扩展来完成此操作。
正如您所见,它创建了 UserProfile 表,但也创建了一个名为 _MigrationHistory 的“系统”表。打开它查看是否确实为 InitialModel
数据库更新创建了一条记录。
然后我们可以查看 UserProfile
表,确保两个默认用户记录也已创建。
此时,如果我们对 Security 类进行任何更改,只需再次执行 Add-Migration
命令,然后通过调用 Update-Database
来应用更改。原则上很简单,我相信它可能会变得更复杂,但这是一个很好的起点。
域上下文的迁移
这是一个棘手的设置,我遇到了许多问题才使其正常工作,首先它不是一个简单的上下文,因为它没有默认构造函数,需要声明一个实现 IDbContextFactory
接口的工厂类;结果是它只在 DbContext 和接口实现声明在同一个项目中时才有效。我发现的另一个问题是 EF Migrations 不喜欢有多个上下文的项目,因此,我将上下文移动到了自己的项目中。嗯,不完全是,我做的是创建一个新的 DbContext,它继承自 LiteDispatchDbContext
,然后将这个新的上下文放在一个新项目中,其唯一目的是管理 EF 迁移。
新项目名为 LiteDispatch.DbContext
,包括上下文工厂 DomainContextFactory
和 LiteDispatchMigrationsDbContext
类。
/// <summary>
/// Replica of the LiteDispatchDbContext for
/// the purpose of creating migrations
/// </summary>
public class LiteDispatchMigrationsDbContext : LiteDispatchDbContext
{
public LiteDispatchMigrationsDbContext(IModelCreator modelCreator) : base(modelCreator)
{
}
}
public class DomainContextFactory
: IDbContextFactory<LiteDispatchMigrationsDbContext>
{
public LiteDispatchMigrationsDbContext Create()
{
return new LiteDispatchMigrationsDbContext(new ModelCreator());
}
}
DomainContextFactory
类的目的是在执行迁移时帮助创建 LiteDispatchDbContext
实例。与安全上下文类似,新项目也包含一个类来帮助填充数据库,在本例中,该类名为 DomainDbManager
。
class DomainDbManager
{
public IModelCreator ModelCreator { get; set; }
public DomainDbManager(LiteDispatchDbContext context)
{
ModelCreator = context.ModelCreator;
}
public void Install()
{
EntityToModel.Install();
ModelToEntity.Install();
var factory = new TransManagerFactoryEF(ModelCreator);
using (var transManager = factory.CreateManager())
{
transManager.ExecuteCommand(locator =>
{
var haulier = locator.FindAll<Haulier>().SingleOrDefault(h => h.Name == "BlueWhale");
if (haulier != null)
{
return Mapper.Map<Haulier, HaulierModel>(haulier);
}
haulier = Haulier.Create(locator, new HaulierModel { Name = "BlueWhale" });
return Mapper.Map<Haulier, HaulierModel>(haulier);
});
}
}
}
DomainDbManager
类确保存在一个名为“BlueWhale”的承运商实例。我们将看到如何修改 Configuration 类来使用这个类,这与我们为安全上下文看到的完全一样。
在开始之前,请确保在处理域上下文的迁移时,PM 控制台中的“默认项目”设置为 LiteDispatch.DbContext
项目。
启用迁移
运行与之前完全相同的命令:'Enable-Migrations'
。
此命令创建 Configuration
类,我们将对其进行修改,以便 Seed 方法调用上面提到的 DomainDbManager
类。
internal sealed class Configuration : DbMigrationsConfiguration<LiteDispatchMigrationsDbContext>
{
public Configuration()
{
AutomaticMigrationsEnabled = false;
}
protected override void Seed(LiteDispatchMigrationsDbContext context)
{
new DomainDbManager(context).Install();
}
}
添加迁移
正如我们之前所见,此时我们可以创建第一个迁移,执行 Add-Migration
命令:'Add-Migration InitialModel'。(我使用了与上一个部分为安全上下文创建的迁移相同的迁移名称,但这无关紧要)。
命令完成后,我们可以看到在 Migrations
文件夹下为 InitialModel 迁移创建了一个新类。
如果我们查看生成的类,可以看到我们实体的所有表都在这个类中创建。
public partial class InitialModel : DbMigration
{
public override void Up()
{
CreateTable(
"dbo.Haulier",
c => new
{
Id = c.Long(nullable: false, identity: true),
Name = c.String(maxLength: 4000),
})
.PrimaryKey(t => t.Id);
CreateTable(
"dbo.DispatchNote",
c => new
{
Id = c.Long(nullable: false, identity: true),
DispatchDate = c.DateTime(nullable: false),
TruckReg = c.String(maxLength: 4000),
DispatchReference = c.String(maxLength: 4000),
CreationDate = c.DateTime(nullable: false),
User = c.String(maxLength: 4000),
Haulier_Id = c.Long(nullable: false),
})
.PrimaryKey(t => t.Id)
.ForeignKey("dbo.Haulier", t => t.Haulier_Id, cascadeDelete: true)
.Index(t => t.Haulier_Id);
CreateTable(
"dbo.DispatchLine",
c => new
{
Id = c.Long(nullable: false, identity: true),
ProductType = c.String(maxLength: 4000),
Product = c.String(maxLength: 4000),
Metric = c.String(maxLength: 4000),
Quantity = c.Int(nullable: false),
ShopId = c.Int(nullable: false),
ShopLetter = c.String(maxLength: 4000),
Client = c.String(maxLength: 4000),
DispatchNote_Id = c.Long(),
})
.PrimaryKey(t => t.Id)
.ForeignKey("dbo.DispatchNote", t => t.DispatchNote_Id)
.Index(t => t.DispatchNote_Id);
}
public override void Down()
{
DropIndex("dbo.DispatchLine", new[] { "DispatchNote_Id" });
DropIndex("dbo.DispatchNote", new[] { "Haulier_Id" });
DropForeignKey("dbo.DispatchLine", "DispatchNote_Id", "dbo.DispatchNote");
DropForeignKey("dbo.DispatchNote", "Haulier_Id", "dbo.Haulier");
DropTable("dbo.DispatchLine");
DropTable("dbo.DispatchNote");
DropTable("dbo.Haulier");
}
}
更新数据库
此时,我们已准备好执行迁移,这将创建数据库和表。只需在 PM 控制台中执行与我们为安全上下文讨论的相同的 'Update-Database'
命令。命令完成后,您应该能在 Web 项目中看到数据库,就在安全上下文的数据库旁边。
打开 Domain 数据库,然后检查表是否已创建,并且是否创建了一个 Haulier 记录。
Azure SQL 部署
此时,Web 应用程序正在 Azure 中运行,使用两个 SQL-CE 数据库。在本节中,我们将看看将其中一个数据库迁移到 Azure SQL 需要什么。在本节结束时,应用程序将连接到一个 SQL-CE 数据库和一个 Azure SQL 数据库。
首先,我们将讨论获得 Azure SQL 实例所需的内容,我们将创建一个 Azure SQL 服务器实例和 Domain 上下文的数据库。然后,我们将简要介绍 Azure SQL 工具和防火墙配置,以便您可以从 Azure 外部连接到数据库。然后,它将涵盖迁移到 Azure SQL 以及如何让应用程序正常工作。
章节内容
在本节中,我们将了解创建新的 Azure SQL Server 数据库所需的内容。
我们需要在 Azure 中创建一个 SQL Server 数据库。在门户中,选择左侧的 SQL Database 菜单,然后单击“Create A SQL Database”选项。
输入数据库详细信息,在本例中,我们将其命名为“LiteDispatchDb”,并指示需要一个新的 SQL Server。我使用了最小可用大小和 Web 选项,这对我应用程序的需求来说绰绰有余。
然后您需要输入服务器详细信息,在这里我们无法指定服务器名称,Azure 会为其分配一个随机名称。但您需要输入用户名、密码以及您首选的服务器位置。
用户详细信息很重要,因为它们将在后续阶段用于连接。
此时,我们应该在门户的 SQL Database 区域中看到已安装数据库的新条目,位于新服务器上。请注意,此时我们第一次获得我们刚刚创建的服务器名称,类似于以下屏幕:
选择新 SQL Server,然后选择“Configure”选项,在此屏幕中,您可以打开 Azure 端的防火墙,以便从外部连接到 SQL Server。
它允许将当前 IP 地址添加到“allowed ip address”中。如果您这样做,您可以在开发时连接到数据库,并且还可以使用 Azure SQL 工具。添加 IP 地址后,只需单击屏幕底部的 Save 菜单选项来保存新设置。
您需要从数据库仪表板收集连接详细信息,有一个选项可以在数据库仪表板中查看它们。
我们将使用 ADO 连接,稍后您将把这些详细信息粘贴到 web.config 文件中。
Azure SQL Server 数据工具
Microsoft 为 Azure SQL Server 实例提供 SQL Server 管理工具,您可以在 http://msdn.microsoft.com/en-us/data/tools.aspx 找到主页,下载地址是 这里。
您需要安装 Visual Studio,并且下载的工具会增强 SQL Server Object Explorer,以便您可以从开发计算机远程访问 Azure 服务器。
您可以通过在 View 下打开“SQL Server Object Explorer”菜单来轻松添加 Azure SQL Server。然后,要添加 Azure SQL Server,只需右键单击“SQL Server”文件夹。
输入 Azure SQL Server 详细信息,包括在门户中提供时使用的用户名和密码。如果遇到连接问题,请确保您在门户中添加了本地 IP 地址,如上所述。值得注意的是,有些公司可能有防火墙策略阻止连接,如果那样的话,您可能需要与您的管理员联系。
有几个我认为有用的好功能,您可以注册、发布和导出数据层应用程序 (DAC) 数据库。DAC 是一个逻辑数据库管理实体,它定义了用户数据库关联的所有 SQL Server 对象(如表、视图和实例对象,包括登录)。DAC 是一个自包含的 SQL Server 数据库部署单元,它使数据层开发人员和数据库管理员能够将 SQL Server 对象打包到名为 DAC 包的便携式构件中,也称为 DACPAC。BACPAC 是一个相关的构件,它封装了数据库模式以及数据库中存储的数据。您可以在此 数据层应用程序 链接中找到更多信息。
还有一个菜单选项可以检查两个数据库实例之间的模式差异。
应用程序设置
关于连接设置,由于解决方案使用了 EF Code First,我们可以使用 ADO 连接来连接到 Azure SQL 数据库;门户在上面提到的链接中提供此类数据,因此要添加到 web.config 的连接具有以下结构:
<add name="DomainDb"
providerName="System.Data.SqlClient"
connectionString="Server=tcp:{server name}.database.windows.net,1433;
Database={database name};
User ID={your user}@{server name};
Password={your password};
Trusted_Connection=False;
Encrypt=True;
Connection Timeout=30;" />
所以,这基本上就是让应用程序针对部署在 Azure SQL Server 上的数据库正常运行所需的内容,如果您使用的是 Code First。现在,剩下的唯一一步就是创建模式并填充一些表,正如我们已经看到的。
部署 Azure SQL 数据库神器 - 迁移
正如我们在本节开头提到的,我们将把管理域实体的上下文部署到 Azure SQL Server 数据库。我们已经将上下文配置为在 SQL-CE 中运行,并且我们创建了初始迁移。
为了创建表模式,我们将使用 Migration 功能,它只需要以下步骤:
- 将连接指向 Azure SQL 数据库
- 运行 Add Migration
- 执行数据库更新
将连接更改为指向新的 Azure SQL 数据库,如上一节所示。然后在 PM 控制台中执行以下命令,请记住在 PM 控制台中将 MVC 项目设置为默认项目。
Add-Migration AzureUpgrade
即使您没有更改实体,您也可能会发现生成了更改。这是 EF 发现新数据库是 Azure SQL Server 的结果,或者我们应该说,是 SQL Server 2012。无论如何,更改应该是最小的。
此时,我们已准备好执行模式安装。只需在 PM 控制台中执行以下命令:
Update-Database
如果一切顺利,您应该能够运行应用程序,并看到它正在正确地检索和创建新 Azure SQL 数据库中的记录。因此,使用 Migrations 功能将数据库模式迁移到 Azure 非常容易,即使我们从 SQL-CE 升级到 Azure SQL Server。我也喜欢迁移生成的纯 C# 代码,在某些情况下,我发现能够修改这些代码然后再运行数据库更新非常方便。只要模式与实体兼容,您就不应该遇到任何问题。
挑战后续说明
迁移与维护 SQL-CE 和 Azure SQL Server
| 我意识到,如果我想保持 SQL-CE 和 Azure SQL 数据库同步,几乎不可能用一个项目来管理迁移。因此,我创建了第二个项目来维护 SQL-CE 数据库。花了我一段时间才意识到最佳解决方案,一旦到位就非常有意义。这样,我就可以大部分时间离线工作或进行本地更改。只有当我满意最新的更改后,我才会更新 Azure SQL 迁移并使用最新的代码部署模式更改。 |
挑战 4 - 虚拟机
在比赛的这个阶段,我想创建另一个名为 LiteTracking 的解决方案。这个新组件的目的是为卡车司机使用的跟踪移动应用程序提供支持。目标是在主应用程序 LiteDispatch 中实时监控在途货物。
LiteTracking 是一个独立的应用程序,它提供一个客户端应用程序的端点,以便可以将通知消息转换为 LiteDispatch 中新仪表板屏幕的在途信息。这样,在途车辆可以告知到达最终交货目的地的距离和预计到达时间 (ETA)。
客户端移动应用程序只需要提供以下信息:卡车注册号和当前坐标。假定客户端设备具有 GPS 功能并连接到互联网。客户端将通过输入卡车注册信息来启动行程,然后将消息发送到 LiteTracking 应用程序。刷新消息会在给定的时间间隔自动发送。
LiteTracking 应用程序部署在 IIS 上的 Azure 网站上。它使用 WebAPI 暴露 REST 方法,以便卡车司机可以通知他们当前的位置。然后,应用程序使用给定的坐标使用路由服务提供商获取所需信息:到目的地的距离和 ETA。一旦获得这些数据,就会生成一个确认响应给卡车司机的客户端应用程序,并创建一个通知消息给 LiteDispatch 组件。
本次挑战将讨论与 Azure 虚拟机相关的以下技术方面:
- 如何创建 Azure 虚拟机实例
- 配置 VM 以在 IIS 上托管应用程序
- 配置 VM 以接受外部请求
- 启用基础设施以发送请求到路由提供商
- 发送通知到 Azure 网站
它还涵盖了以下更改和增强:
- 卡车司机客户端应用程序 - 移动模型
- 必应地图服务集成
- LiteDispatch 应用程序上的仪表板屏幕
- SignalR 实现以启用交互式仪表板
- WebAPI 实现以解耦应用程序组件
拟议移动客户端应用程序的一些截图
![]() | 客户端应用程序的开始页面,这是一个非常简单的应用程序,它捕获当前坐标并将其发送到 Azure VM 上部署的应用程序以获取距离和 ETA 详细信息。 |
![]() | 行程开始时,司机输入卡车注册信息并发送通知。 |
![]() | 消息收到后,给定的坐标将用于在必应地图上设置路线。然后计算距离和 ETA 详细信息,并将它们发送回移动设备,以确认消息已收到并处理。在后端,会发送通知给 LiteDispatch 应用程序,以便更新新的仪表板屏幕。SignalR 用于同步用户浏览器,这样他们就不必手动刷新浏览器。 |
到目前为止,承运商能够创建调度单并查询活动调度单。通过添加 LiteTracking 应用程序,承运商还可以提供跟踪详细信息。因此,在这一阶段结束时将提供以下功能:
下图描述了解决方案在此阶段包含的三个应用程序,展示了它们之间的交互:
移动应用程序使用 REST 方法与 LiteTracking 应用程序通信,因此需要将 Azure 中的 VM 配置为接受外部传入请求,本节将介绍如何配置 VM 上的端口以便外部应用程序可以连接到它。您可能会发现您可以将此类功能部署到 Azure 网站或 Worker Role。但是,正如我们将看到的,如果您想完全控制部署应用程序的环境,虚拟机就成为理想的解决方案。
在 Azure 中创建虚拟机
为了托管 LiteTracking 应用程序,我们将创建一个 Azure 虚拟机,更具体地说是一个运行 IIS 的 W2008 服务器。LiteTracking 应用程序只是一个暴露 WebAPI 端点的 MVC Web 应用程序,供客户端使用。稍后我们将展示如何配置端口以便从 Azure 外部调用它,目前,让我们看看如何让服务器运行起来。
简而言之,我们需要执行以下步骤:
- 使用 W2008 映像创建 VM
- 配置服务器以运行 IIS
- 初步测试安装
- 配置端口
转到您的 Azure 帐户中的 Azure 主仪表板,然后选择创建新的 VM。
然后指示虚拟机的名称、映像(Windows Server 2008 R2 SP1)、大小(Small)、用户详细信息和位置。
所需做的就是这样,只需等待 5 分钟,新的服务器应该会在其完全配置后显示在您的仪表板上。此时,我们想远程登录到新服务器,要做到这一点,请转到虚拟机菜单并选择您的新服务器;转到仪表板,然后在底部菜单栏中选择“connect”菜单选项。
此操作会下载一个 RDC 文件(远程桌面连接),只需保存并执行它。它会要求您输入用户凭据,也就是您在创建 VM 时创建的凭据。如果您输入了正确的凭据,您会看到您只是通过标准的远程客户端软件连接到您的服务器,在 Azure 中运行服务器并没有什么特别之处,它非常简单。如果您此时检查服务器,您会发现机器上运行的东西非常少。您需要从头开始配置机器,让我们看看需要多长时间才能运行应用程序。
我过去一直使用经典的方法安装角色等。您可以尝试这种方式,但我将解释最快的方式:Web 平台安装程序(又称 WebPI)。但在我安装 Chrome 到服务器之前,只是为了快速避免 IE 在 Windows Server 上运行时显示的所有安全警告。您可能会发现其中一些步骤需要一些时间,我认为这是由于我选择的 VM 大小,我没有尝试更大的尺寸,但我怀疑只有这么少的内存和单个 CPU 在如今只能起到一点作用。
WebPI 可在此下载:此处。运行后,您需要选择以下两个包:
- IIS 推荐配置
- Microsoft .NET Framework 4.0
下载文件几乎不需要时间,盒子上的互联网连接非常好。再次,痛苦在于安装过程本身在这个小型服务器上。所有包安装完成后,您应该检查 IIS 是否正常工作,您可以检查角色是否正常运行。我遇到了几个 WAS 的错误,但我认为这只是噪音,因为我后来在部署和测试应用程序时没有发现任何问题。也许最好的办法是在机器上安装一个测试应用程序,我一直发现很有用的是使用默认 VS 模板创建一个非常基本的 MVC4 项目并将其部署到服务器以确保一切正常。但在此案例中,我将使用 LiteTracking 应用程序来做到这一点。此时我将保持非常简单,我将应用程序发布到本地文件夹,然后将其复制/粘贴到 VM 机器,所以我不会尝试直接发布到 VM。我必须手动创建一个 IIS 虚拟目录,然后测试应用程序在服务器本身上使用浏览器运行时是否正常。
所以第一步是获取二进制文件,您只需使用 VS 发布到本地文件夹,然后按我上面提到的方式复制文件。从 VS 中发布非常简单,只需右键单击项目文件并选择发布选项。然后只需指示您要发布到文件位置。
将发布的文件夹复制到剪贴板,然后您只需要回到远程服务器会话,打开一个 Explorer 窗口;导航到 wwwroot 文件夹并复制文件到那里,一个好的做法是在传输之前将文件压缩成 zip 文件。我发现传输单个文件效果更好。
然后我需要创建一个新的网站和一个新的应用程序池,用于 .NET 4。
我们将位置设置为我们复制二进制文件的文件夹。
我更改了默认端口设置,以便网站将在 **端口 8899** 上监听,这是一个非常重要的细节,因为我们稍后在配置服务器防火墙时会看到。
此时一切应该都已正常工作,如果您打开浏览器并导航到 **localhost:8899**,您应该会看到我们刚刚部署的网站的主页。
没有什么特别壮观但很有希望,让我们看看是否能让其他东西正常工作。所以让我们检查路由服务是否正常工作。为方便测试,让我们检查一下巴黎离柏林有多远。带有巴黎坐标的 Rest 方法是:https://:8899/Api/Tracking/GetRouteDetails?latitude=48.85693&longitude=2.3412&truckRegistration=12D287
| 目前应用程序设置为计算从给定位置到柏林(我们将柏林设为所有卡车的最终目的地)的距离和 ETA。这显然是在生产环境中需要改进的地方,但为了本次比赛的目的,我想尽可能保持简单。 |
正如您所见,在 Azure 中让必应地图服务正常工作并没有什么特别之处。如果它在我们的机器上工作正常,那么在 Azure 中也会工作得更好,性能总是非常好的,并且在调用 VM 到必应地图服务时您不应该遇到任何问题。另一方面是从 Azure 外部的客户端调用新服务。让我们看看从外部客户端公开服务需要什么。首先,我们需要收集我们的 VM 详细信息,更具体地说,是机器的 DNS 名称。如果 Azure 门户在 VM Dashboard 页面上,您可以看到机器的 DNS 详细信息,在本例中:
如果您在本地浏览器中尝试调用以下 URI:http://litetracking.cloudapp.net:8899/Api/Tracking/GetRouteDetails?latitude=48.85693&longitude=2.3412&truckRegistration=12D287,您会发现超时了。Azure 中的服务器不接受传入请求。所以要做的第一件事是在 Azure Dashboard 中启用一个端点。转到 VM 的主页,然后选择顶部菜单中的 EndPoint 菜单,或选择任务栏上的 Add 菜单。
| 您可能需要用 (litetrackingservices) 替换此处使用的 DNS 服务器名称(litetracking),如果您尝试调用我部署在 Azure 上的服务。正如我们稍后在讨论创建 VM 映像时将看到的,我不得不使用与原始名称不同的 DNS 名称恢复了该机器,因此此处使用的 DNS 名称已不存在。 |
然后输入端点详细信息,它是一个 TCP 端口,在这种情况下,我将使用相同的端口号来路由公共端口和 VM 端口。
此时,我们将面临一个棘手的问题,在 Azure 中设置端点还不够,默认情况下 Windows Server 2008 设置为不接受传入请求,所以我们还需要更改服务器配置。要做到这一点,请打开服务器管理器并导航到 Windows 防火墙选项。
转到入站规则节点并为端口设置一个新规则,指明它是 TCP 端口,端口号为:8899。
对于其他选项(操作和配置文件),默认值是正确的;只需给规则起一个名字并创建它。规则到位后,请再次尝试从本地浏览器调用 REST 方法。这次应该可以工作了。总的来说,这就是将虚拟机服务暴露给 Azure 外部外部客户端所需的方法。
...几天后
正如我提到的,在我撰写文章时,我使用的 VM 名称与当前部署的名称不同。我还发现了一些从其他网络调用 Tracking 服务的问题,因此我做了一些更改。
更改 Azure VM 端点
使用不同于 80 的端口可能会在某些位置导致问题,幸运的是 Azure VM EndPoints 提供了帮助,所以我将 EndPoint 修改如下:
VM DNS 名称
新的 VM DNS 与本文撰写时提到的不同。如果您想测试 VM Tracking REST 方法,您需要使用以下 URI:
truckRegistration 值必须匹配 **Active Dispatches** 屏幕中的某个条目才能工作。如果您打开 LiteDispatch 屏幕,使用 http://litedispatch.azurewebsites.net/Dispatch/Enquiry,您可以获得在途卡车列表,以便您可以相应地更新 truckRegistration 参数。
以下是您可能想要尝试的一些坐标列表:
城市 | 纬度 | 经度 |
---|---|---|
马德里 | 40.4203 | -3.70577 |
巴黎 | 48.85693 | 2.3412 |
罗马 | 41.903049 | 12.4958 |
维也纳 | 48.202541 | 16.368799 |
伊斯坦布尔 | 41.040871 | 28.986179 |
莫斯科 | 55.751709 | 37.618023 |
例如,如果我们想知道维也纳的一辆卡车离我们在柏林办公室有多远,URL 是: http://litetrackingservices.cloudapp.net/Api/Tracking/GetRouteDetails?latitude=48.85693&longitude=2.3412&truckRegistration=12D12321。响应是:
| 我发现必应地图报告的持续时间几乎是我在浏览器中计算路线时的一半,我需要调查一下是怎么回事,也许需要传递一些额外的信息。 |
还有一点,我增强了响应消息,使其包含一个错误部分,以防卡车在 LiteDispatch 应用程序中找不到,并且它还指示处理跟踪请求后是否更新了活动调度列表。
创建映像
此时,您可能需要创建一个服务器映像,安装 IIS 花费了不少时间,配置还可以,但至少有点繁琐。所以让我们看看创建 VM 映像用于备份需要做什么。正如我们所见,您可以使用映像库中的映像来轻松创建虚拟机,但您也可以捕获自己的映像来创建自定义虚拟机。映像是用于创建虚拟机的虚拟硬盘 (.vhd) 文件。映像是模板,因为它没有配置的虚拟机特有的设置,例如计算机名称和用户帐户设置。如果您想创建多个以相同方式设置的虚拟机,您可以捕获配置的虚拟机的映像并将其用作模板。还必须注意,已停止的 VM 仍然会产生 CPU 使用,因此即使未使用,也会产生一些费用。所以删除不使用的 VM 是有价值的,但是,在删除之前创建虚拟映像可以最大限度地减少重新构建新映像所需的时间。
首先,我们需要准备 VM 实例,为此,我们需要从命令行在 %windir%\system32\sysprep 目录中执行 sysprep 命令。在 System Cleanup Action 中,选择 Enter System Out-of-Box Experience (OOBE),并确保选中 Generalize。
操作需要几分钟,但最终机器会停止,在本例中大约是 15 分钟,我曾想在仪表板上停止机器,认为出了什么问题,请耐心等待,因为机器最终应该会自行停止。然后可以调用捕获操作。
映像拍摄后,VM 将被删除,但正如我们稍后讨论的,它很容易恢复运行。新映像可以在 Virtual Machines 下的 Images 菜单中找到。
要重新创建我们的服务器,我们只需创建一个新的 VM,并在此次选择“From Gallery/My Images”选项。
然后,您只需以与最初相同的方式输入 VM 详细信息。您可能会发现无法使用相同的 DNS 名称,释放已删除 VM 的 DNS 名称可能需要一段时间,但老实说,我也不确定它是否最终可用。有一些关于“可用性集”的选项,只需保留默认值;但您可能需要禁用 PowerShell 远程处理选项。
再次配置机器需要几分钟时间,但完成后,请记住在测试路由服务之前在 Azure Dashboard 中再次设置端点。我发现我必须远程登录并打开网站才能使其工作,也许是时间问题,我不确定。我需要进一步研究 VM 在重启后的行为。
到目前为止的结论
在某些方面,在 Azure 中运行虚拟机很简单,我只是认为在开始处理我真正感兴趣的内容之前,有很多小细节需要设置。对于我部署的应用程序,毫无疑问,一个简单的 AzureWebsite 会更方便。但同样真实的是,VM 选项非常强大,在需要完全控制部署环境的场景中,VM 非常理想。
我发现能够创建映像用于备份或仅仅是为了加快未来安装速度非常有益。看到图库中有自定义映像真是太好了。我只担心一个方面,如果您查看 blob,您可以看到 Azure 为您的 VM 和映像创建了一个 blob。如果您深入到容器中,您可以看到大型 blob 实例,在本例中是 127 GB。我最终有三个 blob 实例,我不确定它们是否都被使用。我想在某个时候删除一些。使用的命名约定不太有用,所以我认为在这一点上有一些改进的空间,否则我可以看到一些情况,如果需要许多映像和 VM,事情可能会变得棘手。
整合
此时,我们有一个新的 VM 正在运行,并且我们部署了 LiteTracking 应用程序的第一个版本。我们能够从 Azure 外部调用该服务,它会处理必应地图服务并返回路由详细信息。现在是时候将所有部分整合在一起了,我们需要向我们在 AzureWebsite 上部署的 LiteDispatch 应用程序发送通知,以便它可以更改调度状态并包含路由详细信息。这样,查看在途调度屏幕的用户就可以了解卡车在哪里以及它们是否即将到达。
我昨晚非常富有成效,并且在两个应用程序中添加了大量的代码更改。总之,完成了以下增强:
- LiteDispatch 应用程序
- 新的 TrackingNotification 实体
- Active Dispatches 屏幕的更改,用于渲染持续时间和距离路由详细信息
- 新的 Tracking WebAPI Controller,以便可以从 LiteTracking 应用发送通知
- 新的 Core 库,以便 DTOs 可以在两个应用程序之间共享
- LiteTracking 应用程序
- 使用 REST 方法将路由详细信息发送到 LiteDispatch
下一节将讨论在此比赛阶段添加到解决方案中的最相关的组件。
WebAPI 实现
在让两个应用程序之间实现集成方面,最有趣的方面之一与 WebAPI 相关。我必须在 LiteTracking 应用程序中公开端点,以便任何简单客户端都可以发送卡车坐标,同时,LiteDispatch 应用程序还需要另一个端点来跟踪新 LiteTracking 处理卡车路线消息时创建的通知消息。我想使用 REST 服务,因此我选择了 MVC WebAPI 方法,而不是例如 WCF 服务。
将 WebAPI 控制器添加到 MVC 应用程序非常容易,让我们来看看 LiteTracking 中的控制器。
public class TrackingController : ApiController
{
[HttpGet]
public TrackingResponse GetRouteDetails(string truckRegistration, double latitude, double longitude)
{
var response = new TrackingResponse
{
Latitude = latitude,
Longitude = longitude
};
ProcessRequest(truckRegistration, latitude, longitude, response);
return response;
}
...
}
因此,有几点值得注意:类继承自 ApiController
,并且我们在方法上使用了 HttpGet
属性。如上所述,使用浏览器调用此方法很容易,因为所有参数都可以声明在 URI 中。
在 LiteDispatch 应用程序中,我们也添加了一个新控制器,以便在卡车发送其坐标时可以发送通知消息。该控制器非常相似。
public class TrackingController : ApiController
{
public TrackingController()
{
TrackingAdapter = new TrackingAdapter();
}
public TrackingAdapter TrackingAdapter { get; set; }
[HttpPost]
public TrackingResponseDto CreateTrackingNotification(TrackingNotificationDto dto)
{
return TrackingAdapter.CreateTrackingNotification(dto);
}
}
它有几个不同之处:使用 HttpPost
来标记方法,而不是传递原始参数,我们传递了一个 DTO。所以在这种情况下,从浏览器调用 REST 方法并不那么简单。让我们看看另一端调用此方法的代码。在 LiteTracking 应用程序中,我们有一个名为 SendNotification
的方法,该方法可以按如下方式调用 REST 方法。
private void SendNotification(TrackingResponse response)
{
using (var client = new WebClient())
{
client.Headers[HttpRequestHeader.ContentType] = "application/json";
var notificationDto = new TrackingNotificationDto
{
Distance = response.Distance,
DistanceMetric = response.DistanceMetric,
Duration = response.Duration,
DurationMetric = response.DurationMetric,
Latitude = response.Latitude,
Longitude = response.Longitude,
TruckRegistration = response.TruckRegistration,
Id = Guid.NewGuid()
};
var json = Newtonsoft.Json.JsonConvert.SerializeObject(notificationDto);
var result = client.UploadString(GetTrackingNotificationUri(), json);
var dto = Newtonsoft.Json.JsonConvert.DeserializeObject<TrackingResponseDto>(result);
response.NotificationWasCreated = dto.Accepted;
response.RequestGuid = dto.NotificationId;
response.DispatchNoteId = dto.DispatchNoteId;
response.Error = dto.Error;
var r = dto;
}
}
因此,会创建一个 WebClient
实例,并将内容类型设置为 JSON。然后创建一个 DTO 实例(notificationDto
),并将其序列化为 JSON 数据,该数据在调用方法时使用。还可以看到如何使用 JSON 反序列化响应。在这种情况下,我们使用 Newtonsoft 库,稍后当我们查看 BingMaps 实现时,我们将看到 .NET 框架如何反序列化 JSON 响应。
以下是一些关于 WebAPI 开发的链接。
| 在生产解决方案中,您可能需要围绕调用 REST 方法的代码实现更健壮的逻辑。您应该处理通信异常以及可能的安全方面。 |
Bing Maps 集成
为了跟踪目的,我们使用了 Bing Maps 来收集路线详细信息。目前,应用程序设置为卡车将货物运送到柏林的一个办公室,卡车只需发送其坐标,LiteTracking 应用程序就会处理其余的。它通过调用 BingMap 服务收集路线详细信息,然后将信息发送到 LiteDispatch 应用程序,之后才会向卡车司机的客户端设备发送确认。
与 BingMaps 的集成相对简单,我们再次调用一个 REST 方法,并使用 DataContractJsonSerializer
帮助类反序列化 JSON 响应。这里的诀窍是 Response
类。我从以下链接获取了此类及其子类: Bing Maps REST Service .NET Libraries。这些类使导航响应对象的问题变得容易得多。以下代码用于调用 BingMaps 服务。
private Response GetRouteDetails(Uri uri)
{
var wc = new WebClient();
var response = wc.OpenRead(uri);
var ser = new DataContractJsonSerializer(typeof (Response));
if (response != null)
{
return ser.ReadObject(response) as Response;
}
return new Response();
}
与我们之前看到的 Newtonsoft 方法的主要区别在于,Response
类及其子类需要使用 WCF 标签声明为 Contract
类。而 Newtonsoft 库在处理 JSON 消息时可以使用 POCO 对象。
您可能不知道,但在调用服务之前,您需要一个 BingMap 密钥。您可以在以下位置获取自己的密钥:Bing Maps Account Center。然后只需将其附加到请求中,如下所示。
private void ProcessRequest(string truckRegistration, double latitude, double longitude, TrackingResponse response)
{
const string query =
@"http://dev.virtualearth.net/REST/V1/Routes/Driving?o=json&wp.0={0},{1}&wp.1={2},{3}&optmz=distance&rpo=Points&key={4}";
var origin = new[] {52.516071, 13.37698};
var uri = new Uri(string.Format(query, latitude, longitude, origin[0], origin[1], GetKey()));
var bingResponse = GetRouteDetails(uri);
...
}
GetKey()
方法从应用程序设置中返回一个字符串,但您可以将其替换为您自己的密钥。我觉得使用 API 非常简单,我对返回的信息量感到惊讶。在此示例中,我们只使用了两个字段,但服务返回了大量数据。
以下是我在 BingMaps 开发方面发现的一些有用的链接。
SignalR 实现
我在 LiteDispatch 应用程序的“活动调度”屏幕中添加了 SignalR,其想法是,当另一个用户创建新的调度单或收到跟踪通知时,屏幕将自动刷新,无需用户干预。过去,这种功能通常使用客户端计时器来实现。但有了 SignalR 库,服务器就可以向客户端发送消息,从而通知某些状态已更改,这在您的工具集中拥有一个非常好的功能。我以前从未用过这个库,我认为这是一个绝佳的机会,总而言之,它对我来说效果非常好,我惊讶于只需很少的代码就可以使其正常工作;这似乎有些魔力,但它确实效果很好。
因此,我实现了两个功能:第一个是一个流行的 SignalR 聊天实现,屏幕上的一个按钮允许用户向在不同会话中打开同一屏幕的任何其他用户发送消息。第二个实现是刷新新创建或更新的调度单。让我们看看它们是如何实现的。
聊天实现
在“活动调度屏幕”上有一个“发送消息”按钮,该按钮会显示一个对话框窗口,供用户输入要发送给其他用户的消息。
您需要使用 NuGet 将 SignalR 包添加到 Web 应用程序中,安装过程可确保在 Web 应用程序启动时正确初始化 SignalR 中心。看看 HubConfig
类和 Application_Start
方法。此示例显示了客户端调用服务器方法,因此,我们声明了一个自定义 Signal Hub。
public class LiteDispatchHub : Hub
{
public void Send(string name, string message)
{
Clients.Others.addNewMessageToPage(name, message);
}
}
该类只有很少几行代码,但有两个非常重要的方面。首先,声明了 Send
方法,以便客户端可以发送消息到服务器,并传递消息文本和用户名。一旦服务器收到消息,它就会调用 addNewMessageToPage
客户端方法。这就是魔力发生的地方,服务器方法定义明确,但客户端方法是动态的,我们需要小心地将它们正确地绑定起来。这就是服务器端所需的一切,让我们看看客户端端需要什么。
Enquiry.cshtml
视图渲染“活动调度”屏幕,在文件开头,声明了自动生成的 Signal 代理。
@section headSection{
<script src="~/Scripts/jquery.signalR-1.1.0.js" type="text/javascript"></script>
<!--Reference the autogenerated SignalR hub script. -->
<script src="~/signalr/hubs" type="text/javascript"></script>
}
大部分魔力发生在应用程序启动并生成集线器脚本时。以下代码声明了服务器在 Send
方法中使用的客户端方法。
<script type="text/javascript">
// Reference the auto-generated proxy for the hub.
var chat = $.connection.liteDispatchHub;
// Function that the hub calls back
chat.client.addNewMessageToPage = function(name, message) {
alert("Message from: " + name + "\r\r" + message);
};
...
我们还有少量代码将按钮的单击事件绑定起来,以便它能够调用服务器的 send
方法。
$.connection.hub.start().done(function () {
$('#sendMessage').click(function () {
var userName = $('#userName').val();
if (userName == "") {
$('#userName').val(prompt('Enter your name:', ''));
userName = $('#userName').val();
}
var message = prompt('Message:', '');
chat.server.send(userName, message);
});
});
| 值得注意的是,服务器方法在客户端以小写形式声明,否则可能会浪费大量时间来找出客户端为何无法调用服务器方法。我认为有一个属性可以指定客户端上的方法名称,以防您想解决这个问题。 |
同步功能
一个常见的要求是确保仪表板网页在服务器端发生更改时自动更新。这些更改通常是由其他用户更新页面上显示的状态引起的,但也可能由后端中的其他服务引起。
在 LiteDispatch 应用程序的情况下,这两种情况都会发生:用户可以创建新的调度单,这些调度单应该显示在屏幕上,但我们还有 LiteTracking 系统生成的跟踪通知。因此,我们有两种类型的事件:新调度和更新调度。
机制很简单,当服务器发现调度已创建或更新时,它会通知所有打开“活动调度”屏幕的客户端。然后,客户端能够检索调度详细信息以及服务器提供的通知详细信息。客户端使用 jQuery 调用 AJAX 方法,这些方法调用控制器中的操作,渲染部分视图,即 HTML。
| 我能够将复杂的对象通过 SignalR 全部发送到客户端,但我不想在客户端使用 JavaScript 进行所有 HTML 格式化。如果我使用 jTable、knockout 或 kendo,我可能会采用这种方法,避免二次往返服务器。 |
客户端实现很简单,所有代码都在我们之前提到的同一个 Enquiry.cshtml
文件中。它声明了两个客户端方法:newDispatch
和 updateDispatch
,除了少量用于渲染从服务器检索到的调度单 HTML 的代码外,没有太多其他东西。我将警报作为调用发生的测试机制留了下来。
chat.client.newDispatch = function (dispatchId) {
alert("A new DispatchNote was created by other user with Id: " + dispatchId);
$.get('@Url.Action("GetDispatchNoteDetails", "Dispatch")' + '?dispatchId=' + dispatchId, function (html) {
renderDispatch(dispatchId, html);
});
};
chat.client.updateDispatch = function (dispatchId) {
alert("An existing DispatchNote was updated by other user with Id: " + dispatchId);
$.get('@Url.Action("GetDispatchNoteDetails","Dispatch")' + '?dispatchId=' + dispatchId, function (html) {
$('#dispatch_' + dispatchId).hide("slow").remove();
renderDispatch(dispatchId, html);
});
};
var renderDispatch = function (dispatchId, html) {
$('#dispatches').prepend(html);
$('#dispatch_' + dispatchId).hide();
setDispatchesStyle();
linksToButtons();
$('#dispatch_' + dispatchId).show(2000);
};
在服务器端,我只需要修改两部分代码,以便服务器调用我们刚刚描述的两个方法。为了调用 newDispatch
,在 DispatchController
中进行了更改;当创建新的调度单时,客户端调用 Confirm
操作,此时服务器调用客户端方法,如下所示。
public ActionResult Confirm()
{
TempData["NotificationMsg"] = "Last dispatch note was confirmed";
var dispatchModel = LiteDispatchSession.LastDispatch;
dispatchModel.CreationDate = DateTime.Now;
dispatchModel.User = WebSecurity.CurrentUserName;
var savedDispatch = DispatchAdapter.SaveDispatch(dispatchModel);
var hubContext = GlobalHost.ConnectionManager.GetHubContext<LiteDispatchHub>();
hubContext.Clients.All.newDispatch(savedDispatch.Id);
return RedirectToAction("Enquiry");
}
对于跟踪通知,LiteTracking 系统调用 WebAPI CreateTrackingNotification
方法,因此在这里,服务器调用客户端方法,如下所示。
[HttpPost]
public TrackingResponseDto CreateTrackingNotification(TrackingNotificationDto dto)
{
var response = TrackingAdapter.CreateTrackingNotification(dto);
if (response.DispatchNoteId > 0)
{
var hubContext = GlobalHost.ConnectionManager.GetHubContext<LiteDispatchHub>();
hubContext.Clients.All.updateDispatch(response.DispatchNoteId);
}
return response;
}
在行动中查看 SignalR
如果您想查看 SignalR 的工作原理,只需按照以下几个步骤操作。
- 转到活动调度仪表板: http://litedispatch.azurewebsites.net/Dispatch/Enquiry
- 更新一辆卡车:调用跟踪 REST 方法
仪表板页面上应该会弹出一个窗口,显示更新的卡车。关闭消息后,仪表板会刷新,更新的卡车会排到列表顶部。
结论
Azure 提供了一种简便的方法来安装和配置 Web 上的虚拟机,因此您可以部署一套完整的应用程序和功能,而这在标准网站上可能难以复制。我们已经看到了如何通过安装操作系统软件和所需附加功能来完全控制机器。然后,我们运行了应用程序,并使用 REST 方法从 BingMaps 服务中检索信息,并且几乎不费力地将该信息发送到比赛第二阶段部署在 Azure 网站上的应用程序。此时,唯一剩下的就是将虚拟机上运行的新跟踪应用程序暴露给外部客户端;因此,我们详细讨论了门户网站和虚拟机中启用此类功能所需的配置设置。
最后,我们演示了如何创建虚拟机映像,以及如何轻松地恢复它并使其在 Azure 门户网站上正常工作。
挑战 5 - 移动访问
对于这项挑战,我准备了一个新应用程序:LiteTracker。这是一个 Windows 应用商店应用程序,可在包括 Windows RT 在内的任何 Windows 8 设备上运行。我想尝试 Azure 中一个特别有趣的功能:移动服务。自去年以来,我一直在关注 Windows 8 中的推送通知功能,而这次竞赛是尝试它的绝佳机会。推送通知专为 Windows 8 或 Windows Phone 8 运行的 Windows 应用商店应用而设计。因此,在过去的几周里,我付出了巨大的努力,推出了我的第一个使用 Azure 推送通知的可运行 Windows 应用商店应用程序。这是一段非常忙碌的时光,我相信还有很多改进的空间。本节将介绍操作方法,哪些方面进展顺利,哪些方面进展不那么顺利。
此时,竞赛中已经有一系列应用程序,Azure 网站:LiteDispatch 托管核心功能。它提供了一个入口点来添加和显示调度单。在上一阶段,托管在 Azure 虚拟机中的一个新组件处理来自卡车驱动程序的 GPS 兼容移动设备的传入跟踪通知,该应用程序:LiteTracking,处理此信息,并借助 Bing Map 服务找出路线信息,然后将其转发给核心 Web 应用程序。过去两周,我们讨论了如何使用 WebAPI 和 SignalR 开发围绕此功能的“响应式”仪表板网页。
新应用程序:LiteTracker 是一个增强的活动调度跟踪仪表板,类似于在 Azure 网站上开发的页面,但具有附加功能和增强的用户界面。用户会发现该应用程序非常易于使用,在一个屏幕上,用户可以跟踪所有在途卡车。但其最佳功能在于与 Bing Maps 和推送通知的集成。通过前者,Bing Map 集成,地图显示卡车的位置,并且很容易看出需要多长时间才能到达目的地。第二个,推送通知,首先,Windows 8 中的 toast 通知确保用户知道卡车何时发送了新的跟踪消息并检查其进度,但应用程序本身订阅了通知,以便它可以自动请求刷新在途卡车详细信息。
总而言之,我认为新应用程序非常有吸引力,它具有吸引人的界面并且易于使用,但同时在一个屏幕上提供了用户需要知道的所有信息。您可能会发现这种方法可能适用于许多其他应用程序,内容将不同,但技术实现可能非常相似。
当开始这个阶段的开发时,我假设以下业务流程将由当前的移动服务功能支持。
- 跟踪事件在LiteDispatch应用程序中创建。
- 因此,LiteDispatch负责创建推送通知。
- 移动服务会将这些消息转发给客户端。
- 当客户端收到消息时,除了显示标准的 toast 通知外,客户端应用程序还可以订阅推送通知,以便刷新在途信息。
在某种程度上,我试图像我们在使用 SignalR
更新 Azure 网站LiteDispatch上的“活动调度”页面时看到的那样,使用推送通知:我想要在 Windows 8 上收到“toast”通知,但我也想将此通知用作刷新机制。
| 为本阶段的竞赛,解决方案中添加了两个新项目。您需要 VS 2012 和 Windows 8 才能使用这些项目,因为它们是 Windows 应用商店应用程序程序集。我已将文章链接添加进来,以便可以获取第 5 个挑战的解决方案源代码。 如果您想编译附加代码以用于 Windows 应用商店客户端应用程序,则需要下载适用于 Windows 应用商店应用程序的 Bing Maps SDK。 |
解决方案组件和技术方面
如上所述,在本阶段结束时,解决方案将包括以下组件。
本文的这一部分讨论了以下相关的 Azure Mobile Service 方面。
- Windows 应用商店应用程序 - 开发人员帐户
- 开发中心 - 应用程序配置
- Azure Mobile Services
- 创建服务
- 创建表和脚本
- 配置推送通知
- 客户端开发
- 客户端推送通知设置
- 客户端推送通知通道
- 如何与客户端上的移动服务数据进行交互
- 订阅客户端上的推送通知
- 从外部应用程序调用移动服务 REST 方法
本节涵盖的其他方面
- 开发使用 C#\XAML 的 Windows 8 应用商店应用程序
- 从 W8 应用商店应用程序调用 WebAPI 端点
- 与 Bing Maps 集成
文章的后续部分讨论了如何在 Azure 中配置移动服务,然后解释了客户端需要进行哪些更改才能使推送通知正常工作,以及 Azure 网站的一些附加功能,以便可以使用移动服务 REST 方法创建移动服务数据表中的记录。该部分最后讨论了客户端特定的方面,如 Big Map 集成、调用部署在 Azure 网站上的 WebAPI 端点以及开发 Windows 8 XAML 应用程序的其他相关方面。
运行基础结构
为了在 Azure 中开发移动应用程序并使用推送通知功能,您需要注册 Microsoft 的开发者帐户。这是必需的,因为应用程序必须使用 Windows 应用商店提供的应用程序标识凭据进行配置。因此,为了开始使用本节中使用的技术集,您需要执行以下步骤。
- 获取开发者帐户 - 有关更多详细信息,请参阅 http://msdn.microsoft.com/en-US/windows/ 网页。
- 在 Windows 应用商店应用程序中注册您的应用程序。
- 从您的 Windows 应用商店应用程序获取应用程序凭据值,并将其应用于 Azure Mobile Services。
- 使用 Visual Studio 将应用程序与 Windows 应用商店关联。
因此,在我们开始编写代码之前,还需要采取几个步骤。根据我的经验,为了进行此类应用程序的测试而获取开发者帐户有些麻烦,费用不是很高,但我可以看到很多人只是因为不愿意提供信用卡详细信息而不尝试。我还没有完成整个应用程序发布过程,但到目前为止,我发现 Windows 应用商店门户网站易于使用。我花了一天时间才让应用程序成功处理了通知,这包括了我们稍后将看到的所有高级方面。
| 有一点我想提一下,当我获取开发者帐户时,Microsoft 提供了两种类型的帐户:个人和企业。在功能方面,两种选项之间的区别并不清楚,但到目前为止,我认为个人帐户提供了我所需的所有功能。 |
Windows 应用商店应用程序
关于注册您的应用程序,有几件事您可能不知道。
- 您无需有任何源代码即可注册您的应用程序。
- 一旦声明了应用程序名称,您只需检索客户端密钥详细信息,无需进一步步骤即可开始创建 Azure Mobile Service。
| 在 Windows 应用商店仪表板中,不容易找到应用程序凭据的位置。在“服务”页面中,查找指向Live Services 网站的链接,然后导航到“对服务进行身份验证”页面。在那里,您可以找到在 Azure Mobile Services 仪表板的“推送”部分所需的 SID 和客户端密钥。 |
因此,在开发中心,在应用程序仪表板中,转到“服务”页面,找到指向Live Services 网站的链接。
在 Live Services 页面中,导航到“对服务进行身份验证”部分,然后复制两个密钥,您将在 Azure 门户中用到它们。
此时,我们可以创建一个新的 Azure Mobile Service,如果您正在寻找有关 Windows 应用商店和 Windows 应用商店应用程序开发的更多文档,您可能会发现以下链接很有帮助。
入门 | Windows 应用商店应用程序官方文档 |
创建您的第一个 Windows 应用商店应用程序 | 一个关于如何使用 C# 创建 Windows 应用商店应用程序的教程 |
Channel 9 视频 | 这是一系列视频,指导开发人员构建他们的第一个 Windows 应用商店应用程序。 |
Azure Mobile Services
在 Azure 中设置新的移动服务非常直接。它只需要在门户网站的向导中输入几个参数:移动服务名称以及将使用的 Azure SQL 数据库。这一点在考虑移动服务时需要注意,目前的实现基于 Azure SQL Server,这一点本身就足以让一些人因为在 Azure 上运行 SQL Server 数据库实例的成本而停止使用该服务。如果在不久的将来,服务能够使用其他存储选项(例如表)将会很有益。
就我而言,我正在重新使用 LiteDispatch 应用程序运行的同一个 Azure SQL 实例,向导提供了使用现有实例的选项,因此至少这样一来,如果有一个正在运行的实例可用,就不会产生任何额外费用。采用这种方法时,我在其他应用程序中没有遇到任何问题。以下屏幕显示了新创建的服务实例,我将其命名为 litetracker,与 Windows 应用商店应用程序相同。
我建议查看 Azure 网站上的“在移动服务中入门数据”。它详细讨论了如何使移动服务仪表板在门户网站上提供的示例 Windows 应用商店应用程序项目正常工作。我认为这是熟悉该功能最好的方法。教程易于遵循,并帮助我几乎不费力地使应用程序正常运行。
关于服务有一个关键方面,我花了很长时间才弄清楚。服务主要是公开表以供移动应用程序进行持久化的功能集。简而言之,服务通过围绕这些表公开 REST 方法来实现这一点。脚本和推送通知只是围绕这些基本组件工作的附加功能。
简单来说,移动服务就是将数据存储在云中。通知只是一种服务器指示客户端某些数据已更改的 Fancy 机制,您通过将数据存储在具有某种逻辑触发器(脚本)的表中来实现这一点,该触发器使用服务器到客户端的通道传递可能基于刚刚持久化的记录的信息。一个重要的方面需要考虑:您必须使用移动服务 REST 方法才能执行服务器脚本中包含的通知逻辑;仅仅添加记录到表中是不够的。服务器以类似于 SignalR 的方式发送通知,在服务器和客户端之间创建通道。客户端 URI 是需要保存在某处的数据,如果客户端需要服务器将信息发送回它。如果客户端发起对话,URI 可以在请求中包含,但如果发生外部事件(如跟踪事件),移动服务需要能够检索需要通知的客户端列表。因此,我们所做的是,当客户端启动时,它将 URI 发送到服务器,以便可以将其存储在我们命名的 Channel
表中。
在这个阶段,我发现似乎有太多小的东西需要设置和配置才能使通知正常工作,我感到它很容易变得有些混乱,但是,一旦理解了设计,它就会被证明是有效且灵活的。我之所以提到这一点,是因为我曾一度认为只有移动应用程序才能触发通知,但我错了,事实并非如此,一旦您熟悉了移动服务 API,您就可以使用任何支持 JSON 的 HTTP 客户端来触发通知。我们稍后将看到 Azure 网站 LiteDispatch 如何使用 JSON 和 REST 方法创建称为 DispatchEvent 的实例。
| 移动服务实际上提供了额外的组件和服务,如客户 API(REST 方法)和计划程序,您可以使用它们来发送通知而无需将记录存储在表中,但您仍然需要存储客户端通道以推送通知。在生产环境中,您可能想关注与通道记录相关的某个方面,即目前没有自动机制可以清除数据表中的过时通道记录。 |
让我们从移动服务的角度总结一下我们在通知方面想要实现的目标。
- 首先,我们要创建一个客户端和服务器之间的移动服务通道,如上所述,我们需要在表中存储客户端 URI。我们需要创建一个名为
Channel
的新表来存储这些详细信息。有关如何完成此操作的简单示例,请查看以下链接: 使用移动服务向用户发送推送通知。 - 然后我们需要另一个名为
DispatchEvent
的数据表,以便当发生跟踪事件时,LiteDispatch 应用程序会在该数据表中创建一个记录。 - 创建了一个插入脚本,以便任何注册了通道的客户端都可以在创建新记录时收到通知。同样,向
Channel
表添加了另一个脚本,以避免在客户端启动时插入重复记录。 - 我们还需要配置推送设置,并提供 Windows 应用程序凭据,以便稍后可以将客户端应用程序与移动服务关联。
但在此之前,让我们看看 Azure 门户在移动服务方面提供了哪些开箱即用的功能。
带有示例代码的链接非常好,并提供了一种快速运行某些内容的简单方法,我认为它们在初步探索时很有用。数据和推送这两个菜单是以下文章部分使用的两个功能。
移动服务 - 数据
我们需要创建两个表:Channel
和 DispatchEvent
。创建它们后,数据网页应如下所示。
要创建新表,请导航到数据页面,然后在底部工具栏选择创建
选项,输入表名,不要修改权限的默认值。对于 Channel
表,对话框窗口如下所示。
对另一个表:DispatchEvent
执行完全相同的操作。以下屏幕显示了创建一些通知后的表内容。
| 当在移动服务中创建表时,只会向其中添加一列,即名为 |
移动服务中的脚本
当创建 DispatchEvent
时,我们希望生成通知给客户端。我们将使用 INSERT
脚本来帮助我们做到这一点。在门户网站中,选择数据
,然后在 DispatchEvent
表中选择 SCRIPT
菜单。然后用以下代码替换默认的 INSERT 脚本。
万一您需要上面的代码,您可以使用以下代码。
function insert(item, user, request) {
request.execute({
success: function() {
request.respond();
sendNotifications();
}
});
function sendNotifications() {
var channelTable = tables.getTable('Channel');
channelTable.read({
success: function(channels) {
channels.forEach(function(channel) {
push.wns.sendToastText04(channel.uri, {
text1: item.eventType,
text2: "Truck: " + item.truck,
text3: item.trackingInfo
}, {
success: function(pushResponse) {
console.log("Sent push:", pushResponse);
}
});
});
}
});
}
}
为了避免在 Channel
表中创建多个条目,插入脚本会检查记录是否已存在。
function insert(item, user, request) {
var channelTable = tables.getTable('Channel');
channelTable
.where({ uri: item.uri })
.read({ success: insertChannelIfNotFound });
function insertChannelIfNotFound(existingChannels) {
if (existingChannels.length > 0) {
request.respond(200, existingChannels[0]);
} else {
request.execute();
}
}
}
推送通知移动服务 - 客户端凭据
在查看客户端实现之前,最后要配置的是在门户网站中使用 Windows 应用商店应用程序详细信息设置推送设置。您需要将 Windows 应用程序凭据:客户端密钥和包 SID 从 Windows 应用商店门户添加到您的应用程序仪表板,正如我们在前面关于 Windows 应用商店的部分中所述。
有了这些信息,您就可以在稍后使用 Visual Studio 关联客户端应用程序。使用移动服务页面的推送菜单输入详细信息。
此时,移动服务已配置完毕,我们只需要对新的 W8 客户端:LiteTracker 进行一些更改即可接收通知,并在 Azure 网站应用程序:LiteDispatch 中进行一些修改,以便跟踪事件可以生成条目到移动服务 DispatchEvent
表中。
移动服务 REST API - 如何从外部应用程序调用通知
本节讨论了在 Azure 网站(LiteDispatch)上所需的更改,以便在收到跟踪消息时,它会在我们上一个部分创建的数据移动服务表中创建一个 DispatchEvent 记录。再次回顾一下业务流程。
- 卡车司机的移动设备将 GPS 坐标发送到 Azure 虚拟机应用程序:LiteTracking。
- 使用 Bing Map 服务处理坐标详细信息,以检索距离和 ETA 信息。
- 跟踪详细信息发送到 Azure 网站:LiteDispatch。
上述所有功能在此阶段都已到位,现在我们需要以下功能。
- Azure 网站在移动服务数据表中创建 DispatchEvent 记录。
- 调用插入脚本,并向创建了 Channel 数据表条目的订阅者发送通知。
- 在客户端应用程序:LiteTracker 中生成 Toast 通知。
- 客户端应用程序自动刷新仪表板,显示来自 Azure 网站的最新可用信息。
在本节中,我们涵盖了第四项,即 Azure 网站创建 DispatchEvent 的内容。正如在本阶段部分开头简要提到的,移动服务通过 REST 方法公开数据表。客户端库只是一组围绕向 Azure 中的端点发送 REST 方法的辅助函数,但您也可以使用基本的 HTTP 请求结合 JSON 消息来调用这些端点。以下链接提供了有关移动服务 REST API 和其他相关主题的广泛文档。
移动服务概述 |
.NET 客户端库 |
Windows Azure 移动服务 REST API 参考 |
查询记录操作 |
在我们的情况下,我们需要插入一条记录,因此我的更改基于关于插入记录操作的文档。TrackingAdapter
被我们上面列出的业务事件第 3 点中描述的接收跟踪通知的控制器使用。 TrackingAdapter
中调用的方法现在还调用 CreateDispatchEvent
方法,如下所示。
private TrackingResponseDto CreateTrackingNotificationImpl(IRepositoryLocator locator, TrackingNotificationDto dto)
{
...
// dispatch found and it is valid
response = dispatchNote.CreateTrackingNotification(locator, dto, response);
if (response.Accepted)
{
var dispatchEvent = Mapper.Map<DispatchEventBase>(dispatchNote);
CreateDispatchEvent(dispatchEvent);
}
return response;
}
从刚被跟踪通知修改的 DispatchNote 中,创建一个 DispatchEvent 实例,CreateDispatchEvent
方法的实现如下。
private void CreateDispatchEvent(DispatchEventBase dispatchEvent)
{
using (var client = new WebClient())
{
client.Headers[HttpRequestHeader.ContentType] = "application/json";
client.Headers.Add("X-ZUMO-APPLICATION", "YOUR_APPLICATION_KEY");
client.BaseAddress = @"YOUR_MOBILE_SERVICE_URI";
var json = JsonConvert.SerializeObject(dispatchEvent);
var result = client.UploadString(GetDispatchEventUri(), "POST", json);
}
}
private string GetDispatchEventUri()
{
return "tables/DispatchEvent";
}
因此,在 LiteDispatch 应用程序的情况下,REST 方法 URI 是 https://litetracker.azure-mobile.net/tables/DispatchNote,请求需要设置为 POST 类型,我们需要确保设置头信息,以便指定 JSON 内容类型,并且“X-ZUMO-APPLICATION”自定义头设置为应用程序密钥。然后在发送请求之前,使用 JSON 库序列化对象。
应用程序密钥在 Azure 门户中找到,转到您的移动服务,然后查看底部工具栏,有一个名为管理密钥的选项,如果选中,您应该会获得应用程序和主密钥,以下屏幕显示了 LiteTracker Mobile Service 实例的密钥。
在 LiteTracker Mobile Service 的情况下,“YOUR_MOBILE_SERVICE_URI”是 https://litetracker.azure-mobile.net/。该服务的仪表板在右侧列中显示此信息。
| 上面的代码可以通过几种方式进行改进,您可能希望更改它以便异步运行,并确保异常得到妥善管理。同样,请求 URI 和应用程序密钥应存储在应用程序配置文件或类似文件中。 |
应用上述更改后,当处理跟踪通知时,Azure 网站会调用移动服务中的 REST 方法,并创建一个新的 DispatchEvent 记录。然后会向任何成功订阅移动服务的客户端发送通知。以下几节涵盖了客户端端进行推送通知所需的更改。
移动服务 - 客户端通道管理
客户端需要指示 Azure Mobile Services 可用,以便消息可以转发给它们。Mobile Services 库提供 MobileServiceClient
类,以便客户端可以轻松访问 Mobile Services API 和数据表。通过这种方式,我们可以将推送通知客户端详细信息持久化到 Azure 中,供移动服务稍后转发通知。如前所述,MobileServiceClient
只是一个辅助类,用于访问移动服务公开的 REST 方法。在 Windows 应用商店应用程序的情况下,MobileServices
类确保将推送通知详细信息存储在 Azure 中。
internal class MobileServices
{
public MobileServices()
{
MobileServiceClient = new MobileServiceClient(
"YOUR_MOBILE_SERVICE_URI",
"YOUR_APPLICATION_KEY"
);
AcquirePushChannel();
}
public MobileServiceClient MobileServiceClient { get; private set; }
public PushNotificationChannel CurrentChannel { get; private set; }
private async void AcquirePushChannel()
{
CurrentChannel = await PushNotificationChannelManager.CreatePushNotificationChannelForApplicationAsync();
var channelTable = MobileServiceClient.GetTable<Channel>();
var channel = new Channel { Uri = CurrentChannel.Uri };
await channelTable.InsertAsync(channel);
}
}
上面的代码非常直接,值得一提的是它如何通过 MobileServiceClient.GetTable 方法获取 Channel
表的句柄,然后稍后通过调用 InsertAsyn 方法在新 Azure 表中插入一个新实例。Channel
类只是一个 POCO 类,如下所示。
public class Channel
{
public int Id { get; set; }
[JsonProperty(PropertyName = "uri")]
public string Uri { get; set; }
}
我们之前提到过,但值得再次回顾,Azure 中的表是动态的,因此当第一次保存 Channel
记录时,表会被修改并创建 Uri 列,非常酷。在应用程序启动时创建一个 MobileServices
实例,以便通道,我们之前看到 Azure 端管理了 Azure Channel
表中不会生成重复项。
以上是应用程序在创建 DispatchEvent 时收到 Toast 通知所需的所有更改。只需检查应用程序是否接受 Toast 通知,为此,您需要检查应用程序清单。选择应用程序 UI 选项卡,然后选择“所有图像资产”,然后验证该选项是否已启用。以下屏幕显示了 Visual Studio 中的配置屏幕。
移动服务 - 订阅推送通知
通过我们到目前为止看到的代码更改,客户端应用程序已经能够在那发生跟踪事件时获得 toast 通知。以下屏幕显示了一个示例。
但最好是 toast 通知提供某种机制,以便客户端应用程序可以自动刷新并显示最新信息。PushNotificationChannel
类提供了一个名为 PushNotificationReceived 的事件处理程序,它允许我们实现正在寻找的功能。当仪表板页面创建时,应用程序会订阅此事件并使用以下代码进行处理。
public sealed partial class DispatchNotesPage
{
public DispatchNotesPage()
{
InitializeComponent();
_mapServices = new MapServices(mapTrucks, BaseUri);
App.MobileServices.CurrentChannel.PushNotificationReceived += CurrentChannel_PushNotificationReceived;
}
private async void CurrentChannel_PushNotificationReceived(PushNotificationChannel sender,
PushNotificationReceivedEventArgs args)
{
await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, RefreshDispatchNoteSummaries);
}
}
RefreshDispatchNoteSummaries 方法创建一个 HttpClient 以调用 LiteDispatch 中的 WebAPI 端点,该端点使用以下代码检索在途调度详细信息。
internal class TrackingServices
{
public async Task<IEnumerable<DispatchNoteSummary>> GetDispatchNotes()
{
const string uriString = @"http://litedispatch.azurewebsites.net/api/tracking/ActiveDispatchNotes";
var dispatchNoteSummaries = new List<DispatchNoteSummary>();
using (var client = new HttpClient())
{
using (var response = await client.GetAsync(uriString))
{
if (response.IsSuccessStatusCode)
{
var result = await response.Content.ReadAsStringAsync();
var dispatches = JsonConvert.DeserializeObject<List<DispatchNoteDto>>(result);
dispatches = dispatches.OrderByDescending(d => d.LastUpdate).ToList();
foreach (var dispatchNoteDto in dispatches)
{
dispatchNoteSummaries.Add(DispatchNoteSummary.Create(dispatchNoteDto));
}
}
}
}
return dispatchNoteSummaries;
}
}
| WebAPI 使用 |
一旦新数据可用,用户界面就会修改以更新最新更改,并显示 Bing Map,显示卡车的路线和跟踪详细信息(距离和时间)。以下屏幕显示仪表板屏幕并描述其主要组件。
Bing Map - Windows 应用商店应用程序集成
Windows 应用商店应用程序的一个有趣方面是与 Bing Maps 的集成,当选择一辆卡车时,卡车会根据其最后坐标显示在地图上,然后通过调用 Route REST Bing Map 端点来计算到目的地的路线。
为了提供该功能,创建了一个名为 MapServices
的服务类,它只公开一个公共方法,该方法需要传递一个 DispatchNoteSummary
实例。该方法的实现如下。
private async void SetRoute(Location startLocation, Location endLocation)
{
ClearMap();
//Create the Request URL for the routing service
01
const string request = @"http://dev.virtualearth.net/REST/V1/Routes/Driving?o=json&wp.0={0},{1}&wp.1={2},{3}&rpo=Points&key={4}";
var routeRequest =
new Uri(string.Format(request, startLocation.Latitude, startLocation.Longitude, endLocation.Latitude,
endLocation.Longitude, _mapTrucks.Credentials));
//Make a request and get the response
02 var r = await GetResponse(routeRequest);
if (r != null &&
r.ResourceSets != null &&
r.ResourceSets.Length > 0 &&
r.ResourceSets[0].Resources != null &&
r.ResourceSets[0].Resources.Length > 0)
{
var route = r.ResourceSets[0].Resources[0] as Route;
if (route == null) return;
//Get the route line data
var routePath = route.RoutePath.Line.Coordinates;
var locations = new LocationCollection();
foreach (var t in routePath)
{
if (t.Length >= 2)
{
locations.Add(new Location(t[0], t[1]));
}
}
//Create a MapPolyline of the route and add it to the map
var routeLine = new MapPolyline
{
Color = Colors.Blue,
Locations = locations,
Width = 5
};
_routeLayer.Shapes.Add(routeLine);
//Add start and end pushpins
var start = new Pushpin
{
Text = "S",
Background = new SolidColorBrush(Colors.Green)
};
_mapTrucks.Children.Add(start);
MapLayer.SetPosition(start,
new Location(route.RouteLegs[0].ActualStart.Coordinates[0],
route.RouteLegs[0].ActualStart.Coordinates[1]));
var end = new Pushpin
{
Text = "E",
Background = new SolidColorBrush(Colors.Red)
};
_mapTrucks.Children.Add(end);
MapLayer.SetPosition(end,
new Location(route.RouteLegs[0].ActualEnd.Coordinates[0],
route.RouteLegs[0].ActualEnd.Coordinates[1]));
//Set the map view for the locations
var locationRect = new LocationRect(locations);
03 locationRect.Width += 0.5; locationRect.Height += 0.5;
_mapTrucks.SetView(locationRect);
}
}
关于上述代码有几点观察。在第 01 行和第 02 行,URI 设置为调用 Bing Maps 中的 REST 方法来计算路线。第 3 行只是一个简单的方法,用于确保 Bing Map 窗口提供一些边距,以便起点和终点稍微位于窗口内部,而不是紧贴其边界。
public void RefreshMap(DispatchNoteSummary selectedItem)
public void RefreshMap(DispatchNoteSummary selectedItem)
{
var location = new Location(selectedItem.Latitude, selectedItem.Longitude);
if (location.Latitude == 0 && location.Longitude == 0) return;
SetRoute(location, new Location(52.516071, 13.37698));
var image = new Image
{
Source = new BitmapImage(new Uri(_baseUri, "/images/truck_map.png")),
Width = 40,
Height = 40
};
MapLayer.SetPosition(image, location);
_mapTrucks.SetView(location);
_mapTrucks.Children.Add(image);
}
SetRoute
私有辅助方法可能是最值得关注的方面。它接受起点和终点位置,并在屏幕地图上绘制路线。该操作是异步的,因此不会阻塞,结果是卡车图标通常在路线之前渲染,计算路线可能是一项昂贵的操作,因此尽快渲染卡车是提供更具响应性的用户界面的好技巧。
屏幕缩放 Windows 应用商店应用程序
能够自动缩放到不同屏幕尺寸的应用程序至关重要,即使对于 Windows 应用商店应用程序也是如此,例如,Windows Surface Pro 平板电脑的分辨率为 1920x1080 像素,但其较小的兄弟 Windows Surface RT 的分辨率仅为 1366x768 像素。
为了提供动态用户界面,自动适应 UI 组件的大小和纵横比,XAML Windows 应用商店应用程序使用 VisualStateManager 组件来处理这类需求。
如果您查看 DispatchNotesPage.xaml,您会在文件末尾找到 VisualStateManager
的一个部分,其中声明了几个视觉状态。
- FilledOrNarrow
- FullScreenPortrait
- FullScreenPortrait_Detail
- Snapped
- Snapped_Detail
带有“detail”后缀的视图状态在选择了卡车项时应用,在只有窄显示可用的情况下就是这种情况,那么我们可能需要隐藏一些详细信息,直到选择了某个项目。
DispatchNotePage
页面继承自 LayoutAwarePage,它包含一组辅助方法,以确保在屏幕分辨率更改时更新 ViewState。在此类中,有一个名为“Visual state switching”的区域,其中声明了以下方法。
StartLayoutUpdates | 创建页面时,它会映射应用程序视图和页面的视觉状态,调用此处理程序。此方法的主要职责是设置控件上的视觉状态。 |
InvalidateVisualState | 调用以将页面的 VisualStateManager 设置为从屏幕大小获得的新视觉状态。 |
DetermineVisualState | 将 ApplicationViewState 翻译为字符串,用于页面内的视觉状态管理。 |
然后,页面可以在 XAML 语言中指示控件对于每个视觉状态的变化,例如,当 ViewState 为 FullScreenPortrait 时,会进行以下更改。
这是一个简单的机制,在大多数情况下效果很好,在某些情况下需要额外的增强,并且某些特定行为可能需要控件中的依赖项属性来实现所需的效果。
比赛落幕
这 10 周非常精彩,对我来说是一次很棒的挑战,因为它提供了绝佳的机会让我接触到许多我平时接触不到的功能。我已经在 Azure 上试验了三年,与早期相比,如今可用的功能真是太棒了。多年来唯一不变的几乎就是名字。我认为小型公司利用这类技术来提供几年前只有大公司才能获得的服务非常有价值。我还没有机会接触其他云平台,但我对 Azure 的印象非常深刻。
过去 10 周一直很忙,有很多个深夜,很少睡眠。我希望我有时间研究一些其他的 Azure 组件,特别是服务总线,也许下次吧:)。但我真的很享受它,我会推荐任何人花时间研究这项技术。
我想感谢 CodeProject 团队组织了这次精彩的挑战;这对于我们参赛者来说很辛苦,但我无法想象每两周评估文章所需的工作量。付出了巨大的努力。
以下是一个一分半钟的视频,展示了 LiteTracker 应用程序在 Windows 8 上运行,它模拟了两个卡车通知,并显示了 Toast 通知以及应用程序如何自行刷新。它还显示了一些用户界面功能和 Bing Map 集成。
历史
2013 年 6 月 24 日 - 第五届挑战 - 文章部分已完成 -- 显示新客户端应用程序的视频已添加 2013 年 6 月 21 日 - 第五届挑战 - 已添加 Azure Mobile Services 配置部分
2013 年 6 月 19 日 - 第五届挑战 - 移动服务和 W8 应用商店 - 最后一个阶段的作用域说明
2013 年 6 月 17 日 - 项目荣获挑战 4 奖项
2013 年 6 月 7 日 - 第四届挑战 - 最新的源代码已添加到文章中 -- SignalR 已在 Azure 中实现 -- 挑战文章部分仍需进一步工作
2013 年 6 月 6 日 - 第四届挑战 - LiteTracking/BingMaps/LiteDispath 之间的集成正在 Azure 中使用 VM 工作
2013 年 6 月 5 日 - 第四届挑战 - 在 Azure 中创建 VM 和映像 - 部署 WebAPI 服务应用程序
2013 年 5 月 28 日 - 第四届挑战 - 描述挑战的作用域
2013 年 5 月 25 日 - 第三届挑战 - 已添加 Azure SQL Server 和架构安装部分
2013 年 5 月 21 日 - 第三届挑战 - 迁移部分已完成 -- 仍在处理 Azure SQL 部分
2013 年 5 月 20 日 - 第三届挑战 - WIP 版本
2013 年 5 月 12 日 - 第二届挑战亮点 - 登录屏幕添加了复活节彩蛋效果
2013 年 5 月 7 日 - 文章中添加了第二届挑战部分:构建网站
2013 年 5 月 6 日 - 项目荣获挑战 1 奖项
2013 年 5 月 5 日 - Azure 网站文章已添加到系列中
2013 年 4 月 25 日 - 创建了文章