September 2015

第 30 卷,第 9 期

云连接移动应用 - 借助身份验证和离线支持构建 Xamarin 应用

作者:Kraig Brockschmidt

如八月期刊第一部分(系列共两部分)“使用 Azure Web 应用和 WebJobs 创建 Web 服务” (msdn.microsoft.com/magazine/mt185572)中所述,现在很多移动应用都可以连接到提供有价值且有趣的数据的一个或多个 Web 服务。对这些服务直接发起 REST API 调用并在客户端中处理响应,尽管这非常简单,但此类方法过于消耗电池电量,而且很多服务强制执行了带宽和各类限制。此外,低端硬件的性能也可能会下降。将任务卸载到自定义后端是可行的,如我们在“Altostratus”项目中的演示(我们将在本文中继续讨论)。

第一部分中讨论的 Altostratus 后端定义会定期从 StackOverflow 和 Twitter(只是为了使用两种不同的数据源)中收集和规范化“对话”,并将它们存储到 Microsoft Azure 数据库中。这意味着,客户端所需的确切数据可直接由后端提供,同时可允许我们调整 Azure 中的后端范围,以便为任意数量的客户端提供空间,而不会达到原始提供程序设置的限制。除规范化后端数据以匹配客户端需求外,我们还使用 Web API 优化数据交换,以节约移动设备的稀缺资源。

在本文中,我们将讨论客户端应用的详细信息(请参阅图 1)。首先,我们从应用的体系结构入手,设置整体上下文,然后转入 Xamarin 和 Xamarin.Forms 的使用、使用后端进行身份验证、构建脱机缓存并在 Team Foundation Server (TFS) 和 Visual Studio Online 上使用 Xamarin 进行构建。

运行在 Android 平板电脑(左侧)、Windows Phone(中间)和 iPhone(右侧)上的 Xamarin 移动应用
图 1 运行在 Android 平板电脑(左侧)、Windows Phone(中间)和 iPhone(右侧)上的 Xamarin 移动应用

客户端应用体系结构

客户端应用包含三个主要页面或视图: 配置、主页和项,它们的类将共享这些名称(请参阅图 2)。辅助登录页没有 UI,只是作为 OAuth 提供程序网页的宿主。借助基本的模型-视图-视图模型 (MVVM) 结构,每个主页(“登录”页除外)都具有关联的视图模型类来处理表示项、类别和配置设置(包括身份验证提供程序的列表)的视图和数据模型类之间的绑定关系。

Altostratus 客户端应用的体系结构,显示涉及的主要类的名称(除非有指定,否则项目中的文件将匹配这些类名称)
图 2 Altostratus 客户端应用的体系结构,显示涉及的主要类的名称(除非有指定,否则项目中的文件将匹配这些类名称)

(注意: 通常,开发人员倾向于将 XAML 视图分离到可移植类库 [PCL](与视图模型和其他类分离),让设计人员可以使用 Blend 等工具对这些视图单独执行操作。我们没有进行分离:一来是使项目保留简单的结构;二来是因为 Blend 目前无法与 Xamarin.Forms 中的控件一起使用)。

数据模型始终会填充本地 SQLite 数据库中的这些项目,它们在应用首次运行时已进行了预填充。数据模型中与后端同步的过程非常简单:检索新数据(尽可能少,从而将网络流量减至最低)、使用该数据更新数据库、清除所有旧数据并指示数据模型刷新其对象。这将触发 UI 的更新,这要归功于数据与视图模型进行了绑定。

我们稍后会探讨,多个不同事件将触发同步:UI 中的刷新按钮、更改配置、对后端进行身份验证(将检索以前保存的配置)以及在至少 30 分钟后恢复应用等。当然,同步是通过后端的 Web API 与后端的主要通信,但是后端还提供 API 以用于注册用户,检索经过身份验证的用户设置,并在经过身份验证的用户更改配置之后更新这些设置。

适用于跨平台客户端的 Xamarin.Forms

