Silverlight 和 WPF

编写 Silverlight 和 WPF 应用程序时请参考 Windows 运行时 XAML

Pete Brown

 

用于编写新的 Windows 应用商店应用程序的 Windows 运行时 (WinRT) XAML 是我们中的许多人喜爱的 XAML 和 C#/Visual Basic 系列的最新成员。它在 2006 年与 Microsoft .NET Framework 3.0 以及“Avalon”(后来称为 Windows Presentation Foundation 或 WPF)一起正式发布。在此之后出现了若干个 WPF 修订版,包括最新的 WPF 4.5,同时我们已具有七个指定的 Silverlight 版本(包括 1.1 和 5.1)、若干 Windows Phone 版本等。您甚至可以在 .NET Micro Framework 设备上找到 XAML 堆栈的一部分。

您可能想知道 XAML 和 .NET Framework 为何有这么多的变体。尽管许多实现集中讨论类似的用法(例如,使用 Silverlight 编写桌面应用程序),但每个平台都是针对不同方案和目标平台进行开发和优化的。例如,Silverlight 在设计上是跨平台的且由 Web 托管。Windows Phone 上的 XAML 设计为面向特定于手机的方案以及硬件,而 Windows 8 上的 WinRT XAML 设计为在裸机(x86/x64 和 ARM)上实现高性能、触摸优先(但不是仅限触摸)的 Windows 应用商店应用程序。

无论如何,这些 XAML 实现的共性远大于差异。正是因为这些相似性,导致差异看起来如此明显。当然,根据我自己的经验以及与其他开发人员交谈,我知道极小的差异可能导致大量的开发难题。然而,我们甚至可以非常详细地讨论兼容性,这一事实说明语言、库和标记之间具有相似性。

在本文中,我的目标是探讨两种重要方案: 与辅助应用程序共享代码以及前瞻性地保护您目前的开发工作。

辅助应用程序:对于希望同时针对 Windows 8 开发辅助 Windows 应用商店应用程序的 WPF 和 Silverlight 应用程序的开发人员而言,这是一种同时代码共享或交叉编译方案。

前瞻性保护:在这种方案中,开发人员现在正在创建新的 WPF 和 Silverlight 应用程序,但当前并不面向 Windows 8。如果组织采用 Windows 8 而开发人员希望准备就绪,则他们希望确保应用程序的相应部分能够更轻松地移植到新的 Windows 用户界面中。

数十年的编程经验告诉我们,代码重用和可移植性从来都不是轻易得到的。但是,通过此处讨论的技术来创建具有良好体系结构的应用程序,您将获得事半功倍的效果。

深思熟虑的体系结构是关键所在

仅在您最初具备良好体系结构的情况下,才能将大型应用程序分解为较小的应用程序。事实上,如果您的应用程序的代码模块之间相互依赖程度较高,类层次结构很繁杂,或者感觉像泥球或一次性代码,则代码重用或移植将极其困难。但是别失望!代码可以重构,编写新代码时可以考虑新的体系结构。

当设计新应用程序时,建议 XAML 开发人员使用以下几种关键方法: 绑定、Model-View-ViewModel (MVVM) 模式和服务类。

绑定:在 XAML 中进行开发时所包容的数据绑定越多,使逻辑与用户界面保持分离就更容易。在理想情况下,您可以为用户界面设置 DataContext,而其他一切都可以通过绑定数据或命令来处理。在实际情况下,极少的应用程序可获得这种分离级别,但与这种分离级别越接近,则您的工作就越轻松。

MVVM 模式:MVVM 模式与数据绑定结合发挥作用。ViewModel 是用户界面将绑定到的内容。Internet 和相关书籍中提供了大量有用的信息(以及工具包,稍后将讨论)供您随意参考,因此这里我不再重复了。

服务类:不要将这种方法与 Web 服务相混淆。它们是在客户端上提供可重用功能的类。在某些情况下,它们可能调用 RESTful 或其他服务。在其他情况下,它们可能与业务逻辑进行交互。在所有情况下,它们将封装潜在可变的代码,并使交换出实现的过程变得更容易。例如,在图 1 中,ViewModel 与服务类交互,以便使用这两个平台服务并解决外部依赖性。

