使用 Windows Communication Foundation (WCF) Web 服务

WCF 是 Microsoft 为生成面向服务的应用程序而提供的统一框架。 开发人员可通过此框架构建安全、可靠、可互操作的分布式应用程序。 本文演示如何在 Xamarin.Forms 应用程序中使用 WCF 简单对象访问协议 (SOAP) 服务。

WCF 描述了具有各种不同协定的服务,包括:

  • 数据协定 – 定义构成消息内容基础的数据结构。
  • 消息协定 - 撰写来自现有数据协定的消息。
  • 错误协定 – 允许指定自定义 SOAP 错误。
  • 服务协定 – 指定服务支持的操作以及与每个操作进行交互所需的消息。 它们还指定可以与每个服务操作关联的任何自定义错误行为。

ASP.NET Web 服务 (ASMX) 与 WCF 之间存在差异,但 WCF 支持 ASMX 提供的相同功能 – 通过 HTTP 发送的 SOAP 消息。 有关使用 ASMX 服务的详细信息,请参阅使用 ASP.NET Web 服务 (ASMX)

重要

Xamarin 平台对 WCF 的支持仅限于使用 BasicHttpBinding 类通过 HTTP/HTTPS 发送的文本编码的 SOAP 消息。

WCF 支持需要使用仅在 Windows 环境中可用的工具来生成代理并托管 TodoWCFService。 构建和测试 iOS 应用程序需要在 Windows 计算机上部署 TodoWCFService 或作为 Azure Web 服务。

Xamarin Forms 本机应用程序通常与 .NET Standard 类库共享代码。 但是,.NET Core 目前不支持 WCF,因此共享项目必须是旧版可移植类库。 有关 .NET Core 中的 WCF 支持的信息,请参阅在 .NET Core 和 .NET Framework 之间进行选择以用于服务器应用

示例应用程序解决方案包括一个可以在本地运行的 WCF 服务,如以下屏幕截图所示:

示例应用程序

注意

在 iOS 9 及更高版本中,应用传输安全性 (ATS) 会在 Internet 资源(如应用的后端服务器)与应用之间的强制实施安全连接,从而防止意外泄露敏感信息。 由于为 iOS 9 生成的应用中默认启用了 ATS,因此所有连接都受 ATS 安全要求的约束。 如果连接不符合这些要求,它们将失败并出现异常。

如果无法对 Internet 资源使用 HTTPS 协议和安全通信,则可以选择退出 ATS。 这可以通过更新应用的 Info.plist 文件来实现。 有关详细信息,请参阅应用传输安全性

使用 Web 服务

WCF 服务提供以下操作:

操作 说明 参数
GetTodoItems 获取待办事项的列表
CreateTodoItem 新建待办事项 XML 序列化的 TodoItem
EditTodoItem 更新待办事项 XML 序列化的 TodoItem
DeleteTodoItem 删除待办事项 XML 序列化的 TodoItem

有关应用程序中使用的数据模型的更多信息,请参阅数据建模

必须生成代理才能使用 WCF 服务,代理允许应用程序连接到该服务。 代理是通过使用定义方法和关联服务配置的服务元数据构造而成。 系统以 Web 服务生成的 Web Services 描述语言 (WSDL) 文档的形式公开此元数据。 可以使用 Visual Studio 2017 中的 Microsoft WCF Web Service Reference 提供程序来生成代理,以将 Web 服务的服务引用添加到 .NET Standard 库。 在 Visual Studio 2017 中使用 Microsoft WCF Web Service Reference 提供程序创建代理的替代方法是使用 ServiceModel 元数据实用工具 (svcutil.exe)。 有关详细信息,请参阅 ServiceModel 元数据实用工具 (Svcutil.exe)

生成的代理类提供了使用 Web 服务的方法,这些服务使用异步编程模型 (APM) 设计模式。 在此模式中,系统通过名为 BeginOperationNameEndOperationName 的两个方法实现异步操作,这两个方法分别用于开始和结束异步操作。

BeginOperationName 方法用于开始异步操作,并返回实现 IAsyncResult 接口的对象。 调用 BeginOperationName 后,应用可以继续对调用线程执行指令,同时异步操作在线程池线程上运行。

每次调用 BeginOperationName 时,应用还应调用 EndOperationName,以获取操作结果。 EndOperationName 的返回值与同步 Web 服务方法返回的类型相同。 例如,EndGetTodoItems 方法返回 TodoItem 实例的集合。 EndOperationName 方法还包括一个 IAsyncResult 参数,该参数应设置为对 BeginOperationName 方法的相应调用返回的实例。