您可能已经在 MSDN 杂志中了解到,可以通过 Xamarin 使用 C# 和 Microsoft .NET Framework 通过在各个平台共享的大量代码构建适用于 Android、iOS 和 Windows 的应用。(有关我们的开发配置的概述,请参阅后面的部分“针对 TFS 和 VSO 使用 Xamarin 进行构建”。)通过为 XAML/C# 提供常见的 UI 解决方案,Xamarin.Forms 框架对此做出了进一步的贡献。借助 Xamarin.Forms,Altostratus 项目在单个 PCL 中共享了该项目的 95% 以上的代码。事实上,该项目中仅特定于平台的代码是启动位,其有以下几个来源:项目模板、处理 Web 浏览器控件的登录页的呈现器,以及用于将预填充的 SQLite 数据库复制到本地存储中的相应读写位置的几行代码。

请注意,还可以对 Xamarin 项目进行配置以使用共享项目,而非 PCL。但是,Xamarin 建议使用 PCL,而且使用 Altostratus 的主要优点在于,我们可以在 Win32 控制台应用程序中使用同一 PCL 来创建预填充数据库。这意味着,我们无需为此复制任何数据库代码,初始值设定项程序将始终与应用的其余部分保持同步。

请注意,共享代码并没有明显减少在每个目标平台上完全测试应用的工作量;如果您以本机方式编写每个应用,则流程的该部分将花费同样长的时间。此外,由于 Xamarin.Forms 是新事物,您可能会发现特定于平台的 bug 或您需要在代码中处理的其他行为。有关我们在编写 Altostratus 时发现的一些行为的详细信息,请访问 bit.ly/1g5EF4j 查看文章。

如果您遇到奇怪的问题,应首先前往 Xamarin bug 数据库 (bugzilla.xamarin.com)。如果在其中找不到关于问题的讨论,可以将问题发布到 Xamarin 论坛 (forums.xamarin.com),您会发现 Xamarin 员工很乐意回复您的问题。

也就是说,与详细了解每个单独平台的 UI 层相比,处理此类个别问题的工作量相当少。而且,由于 Xamarin.Forms 相对来说属于新事物,发现此类问题有助于使框架越来越强大。

特定于平台的调整

在 Xamarin.Forms 中,有时很有必要对一个平台或其他平台进行调整,如微调布局。(有关示例,请参阅 Charles Petzold 的优秀书籍《Programming Mobile Apps with Xamarin.Forms》[bit.ly/1H8b2q6])。 您可能还需要处理一些不一致行为,如 webview 元素触发它的第一个导航事件(同样,有关我们遇到的一些不一致行为,请访问 bit.ly/1g5EF4j 查看我们的文章)。

为此,Xamarin.Forms 包含 API Device.OnPlatform<T>(iOS_valueAndroid_value, Windows_value) 和匹配的 XAML 元素。可以推测出,OnPlatform 根据当前运行时返回不同的值。例如,以下 XAML 代码隐藏了 Windows Phone 上配置页面的登录控件,因为 Xamarin.Auth 组件尚且不支持该平台,因此我们始终运行未经身份验证的 (configuration.xaml):

<StackLayout Orientation="Vertical">
  <StackLayout.IsVisible>
    <OnPlatform x:TypeArguments="x:Boolean" Android="true" iOS="true"
      WinPhone="false" />
  </StackLayout.IsVisible>
  <Label Text="{ Binding AuthenticationMessage }" FontSize="Medium" />
  <Picker x:Name="providerPicker" Title="{ Binding ProviderListLabel }"
    IsVisible="{ Binding ProviderListVisible }" />
  <Button Text="{ Binding LoginButtonLabel}" Clicked="LoginTapped" />
</StackLayout>

谈到组件,Xamarin 本身主要是由提取本机平台常见功能的组件构建而成,其中很多组件先于 UI 的 Xamarin.Forms。其中某些组件内置于 Xamarin 中,而包括社区贡献在内的其他组件需要从 components.xamarin.com 获取。除 Xamarin.Auth 外,Altostratus 还使用连接性插件 (tinyurl.com/xconplugin) 显示指示器,并在设备离线时禁用刷新按钮。

我们发现在设备上更改连接(反映在插件的 IsConnected 属性中)与插件触发其事件之间始终存在一些延迟。这意味着,在设备离线与刷新按钮更改为禁用状态之间有几秒钟时间。为了处理此问题,我们使用刷新命令事件来查看插件的 IsConnected 状态。如果为离线,则立刻禁用按钮,但会设置一个标志来指示 ConnectivityChanged 处理程序在恢复连接时自动启动同步。