ViewModel 与服务类之间的关系
图 1 ViewModel 与服务类之间的关系

我知道,您肯定正在想:“哎!这只不过是另一个层图。”但您知道这些概念是多么的重要!其目的是尽可能使代码与所在平台之间保持分离,从而符合您的预算和时间限制。通过分析代码(例如,分析对桌面元素(如 Windows Imaging 或 DirectShow)执行 COM 或 p-invoke 调用的代码),您可以在 Windows 应用商店应用程序中更轻松地将该实现替换为 WinRT 相机 API。服务类也是一个封装其他平台差异(如合约实现)的好地方: 从 Windows 应用商店应用程序发送电子邮件将使用合约,但在桌面上,这可能意味着自动执行 Outlook 或挂接到 SMTP 服务器中。

当然,热衷于探讨体系结构而从不实际交付,这一点很容易。良好的体系结构应使开发变得更轻松,而不是更困难。如果您发现您的团队纠缠于特定体系结构模式的细枝末节,您可能在浪费时间。而应了解模式及其将带来的结果,然后做出明智、睿智的折衷决策。在大多数情况下,对于良好的体系结构而言,实现 85% 比根本不实现要好。同样,实现最后 15% 所需的成本通常不值。

一旦您分离出了依赖于平台的代码,就可以重用相当多的其他重要代码。

掌握新的 Windows 用户界面设计美学

当开发人员首次考虑构建现有 Windows Presentation Foundation (WPF) 和 Silverlight 应用程序的 Windows 应用商店版本时,他们会立即遇到必须重新考虑 UX 和直观设计的障碍。对于大多数开发人员而言,重新设计整个应用程序的前景并不那么吸引人。如果您认为 Windows 应用商店应用程序面向将来,则现在花点精力在其中加入新 Windows 用户界面设计美学和指南,将来会得到丰厚的回报。

新的 Windows 用户界面设计提供了一个框架,您可用来帮助指导您选择用户界面设计。若干个 BUILD 2011 视频介绍了 Windows 应用商店应用程序的设计。您可以在 MSDN 第 9 频道 bit.ly/oB56Vf 上找到这些视频。此外,此处列出了在桌面应用程序中加入新 Windows 用户界面设计美学时应考虑的事项:

  • 真正数字化。 通常而言,应避免将用户界面设计为对物理对象的低劣模拟(仿真设计)。您不仅应避免重新创建物理对象,还应避免使用派生技术,如光亮的按钮、3D 阴影、逼真阴影和玻璃背景。
  • 具有清晰的类型层次结构。 不要使用大量的不同字体,对于您确实使用的字体,坚持使用一些关键且易于识别的大小。标题应能够很容易地与字段标签和帮助文本相区分。
  • 应用程序品牌塑造。 根据需要使用颜色、徽标、类型等等。这有助于让 Web 开发人员和设计人员加入进来,因为他们通常对塑造品牌具有更丰富的实际经验。
  • 使用基于页面的导航方法。 Silverlight 开发人员将发现这种方法很自然(导航框架几乎完全相同),但 WPF 开发人员可能需要做一些工作,才能从传统的多窗口方法转为对话框桌面方法。
  • 以任务为中心。 使用户界面保持以任务为中心,不要试图以一种格式填塞全部内容。很遗憾,用户通常认为他们在单个页面上需要每个功能,但随着时间推移将变得难以使用、难以维护且难以学习。在某些情况下,可考虑将大型应用程序分解为较小的、以任务为中心的应用程序。
  • 避免不必要的装饰。 使用户界面保持简单。您要使用户的眼球放在他需要集中精力完成的工作上,而不是集中在菜单、导航元素、窗口边界或其他镶边上。