任务并行库 (TPL) 可以通过在同一 Task 对象中封装异步操作来简化使用 APM 开始/结束方法对的过程。 此封装由 TaskFactory.FromAsync 方法的多个重载提供。

有关 APM 的详细信息,请参阅 MSDN 上的异步编程模型TPL 和传统 .NET Framework 异步编程

创建 TodoServiceClient 对象

生成的代理类提供 TodoServiceClient 类,该类用于通过 HTTP 与 WCF 服务进行通信。 该类提供从 URI 标识的服务实例调用 Web 服务方法作为异步操作的功能。 有关异步操作的详细信息,请参阅异步支持概述

系统会在类级别声明 TodoServiceClient 实例,以便只要应用程序需要使用 WCF 服务,该对象就一直存在,如以下代码示例所示:

public class SoapService : ISoapService
{
  ITodoService todoService;
  ...

  public SoapService ()
  {
    todoService = new TodoServiceClient (
      new BasicHttpBinding (),
      new EndpointAddress (Constants.SoapUrl));
  }
  ...
}

TodoServiceClient 实例配置有绑定信息和终结点地址。 使用绑定指定应用程序与服务彼此通信所需的传输、编码和协议详细信息。 BasicHttpBinding 指定将通过 HTTP 传输协议发送文本编码的 SOAP 消息。 指定终结点地址使应用程序能够连接到 WCF 服务的不同实例(如果存在多个已发布的实例)。

有关配置服务引用的详细信息,请参阅配置服务参考

创建数据传输对象

示例应用程序使用 TodoItem 类对数据进行建模。 要将 TodoItem 项存储在 Web 服务中,必须首先将其转换为代理生成的 TodoItem 类型。 可通过 ToWCFServiceTodoItem 方法完成此操作,如以下代码示例所示:

TodoWCFService.TodoItem ToWCFServiceTodoItem (TodoItem item)
{
  return new TodoWCFService.TodoItem
  {
    ID = item.ID,
    Name = item.Name,
    Notes = item.Notes,
    Done = item.Done
  };
}

此方法仅会新建一个 TodoWCFService.TodoItem 实例,并将每个属性设置为 TodoItem 实例中的相同属性。

类似地,当从 Web 服务检索数据时,必须将其从代理生成的 TodoItem 类型转换为 TodoItem 实例。 这是通过 FromWCFServiceTodoItem 方法完成的,如以下代码示例所示:

static TodoItem FromWCFServiceTodoItem (TodoWCFService.TodoItem item)
{
  return new TodoItem
  {
    ID = item.ID,
    Name = item.Name,
    Notes = item.Notes,
    Done = item.Done
  };
}

此方法只是从代理生成的 TodoItem 类型中检索数据并在新创建的 TodoItem 实例中完成设置。

检索数据

TodoServiceClient.BeginGetTodoItemsTodoServiceClient.EndGetTodoItems 方法用于调用 Web 服务提供的 GetTodoItems 操作。 系统会将这些异步方法封装在一个 Task 对象中,如下面的代码示例所示:

public async Task<List<TodoItem>> RefreshDataAsync ()
{
  ...
  var todoItems = await Task.Factory.FromAsync <ObservableCollection<TodoWCFService.TodoItem>> (
    todoService.BeginGetTodoItems,
    todoService.EndGetTodoItems,
    null,
    TaskCreationOptions.None);

  foreach (var item in todoItems)
  {
    Items.Add (FromWCFServiceTodoItem (item));
  }
  ...
}

Task.Factory.FromAsync 方法会创建一个 Task,该任务在 TodoServiceClient.BeginGetTodoItems 方法完成后执行 TodoServiceClient.EndGetTodoItems 方法,其中 null 参数表示未向 BeginGetTodoItems 委托中传递任何数据。 最后,TaskCreationOptions 枚举的值指定了为任务创建和执行使用的默认行为。

TodoServiceClient.EndGetTodoItems 方法会返回 TodoWCFService.TodoItem 实例的 ObservableCollection,然后系统会将其转换为 TodoItem 实例的 List 以进行显示。

创建数据

TodoServiceClient.BeginCreateTodoItemTodoServiceClient.EndCreateTodoItem 方法用于调用 Web 服务提供的 CreateTodoItem 操作。 系统会将这些异步方法封装在一个 Task 对象中,如下面的代码示例所示:

public async Task SaveTodoItemAsync (TodoItem item, bool isNewItem = false)
{
  ...
  var todoItem = ToWCFServiceTodoItem (item);
  ...
  await Task.Factory.FromAsync (
    todoService.BeginCreateTodoItem,
    todoService.EndCreateTodoItem,
    todoItem,
    TaskCreationOptions.None);
  ...
}

Task.Factory.FromAsync 方法会创建一个 Task,该任务会在 TodoServiceClient.BeginCreateTodoItem 方法完成后执行 TodoServiceClient.EndCreateTodoItem 方法,todoItem 参数是传递到 BeginCreateTodoItem 委托中的数据,用于指定要由 Web 服务创建的 TodoItem。 最后,TaskCreationOptions 枚举的值指定了为任务创建和执行使用的默认行为。

如果 Web 服务无法创建由应用程序处理的 TodoItem,则会抛出 FaultException

更新数据

TodoServiceClient.BeginEditTodoItemTodoServiceClient.EndEditTodoItem 方法用于调用 Web 服务提供的 EditTodoItem 操作。 系统会将这些异步方法封装在一个 Task 对象中,如下面的代码示例所示:

public async Task SaveTodoItemAsync (TodoItem item, bool isNewItem = false)
{
  ...
  var todoItem = ToWCFServiceTodoItem (item);
  ...
  await Task.Factory.FromAsync (
    todoService.BeginEditTodoItem,
    todoService.EndEditTodoItem,
    todoItem,
    TaskCreationOptions.None);
  ...
}

Task.Factory.FromAsync 方法会创建一个 Task,该任务会在 TodoServiceClient.BeginCreateTodoItem 方法完成后执行 TodoServiceClient.EndEditTodoItem 方法,todoItem 参数是传递到 BeginEditTodoItem 委托中的数据,用于指定要由 Web 服务更新的 TodoItem。 最后,TaskCreationOptions 枚举的值指定了为任务创建和执行使用的默认行为。

如果 Web 服务无法找到或更新由应用程序处理的 TodoItem,则会抛出 FaultException

删除数据

TodoServiceClient.BeginDeleteTodoItemTodoServiceClient.EndDeleteTodoItem 方法用于调用 Web 服务提供的 DeleteTodoItem 操作。 系统会将这些异步方法封装在一个 Task 对象中,如下面的代码示例所示:

public async Task DeleteTodoItemAsync (string id)
{
  ...
  await Task.Factory.FromAsync (
    todoService.BeginDeleteTodoItem,
    todoService.EndDeleteTodoItem,
    id,
    TaskCreationOptions.None);
  ...
}

Task.Factory.FromAsync 方法会创建一个 Task,该任务会在 TodoServiceClient.BeginDeleteTodoItem 方法完成后执行 TodoServiceClient.EndDeleteTodoItem 方法,id 参数是传递到 BeginDeleteTodoItem 委托中的数据,用于指定要由 Web 服务删除的 TodoItem。 最后,TaskCreationOptions 枚举的值指定了为任务创建和执行使用的默认行为。

如果 Web 服务无法找到或删除由应用程序处理的 TodoItem,则会抛出 FaultException

配置对 IIS Express 的远程访问

在 Visual Studio 2017 或 Visual Studio 2019 中,你应该能够在无需任何其他配置的情况下,使用 PC 测试 UWP 应用程序。 测试 Android 和 iOS 客户端可能需要执行本节中的其他步骤。 有关详细信息,请参阅从 iOS 模拟器和 Android 模拟器连接到本地 Web 服务

