2018 年 1 月

第 33 卷,第 1 期

通用 Windows 平台 - 使用 UWP 创建业务线应用

作者 Bruno Sonnino

有一天,你的老板命你完成一项新任务,即必须要新建 Windows 业务线 (LOB) 应用。Web 并不可行,必须选择最佳平台,以确保应用至少 10 年都可用。

我们似乎面临非常艰难的选择。可以选择 Windows 窗体或 Windows Presentation Foundation (WPF)。这两种是包含许多组件的成熟技术。作为 .NET 开发人员,应该已拥有丰富的知识和经验,知道如何使用这两种技术。它们在未来 10 年内还会继续存在吗?我认为会存在,因为没有任何迹象表明 Microsoft 会在不久的将来停用这两种技术中的任何一种。不过,除了有点陈旧(Windows 窗体于 2001 年推出,WPF 于 2006 年推出)以外,这些技术还存在其他一些问题。Windows 窗体不使用最新图形卡,仍一直在使用陈旧单调且不变的样式,除非使用附加组件或采用一些“魔术技巧”。尽管 WPF 仍一直在更新,但它尚不支持最新的 OS 和 SDK 改进。此外,这两种技术都不能部署到 Windows 应用商店,也都无法从中受益。部署和安装/卸载、全球发现和分发等都绝非易事。虽然可以使用 Centennial 打包应用,但这样做并不贴合实际。

继续深入探究一下,便会发现可以使用通用 Windows 平台 (UWP) 开发 Windows 10 应用,并且它没有 Windows 窗体和 WPF 的缺点。我们现正积极致力于改善 UWP,用户无需进行更改,即可在各种设备(从小型 Raspberry Pi 到大型 Surface Hub)上使用它。不过,每个人都认为 UWP 是用于开发定目标到 Windows 平板电脑和手机的小型应用,无法使用 UWP 开发 LOB 应用。除此之外,学习 UWP 和使用它开发应用也很困难,因为要掌握所有这些新模式、XAML 语言,而且还存在限制用途的沙盒等。

所有这些都与现实相去甚远。尽管 UWP 过去有一些限制,并且学习和使用起来都很困难,但现在情况不再是这样了。新功能开发已有了积极进展。通过新推出的 Fall Creators Update (FCU) 及其 SDK,甚至可以使用 .NET Standard 2.0 API,并直接访问 SQL Server。同时,新工具和组件提供了最顺畅的新应用开发体验。Windows Template Studio (WTS) 是 Visual Studio 扩展,可方便用户快速开始创建功能齐全的 UWP 应用,并免费使用适用于 UWP 的 Telerik 组件,以获得最佳用户体验。还不错,难道不是吗?现在可以使用新平台开发 UWP LOB 应用了!

在本文中,我将使用 WTS、Telerik 组件和对 SQL Server 的直接访问(使用 .NET Standard 2.0)来开发 UWP LOB 应用,这样大家就可以直接了解所有运作方式。

Windows Template Studio 简介

如前所述,在计算机上安装 FCU 后,需要确保 FCU 及相应的 SDK 使用的是 .NET Standard 2.0(如果不确定是否已安装 FCU,请按 Win+R 并键入 Winver,以查看 Windows 版本。版本应为 1709 或更高版本)。只有在 Visual Studio 2017 更新 4 或更高版本中,UWP 才提供 .NET Standard 2.0 支持。因此,如果尚未更新,必须立即更新。此外,若要使用它,还必须安装 .NET 4.7 运行时。

在基础结构到位后,就可以安装 WTS 了。在 Visual Studio 中,依次转到“工具 | 扩展和更新”,再搜索“Windows Template Studio”。下载后,重启 Visual Studio 以完成安装。也可以访问 aka.ms/wtsinstall,下载安装程序,再安装 WTS。

Windows Template Studio 是新推出的开放源代码扩展(可以从 aka.ms/wts 获取源代码),可方便用户有效快速开始执行 UWP 开发。可以选择项目类型(带汉堡菜单的导航面板、空白或透视和选项卡)、MVVM 模式的模型-视图-视图模型 (MVVM) 框架,以及要在项目中添加的页面和 Windows 10 功能。此项目还将进行多项开发,每日版已提供 PRISM、Visual Basic 模板和 Xamarin 支持(即将在稳定版中提供)。

