编译 WPF 应用程序

Windows Presentation Foundation (WPF) 应用程序可以生成为 .NET Framework 可执行文件 (.exe)、库 (.dll) 或这两类程序集的结合。 本主题将介绍如何生成 WPF 应用程序,并对生成过程中的各个关键步骤进行说明。

生成 WPF 应用程序

WPF 应用程序可通过以下方式编译:

WPF 生成管道

生成 WPF 项目时,会组合调用特定于语言的目标和特定于 WPF 的目标。 执行这些目标的进程被称为生成管道,相关的关键步骤已显示在下图中。

WPF build process

预生成初始化

在生成之前,MSBuild 会先确定重要工具和库的相应位置,其中包括:

  • .NET Framework。

  • Windows SDK 目录。

  • WPF 引用程序集的位置。

  • 程序集搜索路径的属性。

MSBuild 首先会在引用程序集目录 (%ProgramFiles%\Reference Assemblies\Microsoft\Framework\v3.0\) 中搜索程序集。 在执行这个步骤时,生成进程还会初始化各种属性和项组,并执行所有必要的清理工作。

解析引用

生成进程会查找并绑定生成应用程序项目所需的程序集。 这个逻辑包含在 ResolveAssemblyReference 任务中。 在项目文件中声明为 Reference 的所有程序集会连同有关搜索路径的信息以及系统上已安装的程序集的元数据一并提供给任务。 该任务会查找程序集,并使用已安装的程序集的元数据来筛掉那些无需显示在输出清单中的核心 WPF 程序集。 这么做可以避免 ClickOnce 清单中出现冗余信息。 例如,由于 PresentationFramework.dll 可以被视为代表基于 WPF 以及为 WPF 构建的应用程序,并且由于所有 WPF 程序集在每台安装了 .NET Framework 的计算机上都位于相同位置,因此无需在清单中包含 .NET Framework 引用程序集上的所有信息。

标记编译 - 第 1 次传递

在此步骤中,会分析并编译 XAML 文件,这样运行时便无需再花时间来分析 XAML 并验证属性值。 已编译的 XAML 文件是预标记化的,因此,在运行时,它的加载速度应远快于 XAML 文件的加载速度。

在此步骤中,对于属于 Page 生成项的每个 XAML 文件,会发生以下活动:

  1. 由标记编译器对 XAML 文件进行分析。

  2. 会为该 XAML 创建已编译的表示形式并将其复制到 obj\Release 文件夹。

  3. 创建 CodeDOM 型新分部类并将其复制到 obj\Release 文件夹。

另外,还会针对每个 XAML 文件生成特定于语言的代码文件。 例如,对于 Visual Basic 项目中的 Page1.xaml 页面,会生成 Page1.g.vb;对于 C# 项目中的 Page1.xaml 页面,会生成 Page1.g.cs。 文件名中的“.g”表明文件包含的是生成的代码,这些代码针对标记文件的顶级元素(如 PageWindow)进行了分部类声明。 在 C# 中,该类是使用 partial 修饰符来声明的(在 Visual Basic 中则使用 Extends),以表明该类在其他位置(通常是在代码隐藏文件 Page1.xaml.cs 中)还存在另一声明。

分部类扩展自相应的基类(例如页面的基类为 Page),并实现 System.Windows.Markup.IComponentConnector 接口。 IComponentConnector 接口拥有用于初始化组件以及在其内容中连接元素上的名称和事件的方法。 因此,生成的代码文件中都包含如下所示的方法实现:

public void InitializeComponent() {
    if (_contentLoaded) {
        return;
    }
    _contentLoaded = true;
    System.Uri resourceLocater =
        new System.Uri(
            "window1.xaml",
            System.UriKind.RelativeOrAbsolute);
    System.Windows.Application.LoadComponent(this, resourceLocater);
}
Public Sub InitializeComponent() _

    If _contentLoaded Then
        Return
    End If

    _contentLoaded = True
    Dim resourceLocater As System.Uri = _
        New System.Uri("mainwindow.xaml", System.UriKind.Relative)

    System.Windows.Application.LoadComponent(Me, resourceLocater)

End Sub

默认情况下,标记编译会在与 MSBuild 引擎所在的同一 AppDomain 中运行。 这可以显著提高性能。 此行为可通过 AlwaysCompileMarkupFilesInSeparateDomain 属性来切换。 这能带来一个好处,即:通过卸载单独的 AppDomain 即可卸载所有的引用程序集。

标记编译 - 第 2 次传递

并非所有的 XAML 页面都会在标记编译的第 1 次传递过程中被编译。 包含本地定义的类型引用(引用同一项目中其他位置的代码中所定义的类型)的 XAML 文件就不会在此期间编译。 这是因为这些本地定义的类型仅存在于源中,并且尚未编译。 分析器会采用试探法来确定文件是否已编译,而这一操作会涉及在标记文件中查找 x:Name 之类的项。 如果找到此类实例,标记文件的编译将会推迟,直至代码文件完成编译;在代码文件完成编译后,标记文件会在第二次标记编译传递期间得到处理。