Altostratus 还使用 Xamarin.Auth (tinyurl.com/xamauth) 来通过 OAuth 处理身份验证的详细信息,我们即将讨论此内容。这里需要注意的是,该组件目前只支持 iOS 和 Android,不支持 Windows Phone,而且也不在我们修复特定缺陷的项目范围内。幸运的是,该客户端应用在未经身份验证的情况下仍可以正常运行,这表明用户的设置没有保留在云中,而且与后端的数据交换没有完全优化。组件获取更新可以支持 Windows 之后,我们只需删除之前所示的 XAML 中的 OnPlatform 标记,将登录控件设置为可见。

后端身份验证

从整体角度来说,在 Altostratus 应用程序中,我们想要演示在后端存储一些特定于用户的首选项所涉及的机制,以便在处理 HTTP 请求时后端可以自动应用这些首选项。当然,对于此特定的应用程序,我们本可以使用 URI 参数和请求获得相同的结果,但此类示例不能作为更加复杂的方案的基础。在服务器上存储首选项还可以让它们在用户的所有设备之间漫游。

使用任何类型的特定于用户的数据意味着对该唯一的用户进行身份验证。请注意,这与授权不同。身份验证是识别用户并对用户身份进行验证的一种方法。另一方面,授权与任何特定用户(例如,管理员、普通用户、来宾)具备的权限有关。

对于身份验证,我们通过 Google 和 Facebook 等第三方使用社交登录名,而非实现自己的凭据系统(而且后端包含 API,可由客户端应用用于检索配置页面 UI 的提供程序列表)。社交登录名的主要优点在于,我们不必处理凭据或其随附的安全和隐私问题;后端将只会存储电子邮件地址作为用户名,而且客户端只在运行时管理访问令牌。否则,提供程序需要执行所有这些繁重的任务,包括电子邮件验证、密码检索等。

当然,并非所有人都有社交登录名提供程序的帐户,出于隐私的原因,某些用户不想使用社交媒体帐户。同样,社交登录名可能不适用于业务线应用;对于这类情况,我们建议使用 Azure Active Directory。不过,对我们来说,这是一个逻辑选择,因为我们只需要通过某些方式来对单个用户进行身份验证。

通过身份验证之后,用户有权在后端保存首选项。如果我们想要实现其他级别的授权(如修改其他用户的首选项),后端可以根据权限数据库检查用户名。

在 ASP.NET Web API 中为社交登录名使用 OAuth2 OAuth2 (bit.ly/1SxC1AM) 是一个授权框架,允许用户在没有共享其凭据的情况下授予对资源的访问权限。它定义了多个“凭据流”,用于指定凭据在各实体之间的传递方式。ASP.NET Web API 使用所谓的“隐式授予流”,移动应用在其中既不收集凭据,也不存储任何密码信息。该工作由 OAuth2 提供程序和 ASP.NET 标识库 (asp.net/identity) 分别来完成。

若要启用社交登录名,您必须通过应用开发人员门户将您的应用程序注册到每个登录提供程序。(此上下文中的“应用程序”表示所有客户端体验,包括移动和 Web,且不局限于移动应用)。注册后,提供程序将为您提供一个唯一的客户端 ID 和密码。有关示例,请参阅 bit.ly/1BniZ89

我们使用这些值来初始化 ASP.NET 标识中间件,如图 3 中所示。

图 3 初始化 ASP.NET 标识中间件

var fbOpts = new FacebookAuthenticationOptions
{
  AppId = ConfigurationManager.AppSettings["FB_AppId"],
  AppSecret = ConfigurationManager.AppSettings["FB_AppSecret"]
};
fbOpts.Scope.Add("email");
app.UseFacebookAuthentication(fbOpts);
var googleOptions = new GoogleOAuth2AuthenticationOptions()
{
  ClientId = ConfigurationManager.AppSettings["GoogleClientID"],
  ClientSecret = ConfigurationManager.AppSettings["GoogleClientSecret"]
};
app.UseGoogleAuthentication(googleOptions);

类似于“FB_AppID”的字符串是有关 Web 应用的环境和配置设置的密钥,其中存储了实际 ID 和密码。您可以进行更新,而无需重新构建和重新部署应用。