考虑这些概念时,应采用与较旧的 Windows 用户界面设计指南很类似的方式。如果您希望您的应用程序让用户感到很熟悉且适合新的 Windows 用户界面,请遵循这些指南。当然,这有更多的事要做,因此我建议您观看 BUILD 视频,并学习其他开发人员和设计人员已编写的应用程序示例。

一种开始使用用户界面样式的方法是将 Windows 应用商店应用程序样式资源复制到您自己的桌面应用程序中。您可以在 Windows Kits 文件夹中找到许多样式资源。在我的计算机上,该文件夹位于 C:\Program Files (x86)\Windows Kits\8.0\Include\winrt\xaml\design。

某些样式和资源可按原样使用。而某些样式和资源由于更新的控件(如 ListView 和 GridView)而无法使用,另外一些则可能需要进行很大的调整才能使用。但是,这是一种使应用程序获得时尚外观和在桌面样式与 Windows 应用商店样式之间轻松进行过渡的良好方法。甚至当您无法直接使用样式或模板时,您也可以从中学会如何开始制作自己的样式。

在将用户界面移到 Windows 运行时后,您将需要完成一些工作,但如果您现在就在应用程序中加入这些概念,则对于确保在新的 Windows 应用商店用户界面与桌面应用程序之间过渡,您就迈了一大步,而且桌面应用程序不会让您的用户或开发人员感到唐突。

我致力于减少工作量。一次搞定应用程序的样式,使得过渡变得更为轻松,并意外收获时尚的外观。

重用代码和组件

新的 Windows 应用商店应用程序中大量使用了 Windows 运行时。它不仅新颖、快速,还具有很多很酷的功能。但是,有时我们忘记了 Windows 运行时并不是您必须使用的全部功能;除 Windows 运行时之外,使用 XAML 构建的 Windows 应用商店应用程序还使用大量的 .NET Framework 4.5。事实上,.NET Framework 4.5 具有重大更新,包括使其可与 Windows 运行时并行使用的许多更新。但它仍然是 .NET — 共享源代码甚至已编译的库,还有许多其他有用的 .NET 实现。

通过与 WPF 应用程序共享代码,您现在可做的最好的事情是编写可使用 .NET Framework 4.5 的 WPF 代码。Windows 应用商店应用程序除使用 Windows 运行时之外,还使用 .NET 4.5 的一个安全子集。当 .NET Framework 提供了与 Windows 运行时相同的功能时(XAML 就是一个很好的示例),用于 Windows 应用商店应用程序的 .NET 配置文件将使用 WinRT 版本。如果您坚持尽量使用此子集,则您的代码将变得特别易于移植。当您使用服务和异步代码时,您会发现这点尤其有用。

当与 Silverlight 共享时,您将受到一定程度的限制,因为 Silverlight 不使用 .NET Framework 4.5,或者大体上不使用任务并行库。然而,借助于 Silverlight 5 的异步目标包,编写 Silverlight 应用程序的 Visual Studio 2012 用户可以访问某些 Task<T> 和异步/等待功能。但添加服务引用代码不会生成异步客户端代码。如果这对您很重要,则您可以将生成的代理代码移植到 Silverlight。一种更好的方法是使用我在此提出的体系结构,可确保将 Web 服务交互封装在本地服务类中。

那完全相同的代码又如何呢?模型类(在很多情况下为 ViewModel 类)归为此类别。在此,您有两种选择: 可移植的类库 (PCL) 项目和链接项目。

PCL 项目:PCL 是一种在不同 .NET 目标之间共享已编译的 DLL 的好方法。借助于 PCL,您可以在 .NET、用于 Windows 应用商店应用程序的 .NET、Silverlight、Windows Phone 等之间共享程序集。若要使用 PCL,您需要将程序集的引用限制为兼容性列表上的那些引用。在 bit.ly/z2r3eM 中深入探讨 PCL。

具有共享源代码和条件编译的链接项目:这是一个 Visual Studio 功能,使您能够具有多个项目,每个项目针对不同平台,但具有单个共享的源代码副本。以前,我主要使用这种方法在服务器上的 ASP.NET 与客户端上的 Silverlight 之间共享代码。您可以在我的博客 bit.ly/RtLhe7 上找到示例。