安装 WTS 后,可以转到 Visual Studio,依次单击“文件 | 新建项目 | Windows 通用”新建项目,并选择“Windows Template Studio”。此时,新窗口将会打开,以供选择项目(见图 1)。

Windows Template Studio 主屏幕
图 1:Windows Template Studio 主屏幕

选择“导航面板”项目和“MVVM 轻型”框架,再选择“下一步”。现在,选择要在应用中添加的页面,如图 2 所示。

页面选择
图 2:页面选择

可以在右侧的“摘要”列中看到,已经选择了一个名为“主页”的空白页面。选择“设置”页面,并命名为“设置”。选择“大纲-细节”页面,并命名为“销售”,再单击“下一步”按钮。在下一个窗口中,可以为应用选择一些功能,如动态磁贴/Toast 或在应用首次运行时显示的“首次使用”对话框。除了在选择“设置”页面时选择的“设置”存储之外,我现在不会选择任何功能。

现在,单击“创建”按钮,新建包含选定功能的应用。可以运行此应用。这是一个带汉堡菜单的完整应用,包含两个页面(空白和大纲-细节)和一个支持更改应用主题的设置页面(通过单击底部的齿轮图标进行访问)。

如果转到“解决方案资源管理器”,将会发现系统为此应用创建了许多文件夹(如“模型”、“视图模型”和“服务”)。甚至还有“字符串”文件夹,其中包含用于字符串本地化的“en-US”子文件夹。若要在应用中添加更多语言,只需在“字符串”文件夹中添加新的子文件夹,将它命名为相应语言的区域设置(如“fr-FR”),复制 Resources.resw 文件,再将它翻译为新语言即可。只需单击几下即可完成这么多的工作,真是令人印象深刻,难道不是吗?但我敢肯定,这还不是你的老板在命你开发应用时所期待的样子。接下来,将对此应用进行自定义!

通过应用访问 SQL Server

FCU 和 Visual Studio 更新 4 中引入了一项很棒的功能,即 UWP 现已开始支持 .NET Standard 2.0。这是一项意义重大的改进,因为可便于 UWP 应用使用之前无法使用的大量 API,包括 SQL Server 客户端访问 API 和 Entity Framework Core API。

为了能够通过应用访问 SQL Server 客户端,请转到应用属性,并选择“内部版本 16299”作为“应用”选项卡中的“最低版本”。然后,右键单击“引用”节点,选择“管理 NuGet 包”,并安装 System.Data.SqlClient。这样一来,就可以访问本地 SQL Server 数据库了。需要注意的一点是,并不是使用命名管道(默认访问方法)进行访问,而是使用 TCP/IP。因此,必须运行“SQL Server 配置”应用,并为服务器实例启用 TCP/IP 连接。

我还将使用适用于 UWP 网格的 Telerik UI。Telerik 现为一款免费的开放源代码产品,可以从 bit.ly/2AFWktT 下载。因此,让 NuGet 包管理器窗口仍处于打开状态,选择并安装 Telerik.UI.for.UniversalWindowsPlatform 包。若要在 WTS 中添加网格页面,那么此包会自动安装。

对于此应用,我将使用 WorldWideImporters 示例数据库,可以从 bit.ly/2fLYuBk 进行下载,并将它还原到 SQL Server 实例中。

现在,必须更改默认数据访问。如果转到“模型”文件夹,将会看到 SampleOrder 类,且开头有如下注释:

// TODO WTS: Remove this class once your pages/features are using your data.
// This is used by the SampleDataService.
// It is the model class we use to display data on pages like Grid, Chart, and Master Detail.

整个项目中有许多注释,指导用户需要执行哪些操作。在此示例中,需要的是与此类非常相似的 Order 类。将类重命名为 Order,并将它更改为图 3 中所示。

图 3:Order 类

