2019 年 4 月

第 34 卷,第 4 期

[Xamarin]

Xamarin.Forms 4.0 新变化

作者 Alessandro Del Del

在 2018 年 11 月举行的 Connect(); 大会上,Microsoft 公布了 Xamarin.Forms 4.0 的首个公共预览版,这是最新版热门开发技术,简化了跨不同 OS(包括 Android、iOS 和 Windows 10)创建共享 UI 的过程。随后发布了更新后的预览版,让开发人员可以更深入地了解这一主要版本的发布进展情况。让 Xamarin.Forms 4.0 引人注目的最重要新功能可归纳如下。

Shell 是应用容器,提供了 UI 所需的大部分常见功能。它可便于在一处集中描述应用的可视结构,并包括通用页面导航 UI、有深层链接的导航服务,以及集成搜索处理程序。目前,Shell 仅适用于 iOS 和 Android。

CollectionView 是全新控件,旨在以更高效的方式处理数据列表。它也是更新后的 CarouselView 控件版本的基础。

Visual 是新属性的名称,一些视图可用来根据给定设计呈现控件,从而跨平台轻松创建一致 UI。

Connect(); 特刊 (msdn.com/magazine/mt848639) 全面介绍了 Shell 功能,所以本文就不涉及它了。我将改为重点介绍 CollectionView、CarouselView、Visual,以及对现有代码库的其他各种改进。我还将简要介绍一下 Visual Studio 2019 预览版 2 中一些针对 Xamarin.Forms 的新功能。请注意,本文介绍的版本仍处于预览阶段,可能会发生变更。

创建和设置项目

若要使用最新的 Xamarin.Forms 4.0 预览版,需要先照常在 Visual Studio 2017 中创建移动应用 (Xamarin.Forms) 项目。选择 iOS 和 Android 平台,以及 .NET Standard 代码共享策略。在新项目就绪后,使用 NuGet 包管理器,务必要将 Xamarin.Forms 库升级到最新预发行版(截至本文撰写之时,最新预发行版为 4.0.0.94569-pre3)。安装此预览版时,NuGet 还会将 Xamarin.iOS.MaterialComponents 包添加到 iOS 平台项目中,我稍后将在讨论 Visual 属性时介绍这一点。

最后一步包括,在用于 Android 的 MainActivity.cs 文件和用于 iOS 的 AppDelegate.cs 文件中,启用 Xamarin.Forms 4.0 预览版中的所有试验性功能。在这两个文件中,将以下代码行添加到 Xamarin.Forms.Forms.Init 方法调用前面:

global::Xamarin.Forms.Forms.SetFlags("Shell_Experimental", "Visual_Experimental",
  "CollectionView_Experimental");

若要试用 Shell 功能,应添加 Shell_Experimental 标志;否则,在接下来的主题中,此为可选标志。

处理数据:CollectionView 控件

Xamarin.Forms 4.0 引入了全新控件 CollectionView,可用于显示和编辑数据列表。从体系结构的角度来看,CollectionView 有几大优势。首先,与 ListView 不同,数据模板不再依赖 Cell 视图,这就简化了可视化树。第二大优势应运而生,可以使用基于 XAML 的相同熟悉方法,对 CollectionView 对象进行编码,但代码更简单。此外,CollectionView 控件还支持水平布局和网格视图,简化了列表布局的管理。首先,创建一些将在 CollectionView 控件中显示的示例数据。在 .NET Standard 项目中,添加图 1 中所示的类,它表示简化后的产品定义。

图 1:Product 类的简化实现

public class Product : INotifyPropertyChanged
{
  public event PropertyChangedEventHandler PropertyChanged;
  private void OnPropertyChanged([CallerMemberName] string propertyName = null)
  {
    PropertyChanged?.Invoke(this,
      new PropertyChangedEventArgs(propertyName));
  }
  private int _productQuantity;
  private string _productName;
  private string _productImage;
  public int ProductQuantity
  {
    get
    {
      return _productQuantity;
    }
    set
    {
      _productQuantity = value;
      OnPropertyChanged();
    }
  }
  public string ProductName
  {
    get
    {
      return _productName;
    }
    set
    {
      _productName = value;
      OnPropertyChanged();
    }
  }
  public string ProductImage
  {
    get
    {
      return _productImage;
    }
    set
    {
      _productImage = value;
      OnPropertyChanged();
    }
  }
}

