概述
本指南介绍如何使用适用于 Windows 和 Xamarin 应用的 Azure 应用服务移动应用的托管客户端库执行常见方案。 如果你不熟悉移动应用,应考虑先完成 Azure 移动应用快速入门 教程。 本指南重点介绍客户端托管 SDK。 若要详细了解移动应用的服务器端 SDK,请参阅 .NET Server SDK 或 Node.js 服务器 SDK 的文档。
参考文档
客户端 SDK 的参考文档位于此处: Azure 移动应用 .NET 客户端参考。 还可以在 Azure-Samples GitHub 存储库中找到多个客户端示例。
支持的平台
.NET 平台支持以下平台:
- 适用于 API 19 到 24 的 Xamarin Android 版本(KitKat 到 Nougat)
- 适用于 iOS 8.0 及更高版本的 Xamarin iOS 版本发布
- 通用 Windows 平台
- Windows Phone 8.1
- Windows Phone 8.0(Silverlight 应用程序除外)
“服务器流”身份验证使用 WebView 来呈现界面。 如果设备无法显示 WebView UI,则需要其他身份验证方法。 因此,此 SDK 不适用于监视类型或类似受限的设备。
安装与先决条件
我们假设你已创建并发布移动应用后端项目,其中包括至少一个表。 在本主题中使用的代码中,表的名称 TodoItem
如下: Id
、 Text
和 Complete
。 此表是在完成 Azure 移动应用快速入门时创建的同一个表。
C# 中的相应类型化客户端类型如下:
public class TodoItem
{
public string Id { get; set; }
[JsonProperty(PropertyName = "text")]
public string Text { get; set; }
[JsonProperty(PropertyName = "complete")]
public bool Complete { get; set; }
}
JsonPropertyAttribute 用于定义客户端字段和表字段之间的 PropertyName 映射。
若要了解如何在移动应用后端中创建表,请参阅 .NET Server SDK 主题 或 Node.js Server SDK 主题。 如果使用快速入门在 Azure 门户中创建了移动应用后端,还可以在 Azure 门户中使用简易表设置。
如何:安装托管客户端 SDK 包
使用以下方法之一从 NuGet 安装移动应用的托管客户端 SDK 包:
-
Visual Studio 右键单击项目,单击“ 管理 NuGet 包”,搜索
Microsoft.Azure.Mobile.Client
包,然后单击“ 安装”。 -
Xamarin Studio 右键单击项目,单击“ 添加>NuGet 包”,搜索
Microsoft.Azure.Mobile.Client
包,然后单击“ 添加包”。
在主活动文件中,请记得添加以下 using 语句:
using Microsoft.WindowsAzure.MobileServices;
注释
请注意,Android 项目中引用的所有支持包必须具有相同的版本。 SDK 具有 Xamarin.Android.Support.CustomTabs
Android 平台的依赖项,因此,如果项目使用较新的支持包,则需要直接安装具有所需版本的此包以避免冲突。
如何:在 Visual Studio 中使用调试符号
Microsoft.Azure.Mobile 命名空间的符号在 SymbolSource 上可用。 请参阅 SymbolSource 说明,将 SymbolSource 与 Visual Studio 集成。
创建移动应用客户端
以下代码创建用于访问移动应用后端的 MobileServiceClient 对象。
var client = new MobileServiceClient("MOBILE_APP_URL");
在前面的代码中,将 MOBILE_APP_URL
替换为移动应用后端的 URL,该 URL 可在 Azure 门户中的移动应用后端的刀片视图中找到。 MobileServiceClient 对象应为单一实例。
处理表格
以下部分详细介绍了如何搜索和检索记录并修改表中的数据。 下面介绍了以下主题:
- 创建表引用
- 查询数据
- 筛选返回的数据
- 对返回的数据进行排序
- 按页返回数据
- 选择特定列
- 按 ID 查找记录
- 处理非类型化查询
- 插入数据
- 更新数据
- 删除数据
- 冲突解决和乐观并发
- 绑定到 Windows 用户界面
- 更改页面大小
如何创建表引用
访问或修改后端表中的数据的所有代码都会调用对象上的 MobileServiceTable
函数。 通过调用 GetTable 方法获取对表的引用,如下所示:
IMobileServiceTable<TodoItem> todoTable = client.GetTable<TodoItem>();
返回的对象使用类型化序列化模型。 还支持非类型化序列化模型。 以下示例 创建对非类型化表的引用:
// Get an untyped table reference
IMobileServiceTable untypedTodoTable = client.GetTable("TodoItem");
在非类型化查询中,必须指定基础 OData 查询字符串。
如何:从移动应用查询数据
本部分介绍如何向移动应用后端发出查询,其中包括以下功能:
注释
强制实施服务器驱动的页面大小,以防止返回所有行。 分页机制限制大型数据集的默认请求,避免对服务产生负面影响。 若要返回 50 多行,请使用 Skip
和 Take
方法,如 页面中的“返回数据”中所述。
如何:筛选返回的数据
以下代码演示如何通过在 Where
查询中包含子句来筛选数据。 它返回todoTable
中其Complete
属性等于false
的所有项。
Where 函数将行筛选谓词应用于针对表的查询。
// This query filters out completed TodoItems and items without a timestamp.
List<TodoItem> items = await todoTable
.Where(todoItem => todoItem.Complete == false)
.ToListAsync();
可以使用消息检查软件(例如浏览器开发人员工具或 Fiddler)查看发送到后端的请求的 URI。 如果查看请求 URI,请注意查询字符串已修改:
GET /tables/todoitem?$filter=(complete+eq+false) HTTP/1.1
此 OData 请求由服务器 SDK 转换为 SQL 查询:
SELECT *
FROM TodoItem
WHERE ISNULL(complete, 0) = 0
传递给方法的 Where
函数可以具有任意数量的条件。
// This query filters out completed TodoItems where Text isn't null
List<TodoItem> items = await todoTable
.Where(todoItem => todoItem.Complete == false && todoItem.Text != null)
.ToListAsync();
此示例将由服务器 SDK 转换为 SQL 查询:
SELECT *
FROM TodoItem
WHERE ISNULL(complete, 0) = 0
AND ISNULL(text, 0) = 0
此查询也可以拆分为多个子句:
List<TodoItem> items = await todoTable
.Where(todoItem => todoItem.Complete == false)
.Where(todoItem => todoItem.Text != null)
.ToListAsync();
这两种方法是等效的,可以互换使用。 在一个查询中连接多个谓词的前一个选项更紧凑,建议使用。
Where
子句支持可以转换为 OData 子集的操作。 操作包括:
- 关系运算符 (==, !=, <, <=, >= >),
- 算术运算符 (+, -, /, *, %),
- 数字精度 (Math.Floor, Math.Ceiling),
- 字符串函数(长度, 子字符串, 替换, 索引, 以...开始, 以...结束)
- 日期属性(Year、Month、Day、Hour、Minute、Second)、
- 访问对象的属性,以及
- 合并其中任一操作的表达式。
考虑服务器 SDK 支持的内容时,可以考虑 OData v3 文档。
如何:对返回的数据进行排序
以下代码演示如何通过在查询中包含 OrderBy 或 OrderByDescending 函数对数据进行排序。 它返回从 todoTable
中按 Text
字段升序排序的项。
// Sort items in ascending order by Text field
MobileServiceTableQuery<TodoItem> query = todoTable
.OrderBy(todoItem => todoItem.Text)
List<TodoItem> items = await query.ToListAsync();
// Sort items in descending order by Text field
MobileServiceTableQuery<TodoItem> query = todoTable
.OrderByDescending(todoItem => todoItem.Text)
List<TodoItem> items = await query.ToListAsync();
如何:在页面中返回数据
默认情况下,后端仅返回前 50 行。 可以通过调用 Take 方法增加返回的行数。 与 Take
Skip 方法一起使用,请求查询返回的总数据集的特定“页面”。 执行后,以下查询将返回表中的前三项。
// Define a filtered query that returns the top 3 items.
MobileServiceTableQuery<TodoItem> query = todoTable.Take(3);
List<TodoItem> items = await query.ToListAsync();
以下修订后的查询跳过前三个结果,并返回接下来的三个结果。 此查询生成数据的第二个“页面”,其中页面大小为三个项目。
// Define a filtered query that skips the top 3 items and returns the next 3 items.
MobileServiceTableQuery<TodoItem> query = todoTable.Skip(3).Take(3);
List<TodoItem> items = await query.ToListAsync();
IncludeTotalCount 方法请求返回所有记录的总计数,忽略指定的任何分页或限制子句:
query = query.IncludeTotalCount();
在实际应用中,可以使用类似于前面的示例的查询和寻呼控件或类似的 UI 在页面之间导航。
注释
若要替代移动应用后端中的 50 行限制,还必须将 EnableQueryAttribute 应用于公共 GET 方法并指定分页行为。 应用于该方法时,以下方法将返回的最大行数设置为 1000:
[EnableQuery(MaxTop=1000)]
如何选择特定列
可以通过向查询添加 Select 子句来指定要包含在结果中的属性集。 例如,以下代码演示如何仅选择一个字段,以及如何选择和设置多个字段的格式:
// Select one field -- just the Text
MobileServiceTableQuery<TodoItem> query = todoTable
.Select(todoItem => todoItem.Text);
List<string> items = await query.ToListAsync();
// Select multiple fields -- both Complete and Text info
MobileServiceTableQuery<TodoItem> query = todoTable
.Select(todoItem => string.Format("{0} -- {1}",
todoItem.Text.PadRight(30), todoItem.Complete ?
"Now complete!" : "Incomplete!"));
List<string> items = await query.ToListAsync();
到目前为止介绍的所有函数都是累加性的,因此我们可以继续链接它们。 每个链式调用都会影响查询的更大部分。 还有一个示例:
MobileServiceTableQuery<TodoItem> query = todoTable
.Where(todoItem => todoItem.Complete == false)
.Select(todoItem => todoItem.Text)
.Skip(3).
.Take(3);
List<string> items = await query.ToListAsync();
如何:按 ID 查找数据
LookupAsync 函数可用于查找具有特定 ID 的数据库中的对象。
// This query filters out the item with the ID of 37BBF396-11F0-4B39-85C8-B319C729AF6D
TodoItem item = await todoTable.LookupAsync("37BBF396-11F0-4B39-85C8-B319C729AF6D");
如何:执行非类型化查询
使用非类型化表对象执行查询时,必须通过调用 ReadAsync 显式指定 OData 查询字符串,如以下示例所示:
// Lookup untyped data using OData
JToken untypedItems = await untypedTodoTable.ReadAsync("$filter=complete eq 0&$orderby=text");
可以恢复可以像属性包一样使用的 JSON 值。 有关 JToken 和 Newtonsoft Json.NET 的详细信息,请参阅 Json.NET 网站。
如何:将数据插入移动应用后端
所有客户端类型都必须包含一个名为 Id的成员,该成员默认为字符串。 执行 CRUD 操作和离线同步需要此 ID。以下代码演示了如何使用 InsertAsync 方法将新行插入一个表中。 该参数包含要作为 .NET 对象插入的数据。
await todoTable.InsertAsync(todoItem);
如果插入时未包含todoItem
中的唯一自定义 ID 值,服务器将生成一个 GUID。
可以通过在调用返回后检查对象来检索生成的 ID。
若要插入非类型化数据,可以利用 Json.NET:
JObject jo = new JObject();
jo.Add("Text", "Hello World");
jo.Add("Complete", false);
var inserted = await table.InsertAsync(jo);
下面是使用电子邮件地址作为唯一字符串 ID 的示例:
JObject jo = new JObject();
jo.Add("id", "myemail@emaildomain.com");
jo.Add("Text", "Hello World");
jo.Add("Complete", false);
var inserted = await table.InsertAsync(jo);
使用 ID 值
移动应用支持表 ID 列的唯一自定义字符串值。 字符串值允许应用程序使用自定义值,例如 ID 的电子邮件地址或用户名。 字符串 ID 提供以下优势:
- 生成 ID,而无需往返数据库。
- 更方便地合并不同表或数据库中的记录。
- ID 值可以更好地与应用程序的逻辑集成。
如果未在插入的记录上设置字符串 ID 值,移动应用后端将为 ID 生成唯一值。 可以使用 Guid.NewGuid 方法在客户端或后端生成自己的 ID 值。
JObject jo = new JObject();
jo.Add("id", Guid.NewGuid().ToString("N"));
如何:修改移动应用后端中的数据
以下代码演示如何使用 UpdateAsync 方法,通过相同的 ID 用新信息更新现有记录。 该参数包含要作为 .NET 对象更新的数据。
await todoTable.UpdateAsync(todoItem);
若要更新非类型化数据,可以利用 Json.NET ,如下所示:
JObject jo = new JObject();
jo.Add("id", "37BBF396-11F0-4B39-85C8-B319C729AF6D");
jo.Add("Text", "Hello World");
jo.Add("Complete", false);
var inserted = await table.UpdateAsync(jo);
进行更新时,必须指定id
字段。 后端使用 id
字段标识要更新的行。 可以从InsertAsync
调用结果中获取id
字段。 如果您尝试在未提供id
值的情况下更新项目,则会引发ArgumentException
。
如何:删除移动应用后端中的数据
以下代码演示如何使用 DeleteAsync 方法删除现有实例。 实例由 todoItem
上的 id
字段集标识。
await todoTable.DeleteAsync(todoItem);
若要删除非类型化数据,可以利用 Json.NET,如下所示:
JObject jo = new JObject();
jo.Add("id", "37BBF396-11F0-4B39-85C8-B319C729AF6D");
await table.DeleteAsync(jo);
发出删除请求时,必须指定 ID。 其他属性不会传递给服务,或在服务中会被忽略。 调用的结果 DeleteAsync
通常是 null
。 可以从InsertAsync
调用的结果中获取需要传入的ID。 当尝试删除项目而未指定id
字段时,会引发MobileServiceInvalidOperationException
。
如何使用乐观并发解决冲突
两个或多个客户端可能会同时将更改写入同一项。 如果没有冲突检测,最后一次写入将覆盖任何以前的更新。 乐观并发控制 假定每个事务都可以提交,因此不使用任何资源锁定。 在提交事务之前,乐观并发控制会验证没有其他事务修改数据。 如果数据已修改,则回滚提交事务。
移动应用支持乐观并发控制,方法是使用移动应用后端为每个表定义的 version
系统属性列来跟踪每项的更改。 每次更新记录时,移动应用都会将该记录的 version
属性设置为新值。 在每个更新请求期间,请求中包含的记录 version
属性与服务器上的记录的相同属性进行比较。 如果随请求传递的版本与后端不匹配,则客户端库将 MobileServicePreconditionFailedException<T>
引发异常。 包含异常的类型是记录的服务器版本,该记录来自后端。 然后,应用程序可以使用此信息来决定是否使用后端的正确 version
值再次执行更新请求以提交更改。
在表类中为version
系统属性定义一列,以启用乐观并发。 例如:
public class TodoItem
{
public string Id { get; set; }
[JsonProperty(PropertyName = "text")]
public string Text { get; set; }
[JsonProperty(PropertyName = "complete")]
public bool Complete { get; set; }
// *** Enable Optimistic Concurrency *** //
[JsonProperty(PropertyName = "version")]
public string Version { set; get; }
}
使用非类型化表的应用程序通过在表上SystemProperties
设置Version
标志来启用乐观并发,如下所示。
//Enable optimistic concurrency by retrieving version
todoTable.SystemProperties |= MobileServiceSystemProperties.Version;
除了启用乐观并发外,还必须在调用 UpdateAsync 时捕获MobileServicePreconditionFailedException<T>
代码中的异常。 通过将正确的值 version
应用于更新的记录并使用已解决的记录调用 UpdateAsync 来解决冲突。 以下代码演示如何解决检测到的写入冲突:
private async void UpdateToDoItem(TodoItem item)
{
MobileServicePreconditionFailedException<TodoItem> exception = null;
try
{
//update at the remote table
await todoTable.UpdateAsync(item);
}
catch (MobileServicePreconditionFailedException<TodoItem> writeException)
{
exception = writeException;
}
if (exception != null)
{
// Conflict detected, the item has changed since the last query
// Resolve the conflict between the local and server item
await ResolveConflict(item, exception.Item);
}
}
private async Task ResolveConflict(TodoItem localItem, TodoItem serverItem)
{
//Ask user to choose the resolution between versions
MessageDialog msgDialog = new MessageDialog(
String.Format("Server Text: \"{0}\" \nLocal Text: \"{1}\"\n",
serverItem.Text, localItem.Text),
"CONFLICT DETECTED - Select a resolution:");
UICommand localBtn = new UICommand("Commit Local Text");
UICommand ServerBtn = new UICommand("Leave Server Text");
msgDialog.Commands.Add(localBtn);
msgDialog.Commands.Add(ServerBtn);
localBtn.Invoked = async (IUICommand command) =>
{
// To resolve the conflict, update the version of the item being committed. Otherwise, you will keep
// catching a MobileServicePreConditionFailedException.
localItem.Version = serverItem.Version;
// Updating recursively here just in case another change happened while the user was making a decision
UpdateToDoItem(localItem);
};
ServerBtn.Invoked = async (IUICommand command) =>
{
RefreshTodoItems();
};
await msgDialog.ShowAsync();
}
有关详细信息,请参阅 Azure 移动应用主题中的脱机数据同步 。
如何:将移动应用数据绑定到 Windows 用户界面
本部分介绍如何在 Windows 应用中使用 UI 元素显示返回的数据对象。 以下示例代码将绑定到列表的源,并查询不完整的项。 MobileServiceCollection 创建一个移动应用感知绑定集合。
// This query filters out completed TodoItems.
MobileServiceCollection<TodoItem, TodoItem> items = await todoTable
.Where(todoItem => todoItem.Complete == false)
.ToCollectionAsync();
// itemsControl is an IEnumerable that could be bound to a UI list control
IEnumerable itemsControl = items;
// Bind this to a ListBox
ListBox lb = new ListBox();
lb.ItemsSource = items;
托管运行时中的某些控件支持名为 ISupportIncrementalLoading 的接口。 此接口允许控件在用户滚动时请求额外数据。 通过 MobileServiceIncrementalLoadingCollection 为通用 Windows 应用提供此接口的内置支持,该接口会自动处理来自控件的调用。 在 Windows 应用中使用 MobileServiceIncrementalLoadingCollection
,如下所示:
MobileServiceIncrementalLoadingCollection<TodoItem,TodoItem> items;
items = todoTable.Where(todoItem => todoItem.Complete == false).ToIncrementalLoadingCollection();
ListBox lb = new ListBox();
lb.ItemsSource = items;
若要在 Windows Phone 8 和“Silverlight”应用中使用新集合,请在 IMobileServiceTableQuery<T>
和 IMobileServiceTable<T>
上使用 ToCollection
扩展方法。 若要加载数据,请调用 LoadMoreItemsAsync()
。
MobileServiceCollection<TodoItem, TodoItem> items = todoTable.Where(todoItem => todoItem.Complete==false).ToCollection();
await items.LoadMoreItemsAsync();
当您使用通过调用ToCollectionAsync
或ToCollection
创建的集合时,您将获得一个可以绑定到 UI 控件的集合。 此集合可识别分页。 由于集合正在从网络加载数据,因此加载有时会失败。 为了处理这些故障,请在MobileServiceIncrementalLoadingCollection
中重写OnException
方法,以处理调用LoadMoreItemsAsync
时产生的异常。
假设你的表包含许多字段,但是你仅想在控制中显示其中一些字段。 可以使用上一部分中的“选择特定列”中的指南选择要在 UI 中显示的特定列。
更改页面大小
默认情况下,Azure 移动应用为每个请求返回最多 50 个项目。 可以通过增加客户端和服务器上的最大页面大小来更改分页大小。 若要增加请求的页面大小,请在使用PullAsync()
时指定PullOptions
:
PullOptions pullOptions = new PullOptions
{
MaxPageSize = 100
};
假设您已在服务器中将 PageSize
设置为大于或等于 100,则请求最多返回 100 个条目。
使用脱机表
脱机表使用本地 SQLite 存储来存储数据,以便在脱机时使用。 针对本地 SQLite 存储而不是远程服务器存储执行所有表操作。 若要创建脱机表,请先准备项目:
在 Visual Studio 中,右键单击解决方案 >“管理解决方案的 NuGet 包...”,然后搜索并安装解决方案中所有项目的 Microsoft.Azure.Mobile.Client.SQLiteStore NuGet 包。
(可选)若要支持 Windows 设备,请安装以下 SQLite 运行时包之一:
- Windows 8.1 运行时: 安装 适用于 Windows 8.1 的 SQLite。
- Windows Phone 8.1: 安装 适用于 Windows Phone 8.1 的 SQLite。
- 通用 Windows 平台安装适用于通用 Windows 的 SQLite。
(可选)。 对于 Windows 设备,单击“ 引用>添加引用...”,展开 Windows 文件夹 >扩展,然后为 Windows SDK 启用相应的 SQLite 以及Visual C++ 2013 Runtime for Windows SDK。 SQLite SDK 名称因每个 Windows 平台而略有不同。
在创建表引用之前,必须准备好本地存储:
var store = new MobileServiceSQLiteStore(Constants.OfflineDbPath);
store.DefineTable<TodoItem>();
//Initializes the SyncContext using the default IMobileServiceSyncHandler.
await this.client.SyncContext.InitializeAsync(store);
创建客户端后,通常立即完成存储初始化。 OfflineDbPath 应该是适合在支持的所有平台上使用的文件名。 如果路径是完全限定的路径(即它以斜杠开头),则使用该路径。 如果路径未完全限定,该文件将放置在特定于平台的位置。
- 对于 iOS 和 Android 设备,默认路径为“个人文件”文件夹。
- 对于 Windows 设备,默认路径是特定于应用程序的“AppData”文件夹。
可以使用 GetSyncTable<>
方法获取表引用:
var table = client.GetSyncTable<TodoItem>();
无需进行身份验证才能使用脱机表。 只需在与后端服务通信时进行身份验证。
同步离线表
默认情况下,脱机表不会与后端同步。 同步分为两个部分。 可以单独推送更改,而无需下载新项目。 下面是典型的同步方法:
public async Task SyncAsync()
{
ReadOnlyCollection<MobileServiceTableOperationError> syncErrors = null;
try
{
await this.client.SyncContext.PushAsync();
await this.todoTable.PullAsync(
//The first parameter is a query name that is used internally by the client SDK to implement incremental sync.
//Use a different query name for each unique query in your program
"allTodoItems",
this.todoTable.CreateQuery());
}
catch (MobileServicePushFailedException exc)
{
if (exc.PushResult != null)
{
syncErrors = exc.PushResult.Errors;
}
}
// Simple error/conflict handling. A real application would handle the various errors like network conditions,
// server conflicts and others via the IMobileServiceSyncHandler.
if (syncErrors != null)
{
foreach (var error in syncErrors)
{
if (error.OperationKind == MobileServiceTableOperationKind.Update && error.Result != null)
{
//Update failed, reverting to server's copy.
await error.CancelAndUpdateItemAsync(error.Result);
}
else
{
// Discard local change.
await error.CancelAndDiscardItemAsync();
}
Debug.WriteLine(@"Error executing sync operation. Item: {0} ({1}). Operation discarded.", error.TableName, error.Item["id"]);
}
}
}
如果第 PullAsync
一个参数为 null,则不会使用增量同步。 每次同步操作将检索所有记录。
SDK 在拉取记录之前执行隐式 PushAsync()
。
冲突处理发生在 PullAsync()
方法上。 可以像处理在线表格一样解决冲突。 调用 PullAsync()
时生成冲突,而不是在插入、更新或删除操作期间。 如果发生多个冲突,它们将捆绑到单个 MobileServicePushFailedException 中。 分别处理每个失败。
使用自定义 API
自定义 API 可让你定义自定义终结点,这些终结点会公开不映射到插入、更新、删除或读取操作的服务器功能。 使用自定义 API 能够以更大的力度控制消息传送,包括读取和设置 HTTP 消息标头,以及定义除 JSON 以外的消息正文格式。
通过在客户端上调用一个 InvokeApiAsync 方法来调用自定义 API。 例如,以下代码行将 POST 请求发送到后端上的 completeAll API:
var result = await client.InvokeApiAsync<MarkAllResult>("completeAll", System.Net.Http.HttpMethod.Post, null);
此方法调用为类型化形式,并且需要定义 MarkAllResult 的返回类型。 支持类型化和非类型化方法。
除非 API 以“/”开头,否则 InvokeApiAsync() 方法会在要调用的 API 前加上“/api/”。 例如:
-
InvokeApiAsync("completeAll",...)
在后端调用 /api/completeAll -
InvokeApiAsync("/.auth/me",...)
在后端调用 /.auth/me
可以使用 InvokeApiAsync 调用任何 WebAPI,包括未使用 Azure 移动应用定义的 WebAPI。 使用 InvokeApiAsync()时,会随请求一起发送相应的标头,包括身份验证标头。
对用户进行身份验证
移动应用支持使用各种外部标识提供者对应用用户进行身份验证和授权:Facebook、Google、Microsoft 帐户、Twitter 和 Azure Active Directory。 可以在表中设置权限,以便将特定操作的访问权限限制给已经过身份验证的用户。 还可以使用经过身份验证的用户的标识在服务器脚本中实现授权规则。 有关详细信息,请参阅本教程 向应用添加身份验证。
支持两个身份验证流: 客户端托管 流和 服务器托管 流。 服务器管理的流提供最简单的身份验证体验,因为它依赖于提供程序的 Web 身份验证接口。 客户端托管流程允许更深入地集成设备特定的功能,因为它依赖于提供程序专用的设备 SDK。
注释
建议在生产应用中使用客户端管理的流。
若要设置身份验证,必须将应用注册到一个或多个标识提供者。 标识提供者为应用生成客户端 ID 和客户端密码。 然后,在后端设置这些值以启用 Azure 应用服务身份验证/授权。 有关详细信息,请按照教程中的详细说明 向应用添加身份验证。
本节介绍了以下主题:
客户端管理的身份验证
应用可以独立联系标识提供者,然后在使用后端登录期间提供返回的令牌。 通过此客户端流,可以为用户提供单一登录体验,或从标识提供者检索其他用户数据。 相比于使用服务器流,客户端流身份验证更为优选,因为身份提供者 SDK 可以提供更符合本地用户体验的感受,并允许更多的自定义。
为以下客户端流身份验证模式提供了示例:
使用 Active Directory 身份验证库对用户进行身份验证
可以使用 Active Directory 身份验证库(ADAL)通过 Azure Active Directory 身份验证从客户端启动用户身份验证。
按照 如何为 Active Directory 登录配置应用服务 教程,为 AAD 登录配置移动应用后端。 请务必完成注册本机客户端应用程序的可选步骤。
在 Visual Studio 或 Xamarin Studio 中,打开项目并添加对 NuGet 包的
Microsoft.IdentityModel.Clients.ActiveDirectory
引用。 搜索时,包括预发行版本。根据所使用的平台,将以下代码添加到应用程序。 在每一个中,进行以下替换:
将 INSERT-AUTHORITY-HERE 替换为在其中预配应用程序的租户名称。 格式应为 https://login.microsoftonline.com/contoso.onmicrosoft.com。 此值可以从 Azure 门户 中的 Azure Active Directory 的“域”选项卡中复制。
将 INSERT-RESOURCE-ID-HERE 替换为移动应用后端的客户端 ID。 可以在门户中“Azure Active Directory 设置”下面的“高级”选项卡获取此客户端 ID。
将 INSERT-CLIENT-ID-HERE 替换为从本机客户端应用程序复制的客户端 ID。
使用 HTTPS 方案将 INSERT-REDIRECT-URI-HERE 替换为站点的 /.auth/login/done 终结点。 此值应类似于 https://contoso.azurewebsites.net/.auth/login/done。
每个平台所需的代码如下:
Windows:
private MobileServiceUser user; private async Task AuthenticateAsync() { string authority = "INSERT-AUTHORITY-HERE"; string resourceId = "INSERT-RESOURCE-ID-HERE"; string clientId = "INSERT-CLIENT-ID-HERE"; string redirectUri = "INSERT-REDIRECT-URI-HERE"; while (user == null) { string message; try { AuthenticationContext ac = new AuthenticationContext(authority); AuthenticationResult ar = await ac.AcquireTokenAsync(resourceId, clientId, new Uri(redirectUri), new PlatformParameters(PromptBehavior.Auto, false) ); JObject payload = new JObject(); payload["access_token"] = ar.AccessToken; user = await App.MobileService.LoginAsync( MobileServiceAuthenticationProvider.WindowsAzureActiveDirectory, payload); message = string.Format("You are now logged in - {0}", user.UserId); } catch (InvalidOperationException) { message = "You must log in. Login Required"; } var dialog = new MessageDialog(message); dialog.Commands.Add(new UICommand("OK")); await dialog.ShowAsync(); } }
Xamarin.iOS
private MobileServiceUser user; private async Task AuthenticateAsync(UIViewController view) { string authority = "INSERT-AUTHORITY-HERE"; string resourceId = "INSERT-RESOURCE-ID-HERE"; string clientId = "INSERT-CLIENT-ID-HERE"; string redirectUri = "INSERT-REDIRECT-URI-HERE"; try { AuthenticationContext ac = new AuthenticationContext(authority); AuthenticationResult ar = await ac.AcquireTokenAsync(resourceId, clientId, new Uri(redirectUri), new PlatformParameters(view)); JObject payload = new JObject(); payload["access_token"] = ar.AccessToken; user = await client.LoginAsync( MobileServiceAuthenticationProvider.WindowsAzureActiveDirectory, payload); } catch (Exception ex) { Console.Error.WriteLine(@"ERROR - AUTHENTICATION FAILED {0}", ex.Message); } }
Xamarin.Android
private MobileServiceUser user; private async Task AuthenticateAsync() { string authority = "INSERT-AUTHORITY-HERE"; string resourceId = "INSERT-RESOURCE-ID-HERE"; string clientId = "INSERT-CLIENT-ID-HERE"; string redirectUri = "INSERT-REDIRECT-URI-HERE"; try { AuthenticationContext ac = new AuthenticationContext(authority); AuthenticationResult ar = await ac.AcquireTokenAsync(resourceId, clientId, new Uri(redirectUri), new PlatformParameters(this)); JObject payload = new JObject(); payload["access_token"] = ar.AccessToken; user = await client.LoginAsync( MobileServiceAuthenticationProvider.WindowsAzureActiveDirectory, payload); } catch (Exception ex) { AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.SetMessage(ex.Message); builder.SetTitle("You must log in. Login Required"); builder.Create().Show(); } } protected override void OnActivityResult(int requestCode, Result resultCode, Intent data) { base.OnActivityResult(requestCode, resultCode, data); AuthenticationAgentContinuationHelper.SetAuthenticationAgentContinuationEventArgs(requestCode, resultCode, data); }
使用 Facebook 或 Google 令牌的单一 Sign-On
可以使用适用于 Facebook 或 Google 的此代码片段所示的客户端流。
var token = new JObject();
// Replace access_token_value with actual value of your access token obtained
// using the Facebook or Google SDK.
token.Add("access_token", "access_token_value");
private MobileServiceUser user;
private async Task AuthenticateAsync()
{
while (user == null)
{
string message;
try
{
// Change MobileServiceAuthenticationProvider.Facebook
// to MobileServiceAuthenticationProvider.Google if using Google auth.
user = await client.LoginAsync(MobileServiceAuthenticationProvider.Facebook, token);
message = string.Format("You are now logged in - {0}", user.UserId);
}
catch (InvalidOperationException)
{
message = "You must log in. Login Required";
}
var dialog = new MessageDialog(message);
dialog.Commands.Add(new UICommand("OK"));
await dialog.ShowAsync();
}
}
服务器管理的身份验证
注册身份提供者后,在 [MobileServiceClient] 上使用你的提供程序的 MobileServiceAuthenticationProvider 值调用 LoginAsync 方法。 例如,以下代码使用 Facebook 启动服务器流登录。
private MobileServiceUser user;
private async System.Threading.Tasks.Task Authenticate()
{
while (user == null)
{
string message;
try
{
user = await client
.LoginAsync(MobileServiceAuthenticationProvider.Facebook);
message =
string.Format("You are now logged in - {0}", user.UserId);
}
catch (InvalidOperationException)
{
message = "You must log in. Login Required";
}
var dialog = new MessageDialog(message);
dialog.Commands.Add(new UICommand("OK"));
await dialog.ShowAsync();
}
}
如果使用 Facebook 以外的标识提供者,请将 MobileServiceAuthenticationProvider 的值更改为提供程序的值。
在服务器流中,Azure 应用服务通过显示所选提供程序的登录页来管理 OAuth 身份验证流。 标识提供者返回后,Azure 应用服务将生成应用服务身份验证令牌。 LoginAsync 方法返回一个 MobileServiceUser,它同时提供经过身份验证的用户的 UserId 和 MobileServiceAuthenticationToken 作为 JSON Web 令牌(JWT)。 此令牌可以缓存并重复使用,直到它过期。 有关详细信息,请参阅 缓存身份验证令牌。
在使用 Xamarin(无论是 Android 还是 iOS)时,会使用 Xamarin.Essentials WebAuthenticator。 必须将默认上下文(Android)或 UIViewController(iOS)传递给 LoginAsync
方法。 此外,必须处理来自 Web 验证器的返回。 在 Android 中,此作在以下范围内 MainActivity.cs
进行处理:
public override void OnResume()
{
base.OnResume();
Xamarin.Essentials.Platform.OnResume();
}
在 iOS 上,在 AppDelegate.cs'中处理此问题:
public override bool OpenUrl(UIApplication app, NSUrl url, NSDictionary options)
{
if (client.ResumeWithURL(app, url, options))
return true;
return base.OpenUrl(app, url, options);
}
缓存身份验证令牌
在某些情况下,可以通过存储来自提供程序的身份验证令牌,在首次成功身份验证后避免对登录方法的调用。 Microsoft Store 和 UWP 应用可以使用 PasswordVault 在成功登录后以缓存当前身份验证令牌,如下所示:
await client.LoginAsync(MobileServiceAuthenticationProvider.Facebook);
PasswordVault vault = new PasswordVault();
vault.Add(new PasswordCredential("Facebook", client.currentUser.UserId,
client.currentUser.MobileServiceAuthenticationToken));
将 UserId 值存储为凭据的 UserName,将令牌存储为密码。 在后续启动中,可以检查 PasswordVault 的缓存凭据。 以下示例在找到缓存凭据时使用缓存凭据,否则会尝试使用后端再次进行身份验证:
// Try to retrieve stored credentials.
var creds = vault.FindAllByResource("Facebook").FirstOrDefault();
if (creds != null)
{
// Create the current user from the stored credentials.
client.currentUser = new MobileServiceUser(creds.UserName);
client.currentUser.MobileServiceAuthenticationToken =
vault.Retrieve("Facebook", creds.UserName).Password;
}
else
{
// Regular login flow and cache the token as shown above.
}
注销用户时,还必须删除存储的凭据,如下所示:
client.Logout();
vault.Remove(vault.Retrieve("Facebook", client.currentUser.UserId));
Xamarin 应用使用 Xamarin.Auth API 安全地将凭据存储在 Account 对象中。 有关使用这些 API 的示例,请参阅 ContosoMoments 照片共享示例中AuthStore.cs代码文件。
使用客户端管理的身份验证时,还可以缓存从提供商(如 Facebook 或 Twitter)获取的访问令牌。 可以提供此令牌以从后端请求新的身份验证令牌,如下所示:
var token = new JObject();
// Replace <your_access_token_value> with actual value of your access token
token.Add("access_token", "<your_access_token_value>");
// Authenticate using the access token.
await client.LoginAsync(MobileServiceAuthenticationProvider.Facebook, token);
推送通知
以下主题介绍推送通知:
如何:注册推送通知
移动应用客户端允许向 Azure 通知中心注册推送通知。 注册时,您将从特定于平台的推送通知服务(PNS)获取一个句柄。 然后,在创建注册时提供此值以及任何标记。 以下代码向 Windows 通知服务(WNS)注册用于推送通知的 Windows 应用:
private async void InitNotificationsAsync()
{
// Request a push notification channel.
var channel = await PushNotificationChannelManager.CreatePushNotificationChannelForApplicationAsync();
// Register for notifications using the new channel.
await MobileService.GetPush().RegisterNativeAsync(channel.Uri, null);
}
如果要推送到 WNS,则必须 获取 Microsoft Store 包 SID。 有关 Windows 应用的详细信息,包括如何注册模板注册,请参阅 向应用添加推送通知。
不支持从客户端请求标记。 标记请求将在注册过程中被悄无声息地删除。
如果希望将设备与标签一起注册,请创建一个自定义 API,该 API 使用通知中心的多种 API 来代您完成注册。 调用自定义 API 而不是 RegisterNativeAsync()
方法。
如何:获取Microsoft应用商店包 SID
在 Microsoft 应用商店应用中启用推送通知需要包 SID。 若要接收包 SID,请将应用程序注册到 Microsoft 应用商店。
若要获取此值,请:
- 在 Visual Studio 解决方案资源管理器中,右键单击Microsoft应用商店应用项目,单击 “应用商店>关联应用”...。
- 在向导中,单击“ 下一步”,使用Microsoft帐户登录,在 “保留新应用名称”中键入应用的名称,然后单击“ 保留”。
- 成功创建应用注册后,选择应用名称,单击“ 下一步”,然后单击“ 关联”。
- 使用Microsoft帐户登录到 Windows 开发人员中心 。 在 “我的应用”下,单击你创建的应用注册。
- 单击 “应用管理>应用标识”,然后向下滚动以查找 程序包 SID。
包 SID 的许多用法都将其视为 URI,在这种情况下,需要使用 ms-app:// 作为方案。 记下将此值串联为前缀而形成的包 SID 版本。
Xamarin 应用需要一些额外的代码才能注册在 iOS 或 Android 平台上运行的应用。 有关详细信息,请参阅平台的主题:
如何:注册推送模板以发送跨平台通知
若要注册模板,请使用 RegisterAsync()
方法与模板,如下所示:
JObject templates = myTemplates();
MobileService.GetPush().RegisterAsync(channel.Uri, templates);
模板应为 JObject
类型,可以包含以下 JSON 格式的多个模板:
public JObject myTemplates()
{
// single template for Windows Notification Service toast
var template = "<toast><visual><binding template=\"ToastText01\"><text id=\"1\">$(message)</text></binding></visual></toast>";
var templates = new JObject
{
["generic-message"] = new JObject
{
["body"] = template,
["headers"] = new JObject
{
["X-WNS-Type"] = "wns/toast"
},
["tags"] = new JArray()
},
["more-templates"] = new JObject {...}
};
return templates;
}
RegisterAsync() 方法还接受辅助磁贴:
MobileService.GetPush().RegisterAsync(string channelUri, JObject templates, JObject secondaryTiles);
在注册过程中,所有标记都会被剥离,以便安全。 若要将标记添加到安装中的安装或模板,请参阅[使用适用于 Azure 移动应用的 .NET 后端服务器 SDK]。
若要使用这些已注册的模板发送通知,请参阅 通知中心 API。
其他主题
如何:处理错误
当后端发生错误时,客户端 SDK 将引发一个 MobileServiceInvalidOperationException
。 以下示例演示如何处理后端返回的异常:
private async void InsertTodoItem(TodoItem todoItem)
{
// This code inserts a new TodoItem into the database. When the operation completes
// and App Service has assigned an Id, the item is added to the CollectionView
try
{
await todoTable.InsertAsync(todoItem);
items.Add(todoItem);
}
catch (MobileServiceInvalidOperationException e)
{
// Handle error
}
}
在 移动应用文件示例中可以找到处理错误条件的另一个示例。 LoggingHandler 示例提供了一个日志记录委托处理程序,用于记录正在向后端发出的请求。
如何:自定义请求标头
若要支持特定应用方案,可能需要自定义与移动应用后端的通信。 例如,你可能希望将自定义标头添加到每个传出请求,甚至更改响应状态代码。 可以使用自定义 DelegatingHandler,如以下示例所示:
public async Task CallClientWithHandler()
{
MobileServiceClient client = new MobileServiceClient("AppUrl", new MyHandler());
IMobileServiceTable<TodoItem> todoTable = client.GetTable<TodoItem>();
var newItem = new TodoItem { Text = "Hello world", Complete = false };
await todoTable.InsertAsync(newItem);
}
public class MyHandler : DelegatingHandler
{
protected override async Task<HttpResponseMessage>
SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
// Change the request-side here based on the HttpRequestMessage
request.Headers.Add("x-my-header", "my value");
// Do the request
var response = await base.SendAsync(request, cancellationToken);
// Change the response-side here based on the HttpResponseMessage
// Return the modified response
return response;
}
}