public class Order : INotifyPropertyChanged
{
  public long OrderId { get; set; }
  public DateTime OrderDate { get; set; }
  public string Company { get; set; }
  public decimal OrderTotal { get; set; }
  public DateTime? DatePicked { get; set; }
  public bool Delivered => DatePicked != null;
  private IEnumerable<OrderItem> _orderItems;
  public event PropertyChangedEventHandler PropertyChanged;
  public IEnumerable<OrderItem> OrderItems
  {
    get => _orderItems;
    set
    {
      _orderItems = value;
      PropertyChanged?.Invoke(
        this, new PropertyChangedEventArgs("OrderItems"));
    }
  }
  public override string ToString() => $"{Company} {OrderDate:g}  {OrderTotal}";
}

此类实现 INotifyPropertyChanged 接口,因为我希望在订单项有变化时通知 UI,以便能够在显示订单时按需加载订单项。我又定义了一个类 OrderItem,用于存储订单项:

public class OrderItem
{
  public string Description { get; set; }
  public decimal UnitPrice { get; set; }
  public int Quantity { get; set; }
  public decimal TotalPrice => UnitPrice * Quantity;
}

此外,还必须修改图 4**** 中的 SalesViewModel,以反映这些变化。

图 4:SalesViewModel

public class SalesViewModel : ViewModelBase
{
  private Order _selected;
  public Order Selected
  {
    get => _selected;
    set
    {
      Set(ref _selected, value);
    }
  }
  public ObservableCollection<Order> Orders { get; private set; }
  public async Task LoadDataAsync(MasterDetailsViewState viewState)
  {
    var orders = await DataService.GetOrdersAsync();
    if (orders != null)
    {
      Orders = new ObservableCollection<Order>(orders);
      RaisePropertyChanged("Orders");
    }
    if (viewState == MasterDetailsViewState.Both)
    {
      Selected = Orders.FirstOrDefault();
    }
  }
}

如果 Selected 属性发生更改,它便会检查订单项是否已加载;如果没有,它会调用数据服务中的 GetOrder­ItemsAsync 方法来加载它们。

要执行的最后一项代码更改是,在 SampleDataService 类中删除示例数据,并创建 SQL Server 访问,如图 5 所示。我已将此类重命名为 DataService,以反映出它不再是示例。

图 5:用于从数据库中检索订单的代码

public static async Task<IEnumerable<Order>> GetOrdersAsync()
{
  using (SqlConnection conn = new SqlConnection(
    "Database=WideWorldImporters;Server=.;User ID=sa;Password=pass"))
  {
    try
    {
      await conn.OpenAsync();
      SqlCommand cmd = new SqlCommand("select o.OrderId, " +
        "c.CustomerName, o.OrderDate, o.PickingCompletedWhen, " +
        "sum(l.Quantity * l.UnitPrice) as OrderTotal " +
        "from Sales.Orders o " +
        "inner join Sales.Customers c on c.CustomerID = o.CustomerID " +
        "inner join Sales.OrderLines l on o.OrderID = l.OrderID " +
        "group by o.OrderId, c.CustomerName, o.OrderDate,
          o.PickingCompletedWhen " +
        "order by o.OrderDate desc", conn);
      var results = new List<Order>();
      using (SqlDataReader reader = await cmd.ExecuteReaderAsync())
      {
        while(reader.Read())
        {
          var order = new Order
          {
            Company = reader.GetString(1),
            OrderId = reader.GetInt32(0),
            OrderDate = reader.GetDateTime(2),
            OrderTotal = reader.GetDecimal(4),
            DatePicked = !reader.IsDBNull(3) ? reader.GetDateTime(3) :
              (DateTime?)null
          };
          results.Add(order);
        }
        return results;
      }
    }
    catch
    {
      return null;
    }
  }
}
public static async Task<IEnumerable<OrderItem>> GetOrderItemsAsync(
  int orderId)
{
  using (SqlConnection conn = new SqlConnection(
        "Database=WideWorldImporters;Server=.;User ID=sa;Password=pass"))
  {
    try
    {
      await conn.OpenAsync();
      SqlCommand cmd = new SqlCommand(
        "select Description,Quantity,UnitPrice " +
        $"from Sales.OrderLines where OrderID = {orderId}", conn);
      var results = new List<OrderItem>();
      using (SqlDataReader reader = await cmd.ExecuteReaderAsync())
      {
        while (reader.Read())
        {
          var orderItem = new OrderItem
          {
            Description = reader.GetString(0),
            Quantity = reader.GetInt32(1),
            UnitPrice = reader.GetDecimal(2),
          };
          results.Add(orderItem);
        }
        return results;
      }
    }
    catch
    {
      return null;
    }
  }
}