如果决定让产品实例可编辑,请注意 Product 类是如何通过实现 INotifyPropertyChanged 接口来支持更改通知的。下一步是定义公开产品信息的视图模型,其中的数据绑定到 UI。图 2**** 展示了这一步。

图 2:ProductViewModel 类向 UI 公开数据信息

public class ProductViewModel
{
  public ObservableCollection<Product> Products { get; set; }
  public ProductViewModel()
  {
    // Sample products
    this.Products = new ObservableCollection<Product>();
    this.Products.Add(new Product { ProductQuantity = 50, ProductName = "Cheese",
                                    ProductImage = "Cheese.png" });
    this.Products.Add(new Product { ProductQuantity = 10, ProductName = "Water",
                                    ProductImage = "Water.png" });
    this.Products.Add(new Product { ProductQuantity = 6, ProductName = "Bread",
                                    ProductImage = "Bread.png" });
    this.Products.Add(new Product { ProductQuantity = 40, ProductName = "Tomatoes",
                                    ProductImage = "Tomato.png" });
  }
}

在实际情况下,数据可能来自数据库,也可能来自 Web 服务;但在此示例中,我出于演示目的直接在代码中创建一些产品。本文随附的代码包含图 2 中使用的图像文件。在项目的 MainPage.xaml 文件中,可以将 CollectionView 控件声明为显示数据。关键要点是,向 ItemsSource 属性分配要显示的集合,并为列表中的每一项都定义数据模板。图 3 提供了一个示例。

图 3:将 CollectionView 声明为显示数据

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="https://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:XF4_WhatsNew"
             x:Class="XF4_WhatsNew.MainPage">
  <StackLayout>
    <CollectionView x:Name="ProductList" ItemsSource="{Binding Products}">
      <CollectionView.ItemTemplate>
        <DataTemplate>
          <Grid>
            <Grid.ColumnDefinitions>
              <ColumnDefinition Width="24"/>
              <ColumnDefinition />
            </Grid.ColumnDefinitions>
            <Image Source="{Binding ProductImage}" HeightRequest="22" />
            <StackLayout Grid.Column="1" Orientation="Vertical" Spacing="5">
              <Label Text="{Binding ProductName}" FontSize="Medium"
                TextColor="Blue"/>
              <Label Text="{Binding ProductQuantity}" FontSize="Small"
                TextColor="Black"/>
            </StackLayout>
          </Grid>
        </DataTemplate>
      </CollectionView.ItemTemplate>
    </CollectionView>
  </StackLayout>
</ContentPage>

在 XAML 中声明 CollectionView 并为 DataTemplate 对象分配 ItemTemplate 属性,与使用 ListView 控件时几乎相同,重要区别在于,不再需要处理 Cell 视图。然后,创建 ProductViewModel 类的实例,并将它分配给页面的 BindingContext 属性,这样数据绑定就能起作用,如要在 MainPage.xaml.cs 文件中编写的以下代码所示:

public partial class MainPage : ContentPage
{
  private ProductViewModel ViewModel { get; set; }
  public MainPage()
  {
    InitializeComponent();
    this.ViewModel = new ProductViewModel();
    this.BindingContext = this.ViewModel;
  }
}

如果运行此代码,便会看到图 4 中的结果。

使用 CollectionView 显示数据列表
图 4:使用 CollectionView 显示数据列表

你将看到,CollectionView 还提供其他有趣的属性,可用于实现不同布局和处理项选择。

方向和布局:如果使用过 Xamarin.Forms 中的 ListView 控件,就会知道没有可以将方向设置为水平的内置选项。这当然可以使用自定义呈现器来完成,但会增加代码复杂性。同样,也没有可以将布局从列表视图更改为网格视图的内置选项。不过,CollectionView 控件通过 ItemsLayout 属性让设置不同布局选项变得极为简单,可以向它分配 ListItemsLayout 类中的属性(如 VerticalList 和 HorizontalList)。默认情况下,ItemsLayout 属性分配有 ListItemsLayout.VerticalList,因此无需显式分配它来实现垂直布局。但要实现水平方向,请将以下代码片段添加到 XAML 中:

<CollectionView ItemsSource="{Binding Products}"
  It­emsLayout="{x:Static ListItemsLayout.HorizontalList}">
  ...
</CollectionView>

实际上,ItemsLayout 属性还可便于定义网格视图。在这种情况下,使用 GridItemsLayout 类,而不是 ListItemsLayout,如以下示例所示,其中展示了如何创建水平网格视图:

<CollectionView x:Name="ProductList" ItemsSource="{Binding Products}" >
  <CollectionView.ItemsLayout>
    <GridItemsLayout Orientation="Horizontal" Span="3" />
  </CollectionView.ItemsLayout>
    ...
</CollectionView>

默认情况下,GridItemsLayout 在水平布局中的单行内显示项,因此可以设置 Span 属性来确定网格内的行数。将项添加到数据绑定列表时,网格视图将水平扩大。可以使用下面的代码来实现垂直网格视图:

<CollectionView x:Name="ProductList" ItemsSource="{Binding Products}" >
  <CollectionView.ItemsLayout>
    <GridItemsLayout Orientation="Vertical" Span="2" />
  </CollectionView.ItemsLayout>
    ...
</CollectionView>

同样,垂直网格默认在单列中显示项,因此可以更改 Span 属性,并指定网格内的列数。

管理项选择:CollectionView 控件既允许选择单项或多项,也允许禁用项选择。可以向 SelectionMode 属性分配 Xamarin.Forms.SelectionMode 枚举中的值:None、Single 和 Multiple。使用 None,可以非常轻松地禁用项选择。类型为 object 的 SelectedItem 属性表示列表中的一个或多个选定项。然后,当用户选择 CollectionView 中的项时,便会触发 SelectionChanged 事件。只有在 SelectionMode 已分配有 Single(也是未指定 SelectionMode 时的默认值)或 Multiple 时,此类事件才有效。因此,XAML 中可能有以下分配:

<CollectionView x:Name="ProductList" ItemsSource="{Binding Products}"
  SelectionMode="Single" SelectionChanged="ProductList_SelectionChanged">
...
</CollectionView>

然后,使用 SelectionChanged 事件处理程序,可以通过类型为 Xamarin.Forms.SelectionChangedEventArgs 的参数来管理项选择。此对象公开两个有用的属性:CurrentSelection 和 PreviousSelection。顾名思义,第一个属性返回当前选定对象,而第二个属性则返回先前选择的对象。这两个属性非常有用,因为在某些情况下,可能需要知道在选择新对象之前实际选择了什么对象实例。这两个属性都是 IReadOnlyList<object> 类型,都能公开选定项的列表。如果 SelectionMode 设置为 Single,可以按如下所示检索选定项的实例:

private void ProductList_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
  var selectedProduct = this.ProductList.SelectedItem as Product;
  var singleProduct = e.CurrentSelection.FirstOrDefault() as Product;
}

无论使用的是 CollectionView.SelectedItem 属性,还是集合中的第一项,都需要将生成的对象强制转换为预期类型(在此示例中,类型为 Product)。如果 SelectionMode 设置为 Multiple,首先需要将选定项的只读集合强制转换为预期类型的对象列表,如下所示:

private void ProductList_SelectionChanged(object sender,
  SelectionChangedEventArgs e)
{
  var selectedItems = e.CurrentSelection.Cast<Product>();
  foreach(var product in selectedItems)
  {
    // Handle your object properties here...
  }
}

对于单项选择和多项选择,相同的方法都对 SelectionChangedEventArgs.PreviousSelection 属性有效。作为新式视图,CollectionView 还支持在处理项选择时使用模型-视图-视图模型 (MVVM) 模式。这样一来,既能使用可绑定到视图模型中 Command 类实例的 SelectionChangedCommand 属性,还能使用可便于将参数传递给命令的 SelectionChangedCommandParameter。例如,扩展 ProductViewModel 类,以添加将会数据绑定到 CollectionView 的 SelectedItem 属性的 SelectedProduct 属性,以及用于处理项选择的新命令。为简单起见,Command 类型的属性既不使用泛型实现,也不使用命令参数。图 5 展示了视图模型。

图 5:借助 MVVM 支持来扩展视图模型