尽管我传统上对大多数代码重用方案使用链接项目,但 PCL 在新的版本中确实得到了更多的应用。如果您面向的是 WPF 和 WinRT XAML,PCL 可让您使用用于 Windows 应用商店应用程序的 .NET 4.5 配置文件中的几乎所有内容。如果有疑问,请开始使用 PCL。

关于链接项目方法我中意的一件事是:我可以通过局部类和条件编译提供特定于平台的附加功能。条件编译符号在项目属性页上的“生成”选项卡上设置,如图 2 中所示。

显示编译符号的“项目属性”页面
图 2 显示编译符号的“项目属性”页面

默认情况下,将为您定义一系列条件编译符号,如图 3 中所示。

图 3 条件编译符号

平台 编译符号
Windows 运行时和 .NET Framework NETFX_CORE
WPF/.NET Framework 4.5 Desktop (无)
Silverlight SILVERLIGHT

请注意,完整的 .NET Framework 不定义任何编译符号 — 它被视为默认平台。如果这让您感到不便,您可以添加自己的编译符号;只需确保您对解决方案中的所有 .NET Framework 项目都这样做。

条件编译指的也是如何在代码中根据目标平台引入不同的命名空间:

#if NETFX_CORE
using Windows.UI.Xaml;
#else
using System.Windows.Xaml;
#endif

在任何重要应用程序中,您都可能发现您自己同时使用 PCL 和链接项目来共享代码。 但共享 XAML 则更复杂。

共享 XAML 用户界面和资源

出于多种原因,共享 XAML 比共享代码更困难。 共享 XAML 的最重要步骤是针对外形因素和目标平台定制您的用户界面。 (有关此处的一些指针,请参阅“包含新 Windows 用户界面设计美学”。) 在大多数情况下,您的开发时间更好地花在确保代码可重用方面,因为您将看到事半功倍的效果。 然而,跨项目共享 XAML 既是一种需求,也是一种趋势。

XAML 不具有条件编译概念。 因此,与在代码中相比,命名空间差异更难以控制。 除了命名空间自身之外,您将其导入 XAML 中的方法在 WinRT XAML 与其他版本之间也发生了变化。

请看以下 .NET Framework XAML:

xmlns:localControls="clr-namespace:WpfApp.Controls"

以及以下 WinRT XAML:

xmlns:localControls="using:WindowsApp.Controls"

当导入命名空间时,WinRT XAML 使用 XAML 中的“using”语句,而非“clr-namespace”语句。 产品团队为何这么做呢? XAML 中导入的命名空间可能来自非 CLR 代码,例如 C++。 现在不仅在 C++ 中支持 XAML,而且您可以在 C++ 中编写扩展程序集,并在 .NET 代码中使用它们。 这意味着术语“clr-namespace”不再准确。

一种处理差异的方法是以动态方式加载 XAML。 回到 Silverlight 1.1 alpha 版时代,这是用于生成用户控件的方法: 在运行时动态加载并处理 XAML。 自那时起,应用程序开发人员使用此方法来获得跨所有平台的灵活性。

当您以动态方式加载 XAML 时,您处理的是字符串。 这意味着,您可以随意替换文本或获取文本子集,然后再将其加载到可视树中。 例如,假设您在 WPF 或 Silverlight 项目中有一个空的用户控件定义。 此用户控件只是一个用于定义命名空间和类的 shell。 接着,您在 WinRT XAML 项目中有一个对等的 shell。

.NET XAML 如下所示:

<UserControl x:Class="WpfApp.Controls.AddressControl"
  xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml">
  <Grid x:Name="LayoutRoot">
  </Grid>
</UserControl>

WinRT XAML 如下所示:

<UserControl x:Class="WindowsApp.AddressControl"
  xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml">
  <Grid x:Name="LayoutRoot">
  </Grid>
</UserControl>