使用 Xamarin.Auth 处理凭据流总体而言,社交身份验证过程涉及应用、后端和提供程序之间的各种握手。幸运的是,Xamarin.Auth 组件(可从 Xamarin 组件存储中获取)为应用处理了大部分任务。这包括使用浏览器控件并提供回调挂钩,以便应用可在授权完成后作出回应。

在初始状态下,Xamarin.Auth 可让客户端应用执行部分授权事宜,这意味着该应用会存储客户端 ID 和密码。虽然这与 ASP.NET 流稍有不同,但 Xamarin.Auth 包含构造良好的类层次结构。Xamarin.Auth.O­Auth2Authenticator 类派生自 WebRedirectAuthenticator,为我们提供了所需的基本功能,仅需编写少量的其他代码,可从 Android 和 iOS 项目(Xamarin.Auth 尚不支持 Windows)中的 LoginPageRenderer.cs 文件中找到。有关我们此处执行的操作的更多详细信息,请访问 tinyurl.com/kboathalto 查看我们的博客文章。

然后,客户端应用具有派生自 Xamarin.Forms 的基本 ContentPage 的 LoginPage 类,允许我们导航到该位置。此类公开两种方法:CompleteLoginAsync 和 CancelAsync,这两种方法根据用户在提供程序的 Web 界面中执行的操作从 LoginPageRenderer 代码中调用。

发送经过身份验证的请求 成功登录之后,客户端应用会拥有一个访问令牌。若要发出经过身份验证的请求,则请求仅包含如下 Authorization 标头中的该令牌:

GET http://hostname/api/UserPreferences HTTP/1.1
Authorization: Bearer I6zW8Dk...
Accept: */*
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64; Trident/7.0; rv:11.0) like Gecko

此处,“Bearer”指示使用持有者令牌的授权,其后是较长的不透明的令牌字符串。

我们为包含自定义消息处理程序的所有 REST 请求使用 System.Net.Http.HttpClient 库,以将身份验证标头添加到每个请求中。消息处理程序是插件组件,允许您检查并修改 HTTP 请求和响应消息。若要了解更多信息,请参阅 bit.ly/1MyMMB8

消息处理程序在类 Authentication­MessageHandler (webapi.cs) 中实现,并在创建 HttpClient 实例时安装:

_httpClient = HttpClientFactory.Create(
  handler, new AuthenticationMessageHandler(provider));

ITokenProvider 接口只是处理程序从应用中获取访问令牌的一种方法(这在 model.cs 中的 UserPreferences 类中实现)。针对每个 HTTP 请求调用 SendAsync 方法;如图 4 所示,如果令牌提供程序包含一个可使用的标头,则它将添加 Authorization 标头。

图 4 添加 Authorization 标头

public interface ITokenProvider
{
  string AccessToken { get; }
}
class AuthenticationMessageHandler : DelegatingHandler
{
  ITokenProvider _provider;
  public AuthenticationMessageHandler(ITokenProvider provider)
  {
     _provider = provider;
  }
  protected override Task<HttpResponseMessage>
    SendAsync(HttpRequestMessage request,
    System.Threading.CancellationToken cancellationToken)
  {
    var token = _provider.AccessToken;
    if (!String.IsNullOrEmpty(token))
    {
      request.Headers.Authorization =
        new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
    }
    return base.SendAsync(request, cancellationToken);
  }
}

如本文第一部分中所述,如果在后端收到令牌和请求,则该令牌可用于检索该用户的首选项,并自动将它们应用到请求的其余部分。例如,如果用户将会话限制设置为 25(而非默认的 100),则最多和请求一起返回 25 个项,从而节省网络带宽。

为后端数据构建脱机缓存

与移动 Web 相比,移动应用的一大优势是能够灵活支持脱机使用情况。根据定义,移动应用始终呈现在用户的设备上,并且可以使用各种数据存储选项(如 SQLite)来保留脱机数据缓存,而不是依赖基于浏览器的机制。

事实上,Altostratus 移动客户端的 UI 只使用在本地 SQLite 数据库中保留的数据,这就使应用在不连接的情况下完全正常运行。当存在连接时,后台进程会从后端检索当前数据以更新数据库。这会更新位于数据库之上的数据模型对象,这反过来会通过数据绑定触发 UI 更新(可以返回图 2 查看相关内容)。这样一来,它就非常类似于第一部分中讨论的后端体系结构,其中正在运行的 webjobs 收集、规范化并在 SQL Server 数据库中存储数据,以便 Web API 可以直接从该数据库中提供请求。

Altostratus 的脱机支持涉及三个不同的任务:

  • 将预填充的数据库文件直接置于应用程序包中,以便在首次运行时即可提供工作数据,而无需连接。
  • 为在 UI 中出现的数据模型的各个部分实现单向同步过程:会话(项)、类别、身份验证提供程序和用户首选项。
  • 将同步挂接到除 UI 中的刷新按钮之外的相应触发器。

让我们逐个进行了解。

创建预填充的数据库 用户可以从在线商店中安装应用,但在设备脱机后,需要一段时间后才能运行。问题是,您是希望应用告知:“抱歉,只有在联机状态下才能执行有用的操作”? 还是希望应用可以智能地处理此类情况?

为了演示后一种方法,Altostratus 客户端直接在每个平台上的应用程序包中加入了一个预填充的 SQLite 数据库(位于每个平台项目的资源/原始文件夹)。在首次运行时,客户端会将此数据库文件复制到设备上的读写位置,然后以独占的方式在该位置中使用它。因为每个平台对应唯一的文件复制过程,我们在运行时使用 Xamarin.Forms.DependencyService 功能来解析定义为 ISQLite 的接口的具体实现。DataAccessLayer 构造函数 (DataAccess.cs) 中将发生这种情况,然后调用 ISQLite.GetDatabasePath 来检索复制后的读写数据库文件的特定于平台的位置(如图 5 中所示)。

图 5 检索数据库文件的特定于平台的位置

public DataAccessLayer(SQLiteAsyncConnection db = null)
{
  if (db == null)
  {
    String path = DependencyService.Get<ISQLite>().GetDatabasePath();
    db = new SQLiteAsyncConnection(path);               
    // Alternate use to use the synchronous SQLite API:
    // database = SQLiteConnection(path);               
  }
  database = db;
  _current = this;
}

为创建初始数据库,Altostratus 解决方案包含较小的 Win32 控制台应用程序,称为 DBInitialize。它使用相同的共享 PCL 作为应用来使用数据库,因此绝不会出现第二个不匹配的基本代码的问题。但是,DBInitialize 无需使用 DependencyService: 它可以直接创建文件并打开连接:

string path = "Altostratus.db3";
SQLiteAsyncConnection conn = new SQLiteAsyncConnection(path);
var dbInit = new DataAccessLayer(conn);

此处,DBInitialize 调用 DataAccessLayer.InitAsync 来创建表(应用从未对预填充的数据库执行此类操作)并使用其他 DataAccessLayer 方法从后端获取数据。请注意,借助异步调用,DBInitialize 只使用 .Wait,因为它是一个控制台应用程序,无需担心响应式 UI:

DataModel model = new DataModel(dbInit);
model.InitAsync().Wait();
model.SyncCategories().Wait();
model.SyncAuthProviders().Wait();
model.SyncItems().Wait();

为提供用户可查看的内容,它使用计时器每隔半秒在屏幕上放置一个点。

请注意,您始终要使用 DB Browser for SQLite (bit.ly/1OCkm8Y) 等工具来查看预填充的数据库,然后将其导入项目中。一个或多个 Web 请求可能会失败,在这些情况下,数据库将无效。您可以将此逻辑构建到 DBInitialize 中,以便它删除数据库并显示错误。在本例中,我们只是监视错误消息并根据需要再次运行程序。

您可能会问:“预填充数据库的内容是否不会相对较快地过时? 我不希望我的应用用户在首次运行时看到陈旧的数据!” 当然,如果您不定期更新应用,确实会出现以上情况。因此,您要提交应用的定期更新,其中包括数据库以及相对较新的数据(这取决于您的数据的性质)。

如果用户已安装该应用,则更新不会产生任何影响,因为复制打包的数据库文件的代码会查看读写副本是否已存在,然后直接使用该副本。但是,如果只是检查文件是否存在,则一段时间内未运行应用的用户可以停止启动包含旧数据(而不是更近时间内更新的程序包中的数据)的应用。您可以查看缓存数据库的时间戳是否比程序包内的时间戳更早,并使用较新的副本覆盖该缓存。虽然这并非在 Altostratus 中实现的内容,但我们也需要保留现有数据库中的用户首选项信息。

将脱机缓存与后端同步 如前面提到的,Altostratus 客户端会始终针对它的缓存数据库来运行。发送到后端的所有 Web 请求(上载新的用户首选项除外)在同步缓存的上下文中发起。其适用机制作为 DataModel 类的一部分来实现,尤其是通过 sync.cs 中的四种方法实现: SyncSettings(委托给 model.cs 中的 Configuration.ApplyBack endConfiguration)、SyncCategories、SyncAuthProviders 和 SyncItems。很显然,这四种方法中的主力是 SyncItems,我们将在下一部分中探讨是什么触发了这四种方法。

请注意,Altostratus 只朝一个方向同步数据,从后端到本地缓存。此外,我们知道我们感兴趣的数据不会更新得太快(考虑到后端 webjobs 的计划),因此我们只会关心与后端数据存储的最终一致性,而不是按照分钟或秒的顺序进行更新。

从本质上来说,每个同步过程都是相同的: 从后端检索当前数据,使用新值更新本地数据库,从数据库中删除所有旧值,然后重新填充数据模型对象以触发 UI 更新。

对于项,还需要执行一些操作,因为可从 UI 中调用 SyncItems,并且我们想防止过于激动的用户重复按下按钮。专用 DataModel.syncTask 属性指示是否存在活动项同步;如果 syncTask 为非 null,则 SyncItems 会忽略重复请求。此外,由于项请求可能需要一段时间并且涉及较大的数据集,因此,我们想在设备处于脱机状态的情况下取消项同步。为此,我们保存任务的 System.Threading.CancellationToken。

图 6 中所示,专用 SyncItemsCore 方法是此过程的核心。它从数据库中检索上次同步的时间戳,并随 Web 请求加入此时间戳。

图 6 专用 SyncItemsCore 方法

private async Task<SyncResult> SyncItemsCore()
{
  SyncResult result = SyncResult.Success;
  HttpResponseMessage response;
  Timestamp t = await DataAccessLayer.Current.GetTimestampAsync();
  String newRequestTimestamp =
    DateTime.UtcNow.ToString(WebAPIConstants.ItemsFeedTimestampFormat);
  response = await WebAPI.GetItems(t, syncToken);
  if (!response.IsSuccessStatusCode)
  {
    return SyncResult.Failed;
  }
  t = new Timestamp() { Stamp = newRequestTimestamp };
  await DataAccessLayer.Current.SetTimestampAsync(t);
  if (response.StatusCode == System.Net.HttpStatusCode.NoContent)
  {
    return SyncResult.NoContent;
  }
  var items = await response.Content.ReadAsAsync<IEnumerable<FeedItem>>();
  await ProcessItems(items);
  // Sync is done, refresh the ListView data source.
  await PopulateGroupedItemsFromDB();
  return result;
}

通过执行此操作,后端仅返回给定时间之后的新项或更新项。因此,客户端只获取它真正需要的数据,这就节省了用户可能有限的数据计划。这也意味着,客户端在处理每个请求中的数据时工作量较少,这也节省了电池能量并可将网络流量降至最低。尽管听起来不是很多,但处理每个类别的 5 个会话(而不是每个请求的 50 个)减少了 90% 的工作量。如果每天进行 20 到 30 次同步,那么一个应用一个月时间内可轻松累积到数百兆字节。简言之,您的客户肯定会感激您在优化流量方面做出的努力!

请求返回之后,ProcessItems 方法将所有这些项添加到数据库,对标题进行一些清理工作(如删除智能引号),并提取有关说明正文的前 100 个字符以在主列表中显示。我们可以让后端执行标题清理操作,这可以节省客户端上的一些处理时间。我们选择将其留在客户端上是因为其他方案需要执行特定于平台的调整。我们还可以让后端创建 100 个字符的说明,这意味着可节省客户端的一些处理时间,但会增加网络流量。对于我们在此处使用的数据,这可能是一种折中方案,由于 UI 最终是客户端的职责,因此让客户端来执行此步骤似乎是更好的做法。(有关此内容和其他 UI 注意事项的更多详细信息,请访问 tinyurl.com/kboathaltoxtra 参阅我们的博客文章)。

将项添加到数据库之后,会通过 PopulateGroupedItemsFromDB 刷新数据模型组。这里,数据库包含的项可能多于用户当前会话限制设置所需的项,了解这一点非常重要。通过将该限制直接应用于数据库查询,PopulateGroupedItemsFromDB 证明了这一点。

不过,随着时间的推移,我们不希望数据库通过保留大量永远不再显示的项来不断扩展。为此,SyncItems 调用 DataAccessLayer.ApplyConversationLimit 方法来选择数据中的旧项,直到项的数量满足指定限制。因为 Altostratus 数据集内的所有单个项的大小都相对较小,因此我们使用最大会话限制 100,无需考虑用户的当前设置。这样一来,如果用户提高该限制,我们就无需再次从后端中请求数据。但是,如果我们的数据项较大,则最好主动选择数据库并根据需要重新请求项。

同步触发器 很显然,UI 中的刷新按钮是项同步发生的主要方式,但是其他同步进程何时会发生? 项同步是否有其他触发器?

就代码来说,回答这些问题需要一些技巧,因为对 Sync* 方法的调用都发生在同一位置,即 HomeViewModel.Sync 方法。但是,此方法和辅助入口点 HomeViewModel.CheckTimeAndSync 都是从其他不同位置调用的。以下是通过 SyncExtent 枚举中的值对同步调用进行参数化的时间、位置和方式的摘要:

  • 启动之后,HomeViewModel 构造函数使用一劳永逸的模式调用 Sync(SyncExtent.All),因此同步完全是在后台发生的。该模式只表示将异步方法中的返回值保存到本地变量中,以禁止有关未使用 await 的编译器警告。
  • 在 Connectivity 插件的 ConnectivityChanged 事件的处理程序的内部,当执行之前的调用(使用与当时请求时相同的盘区)时,我们会在设备处于脱机状态时调用 Sync。
  • 如果用户访问配置页面并更改活动类别或会话限制,或登录后端并应用后端设置,则 DataModel.Configuration.HasChanged 标志会记住这一事实。当用户返回到主页时, HomePage.OnAppearing 处理程序会调用 HomeViewModel.CheckRefresh,其会检查 HasChanged 并根据需要调用 Sync(SyncExtent.Items)。
  • App.OnResume event (app.cs) 调用 CheckTimeAndSync,其会应用某些逻辑,根据应用挂起时间来确定同步内容。很明显,这些条件高度依赖您的数据和后端操作的性质。
  • 最后,刷新按钮调用带有标志的 CheckTimeAndSync,以始终至少执行一个项同步。刷新按钮使用 CheckTimeAndSync,因为用户可能(尽管可能性很小)使应用程序在前台运行半小时以上,甚至一天,在这种情况下,刷新按钮还应执行我们在恢复时执行的其他同步。

将所有内容整合到 HomeViewModel.Sync 中的一个好处是它可以在适当的时间设置公共 HomeViewModel.IsSyncing 属性。此属性是数据绑定到 Home.xaml 中的 Xamarin.Forms.ActivityIndicator 的 IsVisible 和 IsRunning 属性。设置此标志的简单行为可控制该指示器的可见性。

在 TFS 和 Visual Studio Online 上使用 Xamarin 进行构建

对于 Altostratus 项目,我们使用了在某种程度上跨平台工作的通用开发环境:安装仿真器的 Windows PC、Android 和 Windows Phone 的受限设备以及安装 iOS 仿真器和受限 iOS 设备的本地 Mac OS X 计算机(请参阅图 7)。通过此类设置,您可以使用 Mac OS X 计算机进行远程 iOS 构建和调试,从而直接在 PC 上的 Visual Studio 中执行所有开发和调试工作。此外,还可以从 Mac 中提交存储就绪的 iOS 应用。

Xamarin 项目的通用跨平台开发环境以及使用 Visual Studio Tools for Apache Cordova 等其他技术的开发环境
图 7 Xamarin 项目的通用跨平台开发环境以及使用 Visual Studio Tools for Apache Cordova 等其他技术的开发环境

我们为进行团队协作和源控制部署了 Visual Studio Online,并对其进行配置,为后端和 Xamarin 客户端执行连续集成生成。如果我们立即启动此项目,将能够使用最新的 Visual Studio Online 生成系统来直接在托管生成控制器中构建 Xamarin 应用。有关更多信息,请访问 tinyurl.com/kboauthxamvso 查看我们的博客文章。不过,2015 年早些时候,托管生成控制器尚且不具备此项支持。幸运的是,将运行 TFS 的本地计算机用作 Visual Studio Online 的生成控制器确实是一件非常简单的事情。在该服务器上,我们安装了 Xamarin 随附的免费 TFS Express 版本以及必要的 Android 和 Windows 平台 SDK,并确保将 Android SDK 存放到 c:\android-sdk 等生成帐户可以访问的位置。(默认情况下,它的安装程序将 SDK 存放到当前用户的存储中,生成帐户没有权限访问)。 bit.ly/1OhQPSW 中的 Xamarin 文档“配置 Team Foundation Server for Xamarin”有关于这一点的讨论。

完全配置了生成服务器之后,通过以下步骤连接到 Visual Studio Online(请参阅 bit.ly/1RJS4QL 中的“部署和配置生成服务器”):

  1. 打开 TFS 管理控制台。
  2. 在左侧导航窗格中,展开服务器名称,然后选择“生成配置”。
  3. 在生成服务下,单击“属性”,打开“生成服务属性”对话框。
  4. 单击对话框顶部的“停止服务”。
  5. 在“通信”下,在“为项目集合提供生成服务”下的框中输入您的 Visual Studio Online 集合的 URL,例如,https://<您的帐户>.visualstudio.com/defaultcollection。
  6. 单击对话框底部的“启动”按钮重启服务。

这就完成了所有操作! 当您在 Visual Studio 团队资源管理器 中创建生成定义时,连接到 Visual Studio Online 的 TFS 计算机将显示在可用的生成控制器的列表中。选择该选项后,您从 Visual Studio 中排队或在签入后排队的生成将被路由到 TFS 计算机。

总结

我们希望您会喜欢我们对于 Altostratus 项目的探讨,并且您会发现这些代码适用于您自己的移动云连接应用。同样,我们对于此项目的目标是为跨平台移动应用的清晰示例提供一个自定义后端,该后端可以代表客户端执行重要的工作以优化需要客户端直接执行的工作。通过让始终运行的后端代表所有客户端收集数据,我们极大地减少了客户端生成的网络流量(及其对数据计划的后续影响)。通过规范化来自不同源的数据,我们尽可能减少客户端上的必要的数据处理,这节省了日益重要的电池电量。通过使用后端进行身份验证,我们演示了一些可用方法,在后端存储用户首选项并自动将它们应用到客户端与后端的互动中,这同样优化了网络流量和处理要求。我们知道,对于我们的特定要求,有更加简便的方法可以达到相同的效果,但我们想创建一个可扩展到更加复杂的方案的示例。

我们乐于听取您对于此项目的意见和建议。请向我们提供反馈!

Azure 脱机同步

实现自己的脱机缓存的一个替代方法是表的 Azure 脱机同步,这是 Azure 移动服务的一部分。这将完全没有必要编写任何同步代码,并且无需执行将客户端中的更改推送到服务器的操作。但是,因为它使用表存储,因此不像 SQLite 一样提供相关的数据模型。


Kraig Brockschmidt担任 Microsoft 高级内容开发人员,侧重于跨平台移动应用。他是 Microsoft Press 出版的《Programming Windows Store Apps with HTML, CSS and JavaScript》(两个版本)和 kraigbrockschmidt.com 上的博客的作者。

Mike Wasson是 Microsoft 的内容开发人员。多年来,他一直负责撰写 Win32 多媒体 API 的文档。目前,他正在撰写 Microsoft Azure 和 ASP.NET。

Rick Anderson担任 Microsoft 的高级编程人员,侧重于 ASP.NET MVC、Microsoft Azure 和实体框架。您可以通过 Twitter 关注他:twitter.com/RickAndMSFT

Erik Reitan是 Microsoft 的高级内容开发人员。他侧重于 Microsoft Azure 和 ASP.NET。通过 Twitter 关注他:twitter.com/ReitanErik

Tom Dykstra是 Microsoft 的高级内容开发人员,侧重于 Microsoft Azure and ASP.NET。

衷心感谢以下技术专家对本文的审阅: Michael Collier、Brady Gaster、John de Havilland、Ryan Jones、Vijay Ramakrishnan 和 Pranav Rastogi