文件分类

生成进程会根据输出文件将被置于哪个应用程序集中,将输出文件放入不同的资源组。 在典型的非本地化应用程序中,所有被标记为 Resource 的数据文件都会置于主程序集(可执行文件或库)中。 如果在项目中设置了 UICulture,则所有已编译的 XAML 文件以及那些专门标记为特定于语言的资源都会置于附属资源程序集中。 此外,所有中性语言资源都会置于主程序集中。 生成进程会在执行这个步骤时确定放置位置。

项目文件中的 ApplicationDefinitionPageResource 生成操作可以使用 Localizable 元数据(可接受的值为 truefalse)来扩充,该元数据会指明文件是特定于语言的文件还是中性语言文件。

内核编译

内核编译步骤涉及代码文件的编译。 这要由特定于语言的目标文件 Microsoft.CSharp.targets 和 Microsoft.VisualBasic.targets 中的逻辑来协调。 如果试探法确定标记编译器只需要进行一次传递,则会生成主程序集。 但是,如果项目中的一个或多个 XAML 文件引用了本地定义的类型,则会生成一个临时的.dll 文件,最终的应用程序集则可能会在标记编译的第二次传递完成后再创建。

清单生成

在生成过程的最后阶段,当所有应用程序集和内容文件都准备就绪后,会为应用程序生成 ClickOnce 清单。

部署清单文件可描述部署模型:当前版本、更新行为、发布服务器标识以及数字签名。 该清单应由负责处理部署的管理员来编写。 文件扩展名为 .xbap(对于 XAML 浏览器应用程序 (XBAP));对于已安装的应用程序,扩展名则为 .application。 前者受 HostInBrowser 项目属性支配,因此清单会将应用程序标识为由浏览器承载。

应用程序清单(一个 .exe.manifest 文件)可描述应用程序集和依赖库,并列出应用程序所需的权限。 该文件应由应用程序开发者编写。 为了启动 ClickOnce 应用程序,用户会打开该应用程序的部署清单文件。

始终会为 XBAP 创建这些清单文件。 对于已安装的应用程序,这些文件不会创建,除非在项目文件中为 GenerateManifests 属性指定值 true

除了已分配给典型 Internet 区域应用程序的权限外,XBAP 还会得到两个额外权限:WebBrowserPermissionMediaPermission。 WPF 生成系统会在应用程序清单中声明这些权限。

增量生成支持

WPF 生成系统可以为增量生成提供支持。 该系统能以非常智能化的方式来检测对标记或代码所做的各种更改,而且只会编译那些受到更改操作影响的项目。 增量生成机制会使用以下文件:

  • $(AssemblyName)_MarkupCompiler.Cache 文件,用于维护当前的编译器状态。

  • $(AssemblyName)_MarkupCompiler.lref 文件,用于缓存引用了本地定义类型的 XAML 文件

下面显示了一组用于控制增量生成的规则:

  • 文件是生成系统检测更改时的最小单位。 因此,对于代码文件,生成系统无法判断类型是否已更改或者是否添加了代码。 对于项目文件也是如此。

  • 增量生成机制必须确定 XAML 页面是定义了一个类,还是使用了其他类。

  • 如果 Reference 条目发生变化,则会重新编译所有页面。

  • 如果代码文件发生变化,则会重新编译引用了本地定义类型的所有页面。

  • 如果 XAML 文件更改:

    • 如果 XAML 在项目中被声明为 Page:如果 XAML 没有引用本地定义的类型,则会重新编译该 XAML 以及包含本地引用的所有 XAML 页面;如果 XAML 具有本地引用,则会重新编译包含本地引用的所有 XAML 页面。

    • 如果 XAML 在项目中被声明为 ApplicationDefinition:重新编译所有 XAML 页面(原因:每个 XAML 都引用了可能已更改的 Application 类型)。

  • 如果项目文件将代码文件声明为应用程序定义,而不是 XAML 文件:

    • 检查项目文件中的 ApplicationClassName 值是不是发生了变化(是不是有新的应用程序类型?)。 如果是,则重新编译整个应用程序。

    • 否则,重新编译包含本地引用的所有 XAML 页面。

  • 如果项目文件发生变化:应用前面的所有规则,并确认哪些内容需要重新编译。 以下属性发生变化会触发全面的重新编译:AssemblyNameIntermediateOutputPathRootNamespaceHostInBrowser

可能会出现以下重新编译情形:

  • 重新编译整个应用程序。

  • 仅重新编译那些引用了本地定义类型的 XAML 文件。

  • 不会重新编译任何内容(如果项目未发生任何变化)。

另请参阅