Connect(); 2018 特刊

第 33 卷,第 13 期

跨平台开发 - Xamarin.Forms Shell 简介

作者 David Ortinau; 2018 特刊

对于热衷 XAML 和 C# 的跨平台开发人员,Xamarin.Forms 是一款受欢迎的工具包,因为它最大限度地实现了代码共享,同时还提供对所有本机平台 API 和 UI 控件的完全访问权限。此功能包含的技术和概念可能会令刚开始使用的人欢欣鼓舞,也可能会令他们困惑不解。事实上,一些开发人员一开始就觉得很沮丧。选择 Xamarin 是为了提高工作效率,而最不想遇到的就是不必要的麻烦。在今年的 Connect(); 大会上,我们非常高兴地引入 Xamarin.Forms Shell,这是移动应用开发的默认新起点,不仅可以降低复杂性,还能提高工作效率。

顾名思义,从根本上说,Shell 就是一个容器,负责处理每个应用都需要的基本 UI 功能,这样开发人员就能以应用的核心工作为重点。现有 iOS 和 Android 应用也可以轻松采用 Shell,并立即受益于导航、UI 性能和扩展性方面的改进。Shell 的优势如下:

  • 在一个位置集中描述应用的可视结构
  • 通用导航 UI 和无所不在的导航服务(含深层链接)
  • 集成的搜索处理程序,可提高整体应用内搜索体验
  • 默认可扩展理念,可提高多功能性和灵活性

应用

在每个项目开始时,都会有人为你勾勒出要生成的应用结构(但愿不仅仅只是在脑海中勾勒)。有时是通过设计构成形式提供,有时只是用铅笔在纸上画出的草图。Shell 可以非常轻松地获取内容,并将内容转换为正常运行的应用容器,以供任何人填充内容和功能。

在本文中,我将使用名为“Tailwind Traders”的移动购物应用示例。这是团队生成的新参考应用,用于展示如何结合使用 Xamarin.Forms Shell、Azure、认知服务以及其他多种功能和服务。看一看我们卓越设计团队提供的设计构成,如图 1 所示。

Tailwind Traders 示例应用的设计构成
图 1:Tailwind Traders 示例应用的设计构成

从显示的屏幕中可以看出,应用提供了所需的全部明显功能,包括登录和注册流、产品类别浏览体验和搜索以及签出流。此应用还利用设备摄像头和 Azure 自定义视觉 API 的强大功能来实时识别产品。

快速入门

接下来一起使用 Shell 快速生成此应用。打开 Visual Studio 2019,并使用 Xamarin.Forms 启动新的跨平台应用。为了方便本文演示并了解 Shell 的强大功能,接下来将从空白项目开始生成 Tailwind Traders 应用结构。

生成项目文件后,立即打开 App.xaml.cs,并注意 MainPage 是否已设置为新 Shell 实例。(可以从 aka.ms/xf-shell-templates 下载 Shell 模板。) 从结构上看,这是与过去典型 Xamarin.Forms 应用的唯一区别。代码如下:

namespace TailwindTraders.Mobile
{
  public partial class App
  {
    public App()
    {
      InitializeComponent();
      MainPage = new AppShell();
    }
  }
}

在 .NET Standard 库项目的根目录中,打开 AppShell.xaml,如图 2 所示。

图 2:单页 Shell.xaml

<?xml version="1.0" encoding="UTF-8"?>
<Shell xmlns="http://xamarin.com/schemas/2014/forms"
       xmlns:x="https://schemas.microsoft.com/winfx/2009/xaml"
       xmlns:local="clr-namespace:TailwindTraders"
       RouteHost="tailwindtraders.com"
       RouteScheme="app"
       FlyoutBehavior="Disabled"
       Title="TailwindTraders"
       x:Class=" TailwindTraders.AppShell">
  <ShellItem>
    <ShellSection>
      <ShellContent>
        <local:MainPage/>
      </ShellContent>
    </ShellSection>
  <ShellItem>
</Shell>