这两个 XAML 用户控件都是传统的模板输出。 我所做的唯一更改是给根 Grid 元素命名,并删除一些未使用的命名空间,以使代码保持较短。 可以使用代码共享技术共享控件的代码隐藏,尽管我建议尽可能将代码分区到单独的共享类。

然后,要在运行时加载 XAML,您可以使用与图 4 中所示类似的代码。

图 4 以动态方式加载 XAML 的代码

public partial class AddressControl : UserControl
{
  public AddressControl()
  {
    InitializeComponent();
    LoadXaml();
  }
  private void LoadXaml()
  {
    string xaml =
    "<Grid xmlns=\"https://schemas.microsoft.com/winfx/2006/xaml/presentation\" " +
      "xmlns:x=\"https://schemas.microsoft.com/winfx/2006/xaml\">" +
      "<Grid.RowDefinitions>" +
        "<RowDefinition Height=\"Auto\" />" +
        "<RowDefinition Height=\"30\" />" +
      "</Grid.RowDefinitions>" +
      "<TextBlock Text=\"Address\" Grid.Row=\"0\" FontSize=\"10\" />" +
      "<TextBox x:Name=\"t1\" Grid.Row=\"1\" FontSize=\"15\" />" +
    "</Grid>";
    var reader = new StringReader(xaml);
    var xmlReader = XmlReader.Create(reader);           
    LayoutRoot.Children.Add((UIElement)XamlReader.Load(xmlReader));
  }
}

由于支持直接字符串加载,因此在 WinRT XAML 中加载代码甚至更轻松:

string xaml = "..." LayoutRoot.Children.Add((UIElement)XamlReader.Load(xaml));

为简单起见,我在此处从硬编码字符串中加载 XAML 并使用已可移植的 XAML,而不要求执行任何字符串操作。 对于前者,如果您需要设计器支持,则从 XAML 资源文件中加载代码文本。 然后,加入所有常用类和 xlmns 定义以支持设计器,但在 XAML 加载步骤中去掉它们。

无论 XAML 以什么方式进入可视树,如果您要在以动态方式加载的 XAML 中绑定事件,则也可以从代码中实现:

LayoutRoot.Children.Add((UIElement)XamlReader.Load(xaml));
var t1 = FindName("t1") as TextBox;
if (t1 != null)
{
  t1.TextChanged += t1_TextChanged;
}

就像您可以加载 XAML 用户界面一样,您也可以在运行时加载资源。 您可以使用此处介绍的相同方法从头开始创建它们,并从 XAML 源中加载它们:

private void LoadResources()
{
  string xaml =
    "<Style xmlns=\"https://schemas.microsoft.com/winfx/2006/xaml/presentation\" " +
      "TargetType=\"TextBox\">" +
      "<Setter Property=\"Margin\" Value=\"5\" />" +
    "</Style>";
  // Implicit styles use type for key
  var key = typeof(TextBox);
  App.Current.Resources.Add(key, XamlReader.Load(xaml));
}

当以动态方式加载资源时,确保先加载,然后才能引用它们。 采用隐式样式时,当创建资源时,它们不会应用于可视树中已有的任何项。

明确地说,这种方法总体是有点麻烦,并非对于每种情况都很顺利。 (在某些时候,字符串操作就可以物超所值了。) 但如果您希望最大程度地实现跨平台重用,那这是一种可能的实现方式。 就我个人而言,我仅对具有以下特点的关键用户界面组件使用这种方式:组件会发生大量变化,或者组件太复杂或数量过多而无法按照常规方式复制和粘贴。

当然,您还可以编写自定义生成操作或其他 Visual Studio 扩展,以便在保存、签出、生成文件时或针对某一其他步骤自动执行处理过程。

最终的方法是完全避免使用 XAML,而通过代码创建您整个的用户界面。 通常,对于大多数应用程序,我不建议采用这种方法 — 否则将有大量的附加工作量,且将完全得不到设计器支持。 但是,数据驱动的用户界面确实特别适合采用这种方法。 如果您可以在 XAML 中创建它,则就可以在代码中创建它,如图 5 中所示的 WinRT XAML。