public class ProductViewModel: INotifyPropertyChanged
{
  public ObservableCollection<Product> Products { get; set; }
  private Product _selectedProduct;
  public Product SelectedProduct
  {
    get
    {
      return _selectedProduct;
    }
    set
    {
      _selectedProduct = value;
      OnPropertyChanged();
    }
  }
  private Command _productSelectedCommand;
  public Command ProductSelectedCommand
  {
    get
    {
      return _productSelectedCommand;
    }
      set
      {
        _productSelectedCommand = value;
        OnPropertyChanged();
      }
    }
  public ProductViewModel()
  {
    // Sample products
    this.Products = new ObservableCollection<Product>();
    this.Products.Add(new Product { ProductQuantity = 50,
      ProductName = "Cheese", ProductImage = "Cheese.png" });
    this.Products.Add(new Product { ProductQuantity = 10,
      ProductName = "Water", ProductImage = "Water.png" });
    this.Products.Add(new Product { ProductQuantity = 6,
      ProductName = "Bread", ProductImage = "Bread.png" });
    this.Products.Add(new Product { ProductQuantity = 40,
      ProductName = "Tomatoes", ProductImage = "Tomato.png" });
    this.ProductSelectedCommand =
      new Command(ExecuteProductSelectedCommand,
        CanExecuteProductSelectedCommand);
  }
  private bool CanExecuteProductSelectedCommand(object arg)
  {
    return this.SelectedProduct != null;
  }
  private void ExecuteProductSelectedCommand(object obj)
  {
    // Handle your object here....
    var currentProduct = this.SelectedProduct;
  }
  public event PropertyChangedEventHandler PropertyChanged;
  private void OnPropertyChanged([CallerMemberName] string propertyName = null)
  {
    PropertyChanged?.Invoke(this,
      new PropertyChangedEventArgs(propertyName));
  }
}

然后,在 XAML 中,分配项选择属性,如下所示:

<CollectionView x:Name="ProductList" ItemsSource="{Binding Products}"
                SelectedItem="{Binding SelectedProduct, Mode=TwoWay}"
                SelectionChangedCommand="{Binding ProductSelectionCommand}"
                SelectionMode="Single">
</CollectionView>

使用这种方法,可以处理实现业务逻辑的视图模型中的选定对象,同时将数据处理与视图处理脱离开来。

管理空列表:CollectionView 提供的最重要功能之一,当然也是我最喜欢的功能之一,就是在数据绑定列表为空时免费显示其他视图的内置选项。这是通过 EmptyView 属性实现的,此属性可便于指定在数据为空时显示的视图。其工作原理如下所示:

<CollectionView>
...           
  <CollectionView.EmptyView>
     <Label Text="No data is available" TextColor="Red"
       FontSize="Medium"/>
  </CollectionView.EmptyView>
</CollectionView>

上面的代码生成图 6 中所示的结果。使用这种方法,无需创建空状态对应的数据模板,也无需实现数据模板选择器。

提供空状态对应的视图
图 6:提供空状态对应的视图

或者,若要实现自定义视图,可使用 EmptyViewTemplate 属性。例如,下面的代码展示了如何在列表为空时显示图像和文本消息:

<CollectionView.EmptyViewTemplate>
  <DataTemplate>
    <StackLayout Orientation="Vertical" Spacing="20">
      <Image Source="EmptyList.png" Aspect="Fill"/>
      <Label Text="No data is available" TextColor="Red"/>
    </StackLayout>
  </DataTemplate>
</CollectionView.EmptyViewTemplate>

ScrollingCollectionView 也公开 ScrollTo 方法,可用于以编程方式将列表滚动到指定项。下面展示了如何滚动到列表中的第三项:

 

ProductList.ScrollTo(2);

更简单的 API 外围应用:CollectionView 控件的 API 外围应用比 ListView 更简单。例如,CollectionView 没有可用于分隔符的属性(例如,SeparatorVisibility 和 SeparatorColor),因此你可以在数据模板中为 BoxView 等视图实现你自己的分隔符。CollectionView 不实现 ListView.RowHeight 等属性,因为项高度现在取决于列表中的第一项。ListView 中的 HasUnevenRows 属性在 CollectionView 中的对应项为 ItemSizingStrategy。关于“下拉以刷新”技术,ListView 公开的 IsPullToRefreshEnabled、IsRefreshing 和 RefreshCommand 等属性在 CollectionView 控件中不可用。不过,Microsoft 正在努力实现支持视图刷新状态的属性。GitHub 上的 Xamarin.Forms.CollectionView 规范文档 (bit.ly/2N6iw8a) 详细说明了刷新选项以及其他 API 实现(包括体系结构决策)。