接下来将分解此文件的各个部分。Shell 由三个层次结构元素组成:ShellItem、ShellSection 和 ShellContent。每个 ShellContent 都是 ShellSection 的子级,ShellSection 又是 ShellItem(即 Shell 的所有部分)的子级。这三个元素本身都不代表 UI,而是代表应用体系结构组织。Shell 需要使用这些项,并针对运行平台生成适当的导航 UI:

  • ShellItem:应用的顶级结构,由浮出控件中的项表示。可以包含多个 ShellSection。
  • ShellSection:应用内容分组,可通过底部的选项卡进行导航。可以包含一个或多个 ShellContent(多个 ShellContent 可通过顶部选项卡进行导航)。
  • ShellContent:应用的 ContentPage。

我可以使用这三个元素来描述 Tailwind Traders 移动应用的可视结构。我将忽略登录和注册流,并添加多个 ShellItem(由左侧的浮出控件菜单表示)托管内容。

为什么不对 Shell 概念使用 FlyoutItem BottomTab、TopTab 等名称?我们的 Microsoft 团队对此进行了多次讨论,并认为 Xamarin.Forms 适用于未来的已知平台,而这些平台有时并不共用确切的选项卡或菜单概念。通过保留命名法摘要,可以通过样式和模板来决定是应在不同平台之间一致地表示这些元素,还是应遵循每个平台的设计美学。当然,始终欢迎大家提供这些方面的反馈!

图 3 提供了一个示例。在图中,可以看到浮出控件中的菜单(UI 下三分之二),其中自动填充有 ShellItem。这样就可以转到应用的不同区域了。除了这些项之外,还可以显式添加与 ShellItem 无关的菜单项。顶部的浮出控件标头(两个按钮)表示特殊内容,由要在此空间内显示的任何内容组成。若要在 Shell.xaml 中声明自定义 FlyoutHeader,请运行下面的代码:

<Shell.FlyoutHeader>
  <local:FlyoutHeader />
</Shell.FlyoutHeader>

FlyoutMenu 元素
图 3:FlyoutMenu 元素

使用标头元素,可以控制标头在用户滚动显示内容时的行为。有以下三个选项:

  • Fixed:标头在下面的内容滚动时固定不变。
  • Scroll:与菜单项一起滚动。
  • CollapseOnScroll:在用户滚动时以视差方式折叠。

若要调整此行为,请将 Shell 中的 FlyoutHeaderBehavior 属性设置为前面刚刚详述的相应值。现在,我们让标头在下面的内容滚动时固定不变:

<Shell
  x:Class="TailwindTraders.Mobile.Features.Shell.Shell"
  FlyoutHeaderBehavior="Fixed"  
  ...            
  >             
  ...            
</Shell>

接下来将设置内容。查看设计时,可以看到有一个主屏幕、一系列产品类别、一个配置文件,最后是注销屏幕。接下来从主屏幕开始,运行以下 XAML 代码:

<ShellItem Title="Home">
  <ShellSection>
    <ShellContent>
      <local:HomePage />
    </ShellContent>
  </ShellSection>
</ShellItem>

通过从里到外分解此 XAML,我已将 HomePage 添加到应用中,这是第一个启动的 ContentPage,因为它是 Shell 文件中声明的第一个内容。这与现有 Xamarin.Forms 应用中使用的 ContentPage 类型相同,现托管在 Shell 上下文中。

对于此设计,我只需要设置标题,但 ShellItem 还提供 FlyoutIcon 属性,可便于提供在项左侧显示的图像。图标可以是任意 Xamarin.Forms ImageSource。

现在运行应用。单击主页上的汉堡图标,以打开浮出控件菜单。点击此菜单项可转到主屏幕(这是当前的唯一屏幕)。我们一起来添加更多内容。

接下来,我将实现产品类别,如“节日装饰”、“设备”等。我可以为每种类别添加 ShellItem,但由于产品类别页都是内容不同的相同页面,因此我能够巧妙添加。我将使用简单的 Menu­Item 转到相同页面,并通过 CommandParameter 传递数据,以免不必要的页面重复。下面是用于在 Shell.xaml 中添加 MenuItem 的代码:

<Shell.MenuItems>
  <MenuItem
    Command="{Binding ProductTypeCommand}"
    CommandParameter="1"
    Text="Holiday decorations" />
</Shell.MenuItems>

Shell 的特色功能之一是,支持数据绑定。在此示例中,我在视图模型上有一个可执行导航操作的“Command”。与 ShellItem 一样,MenuItem 也需要使用文本和图标。此外,与 ShellItem 一样,我也可以在 Shell 上设置 MenuItemTemplate 属性,以提供样式或自定义模板来进一步自定义设计。