图 5 使用 WinRT XAML 创建用户界面

private void CreateControls()
{
  Grid g = new Grid();
  RowDefinition r1 = new RowDefinition();
  RowDefinition r2 = new RowDefinition();
  r1.Height = new GridLength(1, GridUnitType.Auto);
  r2.Height = new GridLength(30.0);
  g.RowDefinitions.Add(r1);
  g.RowDefinitions.Add(r2);
  TextBlock t = new TextBlock();
  t.Text = "Address";
  Grid.SetRow(t, 0);
  TextBox tb1 = new TextBox();
  Grid.SetRow(tb1, 1);
  g.Children.Add(t);
  g.Children.Add(tb1);
  LayoutRoot.Children.Add(g);
}

此代码(字体设置除外,旨在保持列表较短)等同于以动态方式加载的 XAML,后者将 TextBlock 和 TextBox 添加到网格中包含的 LayoutRoot Grid 中。

考察开源工具包

许多最好的 XAML/C# 工具包是开放源代码。 在有效的开源项目中,您可能会看到感兴趣的人挑选和添加某些类型的功能,这些功能使得在 Silverlight、WPF 和 Windows 运行时之间移动变为可能。

例如,大量的 MVVM 工具包可跨不同 XAML 特性在很大程度上进行移植。 通过使用其中一个工具包,可以在您分享或移植源代码时,帮助减少需要对源代码进行更改的数量。 同样,您可以发现可跨平台使用的控件、本地数据和其他工具包。

开源开发人员始终会面对代码针对新版本平台无法正常工作的风险,但由于您能够访问源代码,因此您可以操控代码,使之完成应执行的任务。 只需在完成后提供您的代码即可。

确定什么是 Windows 应用商店用户界面应用程序

当您需要创建新的 Windows 应用商店应用程序时,请记住,并非所有桌面应用程序的所有环节都直接转换为 Windows 应用商店应用程序。 例如,一个具有 300 个窗体的大型应用程序可针对不同用户执行完整一套不同的功能,这个应用程序并不一定适合采用新的 Windows 用户界面。 而 Windows 应用商店应用程序应以任务为中心,且定制为适合特定的用途。

考察一个保险应用程序。 您可能有一个由公司内大量用户共享的应用程序。 它处理监督功能,以管理用户的安全性。 它允许通过电话以及现场通过手提电脑进行玻璃损坏索赔。 它具有内置的创建新政策的功能以及其他功能。

如果您将此应用程序分解为若干个以任务为中心(或以用户为中心)的较小应用程序,则它将更适合采用新的 Windows 用户界面。 例如,您可能需要一个单独的现场调整应用程序,它专用于记录现场事故信息。 因为它专注度很高,所以它包含对于拍照和录制视频的支持,以及针对信号断续或较弱地区的本地缓存数据功能。 此应用程序可能与企业中的其他应用程序共享大量代码,但它的关注点使之更易于处理它所面向的情景。

总结

创建 WinRT XAML、Silverlight 和 WPF 的目的各不相同,但它们的相似性大于差异。 在它们之间共享代码很轻松,共享 XAML 是可能的。 您可以遵循大量的其他方法来针对所有这三个平台,并从桌面转向新的 Windows 应用商店用户界面。 我希望在 Twitter 上和在我的博客上(网址为 10rem.net)继续讨论此内容。 如果您用过其他技术来处理使代码面向多个平台或将代码移植到 Windows 运行时,我将非常乐于听到您的意见。

Pete Brown 是 Microsoft 的 Windows 8 XAML 和小工具发烧友。 他还是“运行中的 Silverlight 5”(Manning Publications,2012)和“运行中的 Windows 8 XAML”(Manning Publications,2012)的作者。 他的博客和网站为 10rem.net,您可以通过 Twitter(位于 twitter.com/pete_brown)关注他。

衷心感谢以下技术专家对本文的审阅: Tim Heuer