此代码与在任何 .NET 应用中使用的代码相同;没有任何变化。现在,我需要修改 Sales­Page.xaml 中的列表项数据模板,以反映这些变化,如图 6**** 所示。

图 6:列表项数据模板

<DataTemplate x:Key="ItemTemplate" x:DataType="model:Order">
  <Grid Height="64" Padding="0,8">
    <Grid.ColumnDefinitions>
      <ColumnDefinition Width="Auto"/>
      <ColumnDefinition Width="*"/>
    </Grid.ColumnDefinitions>
    <StackPanel Grid.Column="1" Margin="12,0,0,0" VerticalAlignment="Center">
      <TextBlock Text="{x:Bind Company}" Style="{ThemeResource ListTitleStyle}"/>
      <StackPanel Orientation="Horizontal">
        <TextBlock Text="{x:Bind OrderDate.ToShortDateString()}"
                   Margin="0,0,12,0" />
        <TextBlock Text="{x:Bind OrderTotal}" Margin="0,0,12,0" />
      </StackPanel>
    </StackPanel>
  </Grid>
</DataTemplate><DataTemplate x:Key="DetailsTemplate">
  <views:SalesDetailControl MasterMenuItem="{Binding}"/>
</DataTemplate>

我需要将 DataType 更改为引用新 Order 类,并更改 TextBlocks 中呈现的字段。此外,我还必须将 SalesDetailControl.xaml.cs 中的代码隐藏类更改为,在选定订单发生变化时加载它的订单项。这是在 OnMasterMenuItemChanged 方法中完成,此方法会转换为异步方法:

private static async void OnMasterMenuItemChangedAsync(DependencyObject d,
  DependencyPropertyChangedEventArgs e)
{
  var newOrder = e.NewValue as Order;
  if (newOrder != null && newOrder.OrderItems == null)
    newOrder.OrderItems = await
      DataService.GetOrderItemsAsync((int)newOrder.OrderId);
}

接下来,我必须将 SalesDetailControl 更改为指向并显示新字段,如图 7 所示。

图 7:更改 SalesDetailControl

<Grid Name="block" Padding="0,15,0,0">
  <Grid.Resources>
    <Style x:Key="RightAlignField" TargetType="TextBlock">
      <Setter Property="HorizontalAlignment" Value="Right" />
      <Setter Property="VerticalAlignment" Value="Center" />
      <Setter Property="Margin" Value="0,0,12,0" />
    </Style>
  </Grid.Resources>
  <Grid.RowDefinitions>
    <RowDefinition Height="Auto" />
    <RowDefinition Height="Auto" />
    <RowDefinition Height="*" />
  </Grid.RowDefinitions>
  <TextBlock Margin="12,0,0,0"
    Text="{x:Bind MasterMenuItem.Company, Mode=OneWay}"
    Style="{StaticResource SubheaderTextBlockStyle}" />
  <StackPanel Orientation="Horizontal" Grid.Row="1" Margin="12,12,0,12">
    <TextBlock Text="Order date:"
      Style="{StaticResource BodyTextBlockStyle}" Margin="0,0,12,0"/>
    <TextBlock Text="{x:Bind MasterMenuItem.OrderDate.ToShortDateString(),
      Mode=OneWay}"
      Style="{StaticResource BodyTextBlockStyle}" Margin="0,0,12,0"/>
    <TextBlock Text="Order total:" Style="{StaticResource BodyTextBlockStyle}"
      Margin="0,0,12,0"/>
    <TextBlock Text="{x:Bind MasterMenuItem.OrderTotal, Mode=OneWay}"
      Style="{StaticResource BodyTextBlockStyle}" />
  </StackPanel>
  <grid:RadDataGrid ItemsSource="{x:Bind MasterMenuItem.OrderItems,
    Mode=OneWay}" Grid.Row="2"
    UserGroupMode="Disabled" Margin="12,0"
    UserFilterMode="Disabled" BorderBrush="Transparent"
      AutoGenerateColumns="False">
    <grid:RadDataGrid.Columns>
      <grid:DataGridTextColumn PropertyName="Description" />
      <grid:DataGridNumericalColumn
        PropertyName="UnitPrice"  Header="Unit Price"
        CellContentStyle="{StaticResource RightAlignField}"/>
      <grid:DataGridNumericalColumn
        PropertyName="Quantity"  Header="Quantity"
        CellContentStyle="{StaticResource RightAlignField}"/>
      <grid:DataGridNumericalColumn
        PropertyName="TotalPrice"  Header="Total Price"
        CellContentStyle="{StaticResource RightAlignField}"/>
    </grid:RadDataGrid.Columns>
  </grid:RadDataGrid>