为了完成任务,我可以为每种类别添加更多菜单项。图 4 展示了所有菜单项的代码,而图 5**** 则展示了应用浮出控件菜单中的可视结果。

图 4:所有菜单项

<?xml version="1.0" encoding="UTF-8" ?>
<Shell
  x:Class="TailwindTraders.AppShell"
  FlyoutHeaderBehavior="Fixed"
  xmlns="http://xamarin.com/schemas/2014/forms"
  xmlns:x="https://schemas.microsoft.com/winfx/2009/xaml"
  xmlns:local="clr-namespace:TailwindTraders.Views"
  Title="Tailwind Traders"
  x:Name="theShell"
  Route="tailwindtraders"
  RouteHost="microsoft.com"
  RouteScheme="app">
  <Shell.MenuItems>
    <MenuItem
      Command="{Binding ProductTypeCommand}"
      CommandParameter="1"
      Text="Holiday decorations" />
    <MenuItem
      Command="{Binding ProductTypeCommand}"
      CommandParameter="2"
      Text="Appliances" />
    <MenuItem
      Command="{Binding ProductTypeCommand}"
      CommandParameter="3"
      Text="Bathrooms" />
    <MenuItem
      Command="{Binding ProductTypeCommand}"
      CommandParameter="4"
      Text="Doors &amp; Windows" />
    <MenuItem
      Command="{Binding ProductTypeCommand}"
      CommandParameter="5"
      Text="Flooring" />
    <MenuItem
      Command="{Binding ProductTypeCommand}"
      CommandParameter="6"
      Text="Kitchen" />
    <MenuItem
      Command="{Binding ProductTypeCommand}"
      CommandParameter="7"
      Text="Storage" />
  </Shell.MenuItems>
  <ShellItem Title="Home">
    <ShellSection>
      <ShellContent>
        <local:HomePage />
      </ShellContent>
    </ShellSection>
  </ShellItem>
</Shell>

包含所有菜单项的浮出控件
图 5:包含所有菜单项的浮出控件

添加更多页面

现在,将设计中的配置文件页面添加到应用。向项目添加新的 ContentPage,再返回到 Shell.xaml 文件。可以复制对 HomePage 使用的 XAML(如图 4 所示),并将它替换为配置文件页面,但这样做有阻碍应用的风险,因为 HomePage 是在应用启动期间立即创建的。为了避免一次加载应用的所有页面,我使用如下的数据模板:

<ShellContent
  Title="Profile"
  ContentTemplate="{DataTemplate local:ProfilePage}" />

我提供数据模板,而不是直接将 ContentPage 提供给 ShellContent 的 content 属性。当用户转到屏幕时,Shell 动态实例化所请求的页面。

这种处理方式与 Home­Page 相比,需要注意的一点是,我已省略 ShellItem 和 ShellSection 包装器,并直接将标题置于 ShellContent 中。这样会大大降低详细程度,Shell 知道如何通过提供必需逻辑包装器来为我处理它。还需要注意的是,这些包装器不会将 UI 视图引入树。在编写 Shell 时,考虑到了呈现速度和内存消耗。这样做的结果是,通过在此新 Shell 上下文中托管当前相同的内容和 UI,可以降低对 Android OS 造成的性能影响。当然,你最终仍负责搭建应用的内部架构,但 Shell 提供了一个很好的起点。

设置浮出控件样式

可以使用 CSS 或 XAML 样式设置来设置 Shell 和 FlyoutMenu 各个方面的样式,就像设置其他任何 XAML 元素的样式一样。若要进一步了解浮出控件菜单项的显示方式,该怎么办?回头看看图 3**** 中的设计,就会发现菜单项比其他 Shell 项更醒目。

菜单项和 Shell 项的显示是可扩展的,具体方法是向 Shell 提供 DataTemplate。MenuItem 是使用 Shell 的 MenuItemTemplate 在浮出控件菜单中呈现,ShellItem 是使用 ItemTemplate 呈现。若要完全控制这些外观,请将每个属性设置为包含自定义 ContentView 的 DataTemplate。Shell 将向模板 BindingContext 提供 Title 和 Icon 可绑定属性,如图 6 所示。图 7**** 展示了可视结果。

图 6:在 Shell.xaml 中自定义 ShellItem 的项模板