CarouselView 简介

CarouselView 控件并不是 Xamarin.Forms 4.0 中新推出的。不过,它已根据 CollectionView 结构和性能完全重建。

使用 CarouselView,可以卡格式一次显示列表中的一个对象。图 7 截取自官方 Xamarin 博客 (bit.ly/2BwWMNV),展示了 CarouselView 如何支持轻扫浏览项。

CarouselView 以卡形式显示项,同时启用轻扫手势
图 7:CarouselView 以卡形式显示项,同时启用轻扫手势

关于前面介绍的示例代码,可使用 CarouselView 以卡形式显示产品列表,如图 8 所示。

图 8:使用 CarouselView 以卡形式显示项

<CarouselView x:Name="ProductList" ItemsSource="{Binding Products}">
  <CarouselView.ItemsLayout>
    <GridItemsLayout Orientation="Horizontal"
                     SnapPointsAlignment="Center"
                     SnapPointsType="Mandatory"/>
  </CarouselView.ItemsLayout>
  <CarouselView.ItemTemplate>
    <DataTemplate>
      <Grid>
        <Grid.ColumnDefinitions>
          <ColumnDefinition Width="24"/>
          <ColumnDefinition />
        </Grid.ColumnDefinitions>
        <Image Source="{Binding ProductImage}" HeightRequest="22" />
        <StackLayout Grid.Column="1" Orientation="Vertical"
                     Spacing="5">
          <Label Text="{Binding ProductName}" FontSize="Medium"
                 TextColor="Blue"/>
          <Label Text="{Binding ProductQuantity}" FontSize="Small"
                 TextColor="Black"/>
        </StackLayout>
      </Grid>
    </DataTemplate>
  </CarouselView.ItemTemplate>
  <CarouselView.EmptyView>
    <Label Text="No data is available" TextColor="Red"
           FontSize="Medium"/>
  </CarouselView.EmptyView>
</CarouselView>

接下来将重点介绍代码中的关键要点。

声明 CarouselView:在 XAML 中声明 CarouselView 与声明 CollectionView 非常相似。仍需要将 ItemsSource 属性与要显示的数据集合绑定在一起。请注意,还可以提供空状态对应的视图,就像使用 CollectionView 时一样。

方向:CarouselView 同时支持水平方向和垂直方向。向 ItemsLayout 属性分配 GridItemsLayout 实例,同时将 Horizontal 或 Vertical 传递到 Orientation 属性。

添加吸附点:对 ItemsLayout 使用 SnapPointsAlignment 和 SnapPointsType 属性,可以配置卡的行为。例如,在图 8 中,SnapPointsType 和 SnapPointsAlignment 属性确保卡居中显示。如果没有这些分配,卡可能会在与其他卡切换时中途停止。SnapPointsAlignment 的允许值为 Center、End 和 Start。SnapPointsType 的允许值为 Mandatory、MandatorySingle 和 None。

与 CollectionView 一样,请注意最终版本可能提供不同的 API 实现。使用当前预览版,可能也会在应用呈现视图时遇到 InvalidCastException。这是不稳定的内部版本,所以出现此类问题也在意料之中。

使用 Visual 属性创建一致 UI

Xamarin.Forms 4.0 为开发人员提供了创建 UI 的新方法,他们几乎不费吹灰之力就可以创建在 Android 和 iOS 上看起来基本相同的 UI。关键在于,VisualElement 类公开的新属性 Visual。Visual 属性可确保使用其他新呈现器(而不是默认呈现器)绘制可视元素。在当前的预览版状态下,Xamarin.Forms 4.0 包含基于材料设计的试验性呈现机制,目前可用于以下视图:Button、Entry、Frame 和 ProgressBar。可以按如下所示启用新的可视化呈现:

 

<Entry Visual="Material" />

图 9**** 截取自 Microsoft Xamarin.Forms 4.0 发行说明 (bit.ly/2BB4MxA),展示了跨 Android 和 iOS 一致的 UI 示例(使用 Visual 创建)。