</Grid>

我在控件顶部显示销售数据,并使用Telerik RadDataGrid 显示订单项。现在,当我运行应用时,第二页上显示订单,如图 8**** 所示。

显示数据库订单的应用
图 8:显示数据库订单的应用

我还有空白的主页。我将用它来显示所有客户的网格。在 MainPage.xaml 中,将 DataGrid 添加到内容网格:

<grid:RadDataGrid ItemsSource="{Binding Customers}"/>

必须将命名空间添加到 Page 标记,但无需记住语法和正确的命名空间。只需将鼠标光标悬停于 RadDataGrid 之上,键入“Ctrl+.”,便会看到一个框打开,其中指明了要添加的正确命名空间。从添加的代码行中可以看到,我要将 ItemsSource 属性绑定到 Customers。由于页面的 DataContext 是 MainViewModel 实例,因此我必须在 MainViewModel.cs 中创建此属性,如图 9 所示。

图 9:MainViewModel 类

public class MainViewModel : ViewModelBase
{
  public ObservableCollection<Customer> Customers { get; private set; }
  public async Task LoadCustomersAsync()
  {
    var customers = await DataService.GetCustomersAsync();
    if (customers != null)
    {
      Customers = new ObservableCollection<Customer>(customers);
      RaisePropertyChanged("Customers");
    }
  }
  public MainViewModel()
  {
    LoadCustomersAsync();
  }
}

我要在创建 ViewModel 时加载客户。需要注意的一点是,我不会等待加载完成。当数据完全加载时,ViewModel 会指明 Customers 属性已更改,并在视图中加载数据。DataService 中的 GetCustomersAsync 方法与 GetOrdersAsync 非常相似,如图 10**** 所示。

图 10:GetCustomersAsync 方法