<Shell.ItemTemplate>
  <DataTemplate>
    <ContentView HeightRequest="32">
      <ContentView.Padding>
        <Thickness
          Left="32"
          Top="16" />
      </ContentView.Padding>
      <Label Text="{Binding Title}" />
    </ContentView>
  </DataTemplate>
</Shell.ItemTemplate>
<Shell.MenuItemTemplate>
  <DataTemplate>
    <ContentView HeightRequest="32">
      <ContentView.Padding>
        <Thickness
          Left="32"
          Top="16" />
      </ContentView.Padding>
      <Label Text="{Binding Text}" FontAttributes="Bold" />
    </ContentView>
  </DataTemplate>
</Shell.MenuItemTemplate>

浮出控件项模板结果图像
图 7:浮出控件项模板结果图像

除了自定义项呈现器外,还将向浮出控件添加精美标头,其中包括一个带有标签的框,以及两个可便于快速访问摄像头功能的按钮。与其他模板一样,在 Shell.xaml 文件中为 FlyoutHeaderTemplate 添加一个。内容可以是任意 ContentView,所以此时使用 StackLayout 垂直定位子控件,如图 8 中的代码所示。添加一些样式以让它接近设计构成,并运行应用以查看结果,如图 9**** 所示。可以在 Shell 元素中设置 FlyoutHeaderBehavior,以确定标头是固定不变,还是在用户滚动屏幕时一起滚动或折叠。

图 8:FlyoutHeaderTemplate

<Shell.FlyoutHeaderTemplate>
  <DataTemplate>
    <StackLayout HorizontalOptions="Fill" VerticalOptions="Fill"
      BackgroundColor="White" Padding="16">
      <StackLayout.Resources>
        <Style TargetType="Button">
          <Setter Property="BackgroundColor" Value="White" />
          <Setter Property="BorderColor" Value="#2F4B66" />
          <Setter Property="BorderWidth">2</Setter>
          <Setter Property="CornerRadius">28</Setter>
          <Setter Property="HeightRequest">56</Setter>
          <Setter Property="Padding">
            <Thickness
              Left="24"
              Right="24" />
           </Setter>
         </Style>
       </StackLayout.Resources>
       <Label FontSize="Medium" Text="Smart Shopping">
         <Label.Margin>
           <Thickness Left="8" />
         </Label.Margin>
       </Label>
       <Button Image="photo" Text="By taking a photo">
         <Button.Margin>
           <Thickness Top="16" />
         </Button.Margin>
       </Button>
       <Button Image="ia" Text="By using AR">
         <Button.Margin>
           <Thickness Top="8" />
         </Button.Margin>
       </Button>
     </StackLayout>
   </DataTemplate>
 </Shell.FlyoutHeaderTemplate>

包含标头的浮出控件图像
图 9:包含标头的浮出控件图像

导航

现在是时候实现转到菜单项页面的命令了。为此,我将使用 Shell 引入的基于 URI 的新路由。使用 URI,用户可以立即跳转到应用的任何部分,甚至能向后跳转,而无需在两个点之间创建所有页面。接下来将介绍此过程是如何完成的。

首先,我需要声明路由,从适用于我的应用的方案和主机开始,如下所示:

<Shell
  Route="tailwindtraders"
  RouteHost="www.microsoft.com"
  RouteScheme="app"

通过将这些片段组合到 URL 中,我最后得到以下 URI:app://www.microsoft.com/tailwindtraders。

我在 Shell 文件中定义的每个 Shell 元素还有 route 属性,稍后可用于以编程方式导航。对于不使用 Shell 元素表示的页面,我可以显式注册路由。这就是我将对添加到浮出控件中的菜单项所执行的操作。其中每一项都会转到 ProductCategoryPage,此页面列出了特定类别的产品。下面是路由注册代码:

Routing.RegisterRoute("productcategory", typeof(ProductCategoryPage));

现在,我可以在 Shell.cs 的构造函数中声明必要的路由,也可以在调用路由之前运行的任意位置声明。菜单项公开用于实现必要导航的命令,如下面的代码所示:

public ICommand ProductTypeCommand { get; } =
  new Command<string>(NavigateToProductType);
private static void NavigateToProductType(string typeId)
  {
    (App.Current.MainPage as Xamarin.Forms.Shell).GoToAsync(
      $"app:///tailwindtraders/productcategory?id={typeId}", true);
  }