使用 Visual 属性创建的一致 UI
图 9:使用 Visual 属性创建的一致 UI

也可以在 ContentPage 一级分配 Visual 属性,以确保代码中的任何受支持视图都会使用指定设计。若要使用 Visual,项目必须满足一些要求:

  • 在 iOS 项目中,必须安装 Xamarin.iOS.MaterialComponents NuGet 包。不过,此包在你将 Xamarin.Forms 包升级到 4.0 时自动安装。
  • 对于 Android,项目必须基于 API 29,且支持库必须使用版本 28。此外,应用主题还必须继承自“材料组分”主题之一。

值得注意的是,Visual 开发才刚刚开始,因此开发人员可以合理地期望许多更新推出。总之,这是一个非常有用的附加属性。当你需要创建跨不同平台完全相同的 UI 时,它会为你节省大量时间。

其他改进

在 Xamarin.Forms 4.0 中,现在可以指定在“下拉以刷新”启用时的旋转图标颜色。这是通过分配 RefreshControlColor 属性完成的,如下所示:

<ListView RefreshControlColor="Green"/>

此外,在 ListView 中,只需分配 HorizontalScrollBarVisibility 和 VerticalScrollBarVisibility 属性,就可以隐藏滚动条,而无需编写自定义呈现器:

<ListView HorizontalScrollBarVisibility="Always"
  VerticalScrollBarVisibility="Never"/>

可取值为 Always(始终可见)、Never(从不可见)和 Default(以目标平台的默认值为依据)。  此外,Editor 控件现在支持文本预测,就像在 Entry 控件中一样。可以直接向 IsTextPredictionEnabled 属性分配 True 或 False,如下所示:

<Editor IsTextPredictionEnabled="True" />

在 Xamarin.Forms 4.0 中,SwitchCell 视图接收 OnColor 属性,就像之前主要版本中的 Switch 视图一样。因此,现在,在数据模板中使用 SwitchCell 时,可以为活动状态设置不同的颜色:

<SwitchCell OnColor="Red"/>

从实用的角度来看,这些是最有趣的改进,而全部更新和已修复的问题则在发行说明页上列出,并且最终会在 Xamarin.Forms 4.0 最后投入生产后立即更新。

简要介绍 Visual Studio 2019 中的 Xamarin.Forms

Microsoft 最近发布了 Visual Studio 2019 预览版 3 (bit.ly/2QcOK6z),提前展示了 Visual Studio 下一主要版本即将推出的功能,包括支持 Xamarin.Forms 4.0。例如,现在可以在“新跨平台应用”对话框中选择新项目模板“Shell”(见图 10)。此模板根据 Shell 功能生成新项目,在主页 XAML 代码的 Shell 对象中引用了多个视图。

基于 Shell 的新项目模板
图 10:基于 Shell 的新项目模板

Visual Studio 2019 还在 Xamarin.Forms 中引入了新“属性”面板(如图 11 所示),其他开发平台中已有此面板。当你在 XAML 代码或 Xamarin.Forms 预览器中单击视图后,它会立即显示属性值。

新“属性”面板
图 11:新“属性”面板

如果你通过“属性”面板更改属性值,XAML 代码会自动更新为反映你所做的更改。请注意一些类型适用的特定编辑器,如类型为 Xamarin.Forms.Color 的属性适用的颜色编辑器。

总结

与之前的主要版本一样,Xamarin.Forms 4.0 展示了 Microsoft 在改善开发体验方面的巨大投资,不仅推出了新功能,还提高了现有工具和代码库的可靠性,而这些正是开发人员在实际项目中所需要的。很期待即将举行的 Microsoft Build 大会(5 月 6 日至 8 日在西雅图召开)带来其他更多新闻。


Alessandro Del Sole 自 2008 年以来一直是 Microsoft MVP。此外,他还是 Xamarin 认证开发人员。他发表过很多关于使用 Visual Studio 进行 .NET 开发的书籍、电子图书、指导视频和文章。Del Sole 在 Fresenius Medical Care 担任高级软件工程师,专注于使用 Xamarin 生成面向医疗保健市场的 .NET 和移动应用。你可以关注他的 Twitter @progalex。**

衷心感谢以下 Microsoft 技术专家对本文的审阅:Paul DiPietro