public static async Task<IEnumerable<Customer>> GetCustomersAsync()
{
  using (SqlConnection conn = new SqlConnection(
    "Database=WideWorldImporters;Server=.;User ID=sa;Password=pass"))
  {
    try
    {
      await conn.OpenAsync();
      SqlCommand cmd = new SqlCommand("select c.CustomerID,
        c.CustomerName, " +
        "cat.CustomerCategoryName, c.DeliveryAddressLine2, 
          c.DeliveryPostalCode, " +
        "city.CityName, c.PhoneNumber " +
        "from Sales.Customers c " +
        "inner join Sales.CustomerCategories cat on c.CustomerCategoryID =
          cat.CustomerCategoryID " +
        "inner join Application.Cities city on c.DeliveryCityID =
          city.CityID", conn);
      var results = new List<Customer>();
      using (SqlDataReader reader = await cmd.ExecuteReaderAsync())
      {
        while (reader.Read())
        {
          var customer = new Customer
          {
            CustomerId = reader.GetInt32(0),
            Name = reader.GetString(1),
            Category = reader.GetString(2),
            Address = reader.GetString(3),
            PostalCode = reader.GetString(4),
            City = reader.GetString(5),
            Phone = reader.GetString(6)
          };
          results.Add(customer);
        }
        return results;
      }
    }
    catch
    {
      return null;
    }
  }
}

这样一来,就可以运行应用并在主页上显示客户了,如图 11 所示。借助 Telerik 网格,可以免费使用许多功能。网格中内置了分组、排序和筛选功能,无需额外执行任何操作。

内置了分组和筛选功能的客户网格
图 11:内置了分组和筛选功能的客户网格

完成设计

现在,我已生成一个基本 LOB 应用,其中包含客户网格和显示订单的大纲-细节视图,但还可以进行一些调试。这两个页面上横栏中的图标都是相同的,可以进行自定义。这些图标是在 ShellViewModel 中进行设置。如果转到那里,将会看到下面这些注释,指明在哪里可以更改图标和项文本:

// TODO WTS: Change the symbols for each item as appropriate for your app
// More on Segoe UI Symbol icons:
// https://docs.microsoft.com/windows/uwp/style/segoe-ui-symbol-font
// Or to use an IconElement instead of a Symbol see
// https://github.com/Microsoft/WindowsTemplateStudio/blob/master/docs/
projectTypes/navigationpane.md
// Edit String/en-US/Resources.resw: Add a menu item title for each page

如果使用 IconElements,可以使用字体符号图标(就像在实际代码中一样)或其他源中的图像。(可以使用 .png 文件、XAML 路径或其他任何字体字符;有关详细信息,请访问 bit.ly/2zICuB2。) 我将使用 Segoe UI 符号中的两个符号,即“People”和“ShoppingCart”。为此,我必须更改代码中的 NavigationItems:

_primaryItems.Add(new ShellNavigationItem("Shell_Main".GetLocalized(),
  Symbol.People, typeof(MainViewModel).FullName));
_primaryItems.Add(new ShellNavigationItem("Shell_Sales".GetLocalized(),
  (Symbol)0xE7BF, typeof(SalesViewModel).FullName));

对于第一个项,Symbol 枚举中已有符号 Symbol.People,但第二个项没有此类枚举,所以我使用十六进制值,并将它转换为 Symbol 枚举。为了更改页面标题和菜单项描述文字,我将编辑 Resources.resw,并将 Shell_Main 和 Main_Title.Text 更改为 Customers。我还可以更改一些属性,向网格添加一些自定义:

<grid:RadDataGrid ItemsSource="{Binding Customers}" UserColumnReorderMode="Interactive"
                  ColumnResizeHandleDisplayMode="Always"
                  AlternationStep="2" AlternateRowBackground="LightBlue"/>

向应用添加动态磁贴

我还可以添加动态磁贴,从而提升应用性能。为此,我将转到“解决方案资源管理器”,右键单击项目节点并依次选择“Windows Template Studio | 新功能”,再选择“动态磁贴”。单击“下一步”按钮后,将看到受影响的磁贴(包括新磁贴和已更改的磁贴),这样就可以确定是否真的满意所完成的工作。单击“完成”按钮会将更改添加到应用。

系统会完成添加动态磁贴所需的一切操作,用户只需创建磁贴内容。已在 LiveTileService.Samples 中完成了此操作。它是将 SampleUpdate 方法添加到 LiveTileService 的分部类。如图 12 所示,我将把文件重命名为 LiveTileService.LobData,并向它添加两个方法(Update­CustomerCount 和 UpdateOrdersCount),用于在动态磁贴中显示数据库中的客户数或订单数。

图 12:用于更新动态磁贴的类

internal partial class LiveTileService
{
  private const string TileTitle = "LoB App with UWP";
  public void UpdateCustomerCount(int custCount)
  {
    string tileContent =
      $@"There are {(custCount > 0 ? custCount.ToString() : "no")}
        customers in the database";
    UpdateTileData(tileContent, "Customer");
  }
  public void UpdateOrderCount(int orderCount)
  {
    string tileContent =
      $@"There are {(orderCount > 0 ? orderCount.ToString() : "no")}
        orders in the database";
    UpdateTileData(tileContent,"Order");
  }
  private void UpdateTileData(string tileBody, string tileTag)
  {
    TileContent tileContent = new TileContent()
    {
      Visual = new TileVisual()
      {
        TileMedium = new TileBinding()
        {
          Content = new TileBindingContentAdaptive()
          {
            Children =
            {
              new AdaptiveText()
              {
                Text = TileTitle,
                HintWrap = true
              },
              new AdaptiveText()
              {
                Text = tileBody,
                HintStyle = AdaptiveTextStyle.CaptionSubtle,
                HintWrap = true
              }
            }
          }
        },
        TileWide = new TileBinding()
        {
          Content = new TileBindingContentAdaptive()
          {
            Children =
            {
              new AdaptiveText()
              {
                Text = $"{TileTitle}",
                HintStyle = AdaptiveTextStyle.Caption
              },
              new AdaptiveText()
              {
                Text = tileBody,
                HintStyle = AdaptiveTextStyle.CaptionSubtle,
                HintWrap = true
              }
            }
          }
        }
      }
    };
    var notification = new TileNotification(tileContent.GetXml())
    {
      Tag = tileTag
    };
    UpdateTile(notification);
  }
}

UpdateSample 最初是在 ActivationService 的 StartupAsync 方法的初始化过程中调用。我将把它替换为新的 UpdateCustomerCount:

private async Task StartupAsync()
{
  Singleton<LiveTileService>.Instance.UpdateCustomerCount(0);
  ThemeSelectorService.SetRequestedTheme();
  await Task.CompletedTask;
}

此时,我仍未更新客户数。当我在 MainViewModel 中获取客户时,客户数就会进行更新:

public async Task LoadCustomersAsync()
{
  var customers = await DataService.GetCustomersAsync();
  if (customers != null)
  {
    Customers = new ObservableCollection<Customer>(customers);
    RaisePropertyChanged("Customers");
    Singleton<LiveTileService>.Instance.UpdateCustomerCount(Customers.Count);
  }
}

当我在 SalesViewModel 中获取订单时,订单数就会进行更新:

public async Task LoadDataAsync(MasterDetailsViewState viewState)
{
  var orders = await DataService.GetOrdersAsync();
  if (orders != null)
  {
    Orders = new ObservableCollection<Order>(orders);
    RaisePropertyChanged("Orders");
    Singleton<LiveTileService>.Instance.UpdateOrderCount(Orders.Count);
  }
  if (viewState == MasterDetailsViewState.Both)
  {
    Selected = Orders.FirstOrDefault();
  }
}

这样一来,我就生成了如图 13**** 所示的应用。

完成的应用
图 13:完成的应用

此应用可以显示从本地数据库中检索到的客户和订单,同时在动态磁贴中更新客户数和订单数。可以对列出的客户进行分组、排序或筛选,并使用大纲-细节视图显示订单。还不错!

总结

可以看到,UWP 不仅适用于开发小型应用。还可以用它来开发 LOB 应用,同时从多个源中获取数据,包括本地 SQL Server(甚至可以使用 Entity Framework 作为 ORM)。通过 .NET Standard 2.0,无需进行任何更改,即可访问许多 .NET Framework API。WTS 可方便用户快速开始按照最佳做法使用首选工具轻松创建应用,并向应用添加 Windows 功能。可以使用很棒的 UI 控件来改进应用的外观。此外,无需进行任何更改,即可在各种设备上运行应用,包括手机、桌面设备、Surface Hub 和 HoloLens。

在部署方面,可以将应用发送到应用商店,并能实现全球发现、轻松安装和卸载以及自动更新。如果不想通过应用商店部署应用,可以使用 Web 进行部署 (bit.ly/2zH0nZY)。可以看到,若要新建 Windows LOB 应用,当然应考虑 UWP,因为它能够提供开发此类应用及其他应用所需的一切。此外,它还具有一项巨大优势,就是我们正在积极开发 UWP,将在随后几年中推出更多改进。


自 2007 年起,Bruno Sonnino 就一直荣获 Microsoft 最有价值专家 (MVP) 称号。*他是开发者、顾问和作者,著有许多与 Windows 开发相关的书籍和文章。*可以关注他的 Twitter (@bsonnino),也可以阅读他的博客文章 (blogs.msmvps.com/bsonnino)。

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


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