Shell 的另一大优势是,它有可从应用中的任意位置访问的静态导航方法。过去需要担心导航服务是否可用、将它从视图传递到视图模型,还需要添加导航页面来包装所有内容,这样的日子已经一去不复返了。现在,可以获取对应用 Shell 的引用,即可作为 App.Current 属性访问的应用 MainPage。前面的代码片段中体现了这一点。若要执行导航,请调用 GoToAsync 方法,同时将有效 URL 作为 ShellNavigationState 传入。可以通过字符串或 URI 构造 ShellNavigationState。再次查看代码,可以看到 GoToAsync 也只允许提供字符串,Shell 将执行操作来实例化 ShellNavigationState。

数据可以通过查询字符串参数在视图和视图模型之间进行传递。当你使用 QueryProperty 属性修饰相应属性时,Shell 将在 ContentPage 或 ViewModel 上直接设置这些值,如图 10 所示。

图 10:查询属性示例

[Preserve]
[QueryProperty("TypeID", "id")]
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class ProductCategoryPage : ContentPage
{
  private string _typeId;
  public ProductCategoryPage()
  {
    InitializeComponent();
    BindingContext = new ProductCategoryViewModel();
  }
  public string TypeID
  {
    get => _typeId;
    set => MyLabel.Text = value;
  }
}

QueryProperty 从接收类中获取公共属性名(在此示例中为“TypeID”),并获取 URL 中使用的查询字符串参数名称(在此示例中为“id”)。

拦截回退操作

拦截回退操作是移动应用开发中的常见要求,这对 Xamarin.Forms 来说是一个挑战。Shell 修复了此问题,允许在完成前后挂钩到导航路由,以实现大量自定义需求。下面是一个导航处理示例,它先使用 XAML 代码分配事件处理程序:

<Shell           ...
  Navigating="Shell_Navigating"

再对事件处理程序使用 C# 代码:

private void Shell_Navigating(object sender, ShellNavigatingEventArgs e)
{
  if (// Some Boolean evaluation)
  {
    e.Cancel(); // Do not allow this navigation AND/OR do something else
  }
}

在 Shell 实例上,将事件处理程序添加到导航事件。在代码隐藏中,ShellNavigatingEventArgs 提供了导航的基本详细信息,如图 11**** 所示。

图 11:ShellNavigatingEventArgs

元素 类型 说明
当前 ShellNavigationState 当前页的 URI。
ShellNavigatinState 表示导航起源位置的 URI。
目标 ShellNavigationState 表示导航目标位置的 URI。
CanCancel Boolean 指明能否取消导航的属性。
取消 Boolean 用于取消已请求导航的方法。
已取消 Boolean 指明当前导航是否已取消的属性。

选项卡,选项卡,到处都是选项卡

浮出控件菜单是常用于导航的 UI 模式。考虑应用中的内容层次结构时,顶级或最外层导航是浮出控件菜单。随后,下一个详细级别是底部选项卡。如果没有浮出控件,通常将底部选项卡视为应用中的顶级导航。然后,在底部选项卡中,下一个级导航是顶部选项卡。除此之外,就是从一个页面推送到另一个页面的单页。这就是 Shell 提供导航 UI 所采取的一种固执己见的方法。

先从底部选项卡开始。一个 ShellItem 中的每个 ShellSection(如果有多个的话)都可以表示为底部选项卡。下面的 XAML 代码示例展示了如何为应用生成底部选项卡:

<ShellItem Title="Bottom Tab Sample" Style="{StaticResource BaseStyle}">
  <ShellSection Title="AR" Icon="ia.png">
    <ShellContent ContentTemplate="{DataTemplate local:ARPage}"/>
  </ShellSection>
  <ShellSection Title="Photo" Icon="photo.png">
    <ShellContent ContentTemplate="{DataTemplate local:PhotoPage}"/>
  </ShellSection>
</ShellItem>

此代码在一个 ShellItem 中显示两个 ShellSection。这些 ShellSection 在 UI 中表示为屏幕底部选项卡。如果不需要浮出控件,又如何呢?如果只有一个 ShellItem,可以通过将“FlyoutBehavior”设置为“Disabled”来隐藏它。可以使用现有样式选项设置选项卡样式,也可以通过提供自定义呈现器来设置样式。与可以是自定义数据模板的浮出控件菜单项不同,选项卡更特定于平台。若要设置选项卡的颜色样式,请对 TabBar 项使用 Shell 类的样式属性,如下所示:

<Style x:Key="BaseStyle" TargetType="Element">
  <Setter Property=
    "Shell.ShellTabBarBackgroundColor"
    Value="#3498DB" />
  <Setter Property=
    "Shell.ShellTabBarTitleColor"
    Value="White" />
  <Setter Property=
    "Shell.ShellTabBarUnselectedColor"
    Value="#B4FFFFFF" />
  </Style>

将样式类分配给 ShellItem 会将这些颜色应用到相应部分中的所有选项卡。

现在将转向顶部选项卡。若要让内容可从顶部选项卡进行导航,可以在一个 ShellSection 中添加多个 ShellContent 项。样式应用就像上一底部选项卡示例一样。代码如下:

<ShellItem Title="Store Home" Shell.TitleView="Store Home"
  Style="{StaticResource BaseStyle}">
    <ShellSection Title="Browse Product">
      <ShellContent Title="Featured"
        ContentTemplate=
        "{DataTemplate local:FeaturedPage}" />
      <ShellContent Title="On Sale"
        ContentTemplate=
        "{DataTemplate local:SalePage}" />
    </ShellSection>
  </ShellItem>

路线图

Xamarin.Forms Shell 中还有更多功能有待发现。我可以继续描述如何自定义导航栏、后退按钮,以及功能非常强大的搜索处理程序的所有方面。使用此搜索处理程序,向页面添加搜索功能不再是难事。这些功能及其他功能现已发布,相关文档将会在稳定版快要发布时推出。

Shell 之旅才刚刚开始。我们从 Xamarin.Forms 开发人员那里清楚地了解到,通常需要让 iOS 和 Android 应用看起来差不多或完全一样。为了解决此问题,我们计划发布 Material Shell,这是一种 Shell 实现,用于将 Google 的 Material Design 样式应用为所有受支持控件的起点。这些控件仍是本机的,因此不会影响性能或功能。

导航转换和 segue 也在开发中。通过转换,可以控制一个页面如何以动画效果切换到另一个页面(从左到右、从右到左、交叉淡入淡出、卷曲等)。segue 是一种声明方式,比如可声明“当此按钮操作发生时,执行此路由”。 通过它,无需编写 GoToAsync 导航代码,并能在 XAML 中更清晰地表达内容是如何相互关联的。通过结合使用转换和 Material Shell,我们可以提供一些额外动画效果,如便于图像图标等元素可以从一个页面无缝转换到另一个页面的主图动画效果。

立即开始探索

目前,Xamarin.Forms Shell 在 Xamarin.Forms 4.0 预览版中提供,其中包括令人惊叹的新功能,如 CollectionView、CarouselView 和全新 Material Visual。使用 Material Visual,从一致、通用的 UI 样式点(而不是平台专用空白点)启动 Xamarin.Forms 应用不再是难事。通过切换预发行版选项,使用 Visual Studio NuGet 包管理器更新到版本 4.0 预览版 1。

为了简化操作,我们创建了已更新的项目模板包,这些模板在 Shell 上统一,且默认提供 4.0 预览版。从 aka.ms/xf-shell-templates 下载并安装模板。完成后,便能在新建 Xamarin.Forms 项目时使用新的 Shell 强力驱动模板。

随着 Visual Studio 2019 预发行版本的不断演变,Xamarin.Forms 4.0 和 Shell 也将不断演变。我们需要聆听你的反馈。请通过访问 aka.ms/xf-4-feedback,告诉我们你的想法和体验。


David Ortinau 是 Microsoft 移动开发人员工具高级项目经理,主要负责 Xamarin.Forms 领域。自 2002 年以来,Ortinau 一直是 .NET 开发人员,他精通各种编程语言,为各种行业开发了 Web、环境和移动体验。在科技创业公司取得了几次成功并运营了自己的软件公司之后,Ortinau 加入 Microsoft 来追寻自己的理想:开发有助于开发人员打造更优质应用体验的工具。不工作或不陪家人时,他就在林中快跑。**

衷心感谢以下 Microsoft 技术专家对本文的审阅:David Britch、Jason Smith


在 MSDN 杂志论坛讨论这篇文章