默认情况下,IIS Express 将仅响应对 localhost 的请求。 远程设备(例如 Android 设备、iPhone 以及模拟器)将无法访问本地 WCF 服务。 你需要知道本地网络上的 Windows 10 工作站 IP 地址。 鉴于本示例的目的,我们假设你工作站的 IP 地址为 192.168.1.143。 以下步骤说明如何配置 Windows 10 和 IIS Express 以接受远程连接并从物理或虚拟设备连接到服务:

  1. 向 Windows 防火墙添加异常。 必须通过 Windows 防火墙打开一个端口,以便子网上的应用程序通过该端口与 WCF 服务进行通信。 在防火墙中创建开放端口 49393 的入站规则。 在管理命令提示符下运行以下命令:

    netsh advfirewall firewall add rule name="TodoWCFService" dir=in protocol=tcp localport=49393 profile=private remoteip=localsubnet action=allow
    
  2. 配置 IIS Express 以接受远程连接。 可以通过编辑 [solution directory].vs\config\applicationhost.config 中的 IIS Express 配置文件来配置 IIS Express。查找名为 TodoWCFServicesite 元素。 该元素应与以下 XML 类似:

    <site name="TodoWCFService" id="2">
        <application path="/" applicationPool="Clr4IntegratedAppPool">
            <virtualDirectory path="/" physicalPath="C:\Users\tom\TodoWCF\TodoWCFService\TodoWCFService" />
        </application>
        <bindings>
            <binding protocol="http" bindingInformation="*:49393:localhost" />
        </bindings>
    </site>
    

    需要添加两个 binding 元素以向外部流量和 Android 模拟器开放端口 49393。 绑定使用 [IP address]:[port]:[hostname] 格式来指定 IIS Express 如何响应请求。 外部请求将具有必须指定为 binding 的主机名。 将以下 XML 添加到 bindings 元素,并将 IP 地址替换为你自己的 IP 地址:

    <binding protocol="http" bindingInformation="*:49393:192.168.1.143" />
    <binding protocol="http" bindingInformation="*:49393:127.0.0.1" />
    

    更改后,bindings 元素应如下所示:

    <site name="TodoWCFService" id="2">
        <application path="/" applicationPool="Clr4IntegratedAppPool">
            <virtualDirectory path="/" physicalPath="C:\Users\tom\TodoWCF\TodoWCFService\TodoWCFService" />
        </application>
        <bindings>
            <binding protocol="http" bindingInformation="*:49393:localhost" />
            <binding protocol="http" bindingInformation="*:49393:192.168.1.143" />
            <binding protocol="http" bindingInformation="*:49393:127.0.0.1" />
        </bindings>
    </site>
    

    重要

    默认情况下,出于安全原因,IIS Express 将不接受来自外部源的连接。 要启用来自远程设备的连接,必须以管理权限运行 IIS Express。 最简单的方法是使用管理权限运行 Visual Studio 2017。 这将在运行 TodoWCFService 时启动具有管理权限的 IIS Express。

    完成这些步骤后,应该能够运行 TodoWCFService 并从子网上的其他设备进行连接。 可以通过运行应用程序并访问 http://localhost:49393/TodoService.svc 来测试效果。 如果在访问该 URL 时收到“无效请求”错误,则 IIS Express 配置中的 bindings 可能不正确(请求已到达 IIS Express 但被拒绝)。 如果收到其他错误,则可能是应用程序未运行或防火墙配置不正确。

    要允许 IIS Express 继续运行并提供服务,请关闭“项目属性”>“Web”>“调试器”中的“编辑并继续”选项。

  3. 自定义用于访问服务的终结点设备。 在此步骤中,需配置在物理或模拟设备上运行的客户端应用程序以访问 WCF 服务。

    Android 模拟器使用内部代理来防止模拟器直接访问主机的 localhost 地址。 相反,模拟器上的地址 10.0.2.2 通过内部代理路由到主机上的 localhost。 这些代理请求将在请求标头中使用 127.0.0.1 作为主机名,这就是你在上述步骤中为此主机名创建 IIS Express 绑定的原因。

    即使使用的是 Windows 远程 iOS 模拟器,iOS 模拟器也可以在 Mac 生成主机上运行。 来自模拟器的网络请求将以本地网络上的工作站 IP 作为主机名(在本例中为 192.168.1.143,但你的实际 IP 地址可能会有所不同)。 这就是在上述步骤中为此主机名创建 IIS Express 绑定的原因。

    确保 TodoWCF(可移植)项目中 Constants.cs 文件中的 SoapUrl 属性具有适合你网络的值:

    public static string SoapUrl
    {
        get
        {
            var defaultUrl = "http://localhost:49393/TodoService.svc";
    
            if (Device.RuntimePlatform == Device.Android)
            {
                defaultUrl = "http://10.0.2.2:49393/TodoService.svc";
            }
            else if (Device.RuntimePlatform == Device.iOS)
            {
                defaultUrl = "http://192.168.1.143:49393/TodoService.svc";
            }
    
            return defaultUrl;
        }
    }
    

    使用适当的终结点配置 Constants.cs 后,应该能够使用物理或虚拟设备连接到 Windows 10 工作站上运行的 TodoWCFService。