使用 Windows PowerShell 构建 WPF 应用程序的奥秘
Doug Finke
Windows PowerShell 将任务自动化提升到一个全新的高度。 它非但没有抛弃旧技术,还将这些技术更加发扬光大。 采用 Windows PowerShell(以下简称 PowerShell)并不表示必须重新构建现有的应用程序才能继续使用它们。 实际上,您可以使用 PowerShell 无缝集成并扩展现有的应用程序。
PowerShell 是一种自动化技术,以命令行界面 (CLI)、脚本语言和 API 的形式呈现。
在本文中,我将演练一些重要的 PowerShell 方法,并使用 Windows Presentation Foundation (WPF) GUI 构建一个现值计算器(bit.ly/7oEij1,参见图 1)。
图 1 PowerShell 现值计算器
我将介绍几个重要的 PowerShell 元素:WPF PowerShell Kit (WPK);对象管道;函数;PSObject 及属性;与 Microsoft .NET Framework 的无缝集成;模块等。
PowerShell 构建于 .NET Framework 基础之上,您可以像使用其他 .NET 语言一样顺畅访问该框架。 此外,您还可以访问 Windows 操作系统的其余部分及其组件(如服务、进程和 Windows Management Instrumentation (WMI)),并可以访问远程服务器(这些服务器也具有 PowerShell 版本 2 并启用了 Windows 远程管理)。
所有这一切都通过 PowerShell 所公开的新脚本语言来实现。 您只需使用记事本和几个 PowerShell cmdlet(发音为“command-let”,cmdlet 是在 PowerShell 环境中使用的轻型命令)。 好消息是,这些都是现成可用的: cmdlet 内置于 PowerShell 之中,而记事本随 Windows 附带。 三个重要的 cmdlet:Get-Help 用于显示有关 PowerShell 命令和概念的信息;Get-Command 用于获取有关 cmdlet 和其他 PowerShell 命令元素的基本信息;Get-Member 用于获取对象的“成员”(属性和方法)。
这三个 cmdlet 可以执行所有发现功能,帮助您在这一新的任务自动化平台中进行导航。
让我们进入正题
问题:若要创建一个带有“Hello World”标签的 WPF 应用程序,需要多少行代码?
答案:如果使用 PowerShell 和 WPK,则只需两行代码,如图 2 所示。
图 2 只有两行代码的 PowerShell WPF 应用程序
这是使用两行 PowerShell 编写的一个完整 WPF 应用程序。 第 1 行 (Import-Module WPK) 导入 WPK 包,其中包含一组包装 WPF 的 PowerShell cmdlet。 有趣的是,此代码的运行不需要 Visual Studio、XAML 或 C#。 不过,您需要安装 WPK(请参见下一节)。
Windows 7 和 Windows Server 2008 R2 中提供了现成的 PowerShell 版本 2(对于早期 Windows 系统,可下载使用)。 在发布客户端和服务器操作系统的同时,也发布了 PowerShell 包(作为单独的下载),其中包含 WPK。 它的实现效仿了流行的 Unix 脚本工具 Tcl/Tk。
我先从一组简单的 PowerShell 变量开始,逐步构建出一个交互式 WPF 应用程序。 我将使用 PowerShell 集成脚本环境 (ISE)。
想要继续下去么?
如果您有 Windows 7,那么已基本准备就绪(请记住,PowerShell 是内置的)。
如果您运行的不是 Windows 7,则下载并安装适用于早期操作系统的 PowerShell。 请务必选择正确的操作系统下载。 请参见 Windows Management Framework 核心程序包 (bit.ly/9POYjq)。
无论您的操作系统是何版本,都需要下载并安装 WPK (bit.ly/dFVpfL)。 作为 Windows 7 资源工具包的一部分,它包含其他九个 PowerShell 模块,包括用于 PowerShell ISE 的 ISE 包。 PowerShell 版本 2 中提供了现成的 ISE。 ISE 包也是极好的学习资源,演示了如何在几个级别自定义 ISE。
启动 PowerShell 之后,运行此 cmdlet:Set-ExecutionPolicy RemoteSigned。 PowerShell 默认设置为不运行脚本;这是一种安全机制,PowerShell 用户需要重写此默认设置。 若要运行 Set-ExecutionPolicy,您需要具有管理员权限,并通过右键单击 PowerShell 程序文件并选择“以管理员身份运行”,从而以管理员身份显式运行 PowerShell。
从附带的代码下载中下载并解压缩脚本。 运行应用程序的最简单方法是使用 ISE。 在 Windows 7 上,可以单击“开始”,然后键入“ISE”。 (注意:不能通过双击操作运行 PowerShell 脚本(具有 .ps1 文件扩展名)。 运行示例脚本的最简单方法是启动 ISE,并使用“文件”|“打开”来打开脚本文件。)
PowerShell 101
我将构建一个现值计算器;这是一个简单的公式,如图 3 所示。
图 3 现值计算
PowerShell 中的变量以 $ 开头。 在第 7 行中,我直接使用 .NET Framework,以调用 System.Math 命名空间上的静态方法 Pow。 Pow 方法返回指定数字的指定指数幂。 调用静态 .NET 方法所需的语法是用方括号括起类,后跟两个冒号,然后是方法名:[System.Math]::Pow(2,3)。 如果在 ISE 中运行此代码,请按 F5(或单击“运行”按钮)以在输出窗格中查看结果。 也可以将此代码复制并粘贴到 PowerShell 命令行控制台中;它将运行并输出结果。
这是一个良好的开始,但这段代码的可重用性不高。 我可以继续键入新值并运行此脚本,但是我想从其他脚本调用它。 我将添加 function 关键字并将变量转换为参数,以便可以更改输出并使脚本具有交互性(请参见图 4 中的第 2 行)。
图 4 创建 PowerShell 函数
添加 Function 关键字
我将函数命名为 Get-PresentValue。 在命名函数时最好遵循 PowerShell“动词-名词”约定 — 这是 PowerShell 使用和开发中的基本概念。 它有预定义的谓词,如 Get、Invoke、New 和 Remove(键入 Get-Verb 可查看完整列表)。 试着键入 Get-Command;它会返回在 PowerShell 会话中定义的所有 cmdlet(以及更多内容),这是一个庞大的列表。
创建函数十分简单,只要键入 function Get-PresentValue {} 即可。 我将在此处创建参数,以及默认值和函数体,如图 4 所示。
比较一下图 3 与图 4,可以发现我将变量(名称和值)移到单行中,使用逗号分隔,用圆括号括起,并将其置于函数名称之后,从而使这些变量成为函数的参数。 这些参数现在具有默认值。 我原样保留了现值计算。 PowerShell 中的一个重要原则是“减少键入”。在图 4 的第 3 行中,我本可以使用 return 语句。 但在这里省略该语句可获得相同的行为。不过,有时您确实需要使用 return 语句来中断逻辑流。
在此示例中,我演示了调用 Get-PresentValue 函数的几种方法。 首先,不带参数,所有参数都是默认的。 参数可以按位置提供(请参见图 4 中的第 8 行),也可以进行命名(请参见第 9 行)。 建议您研究一下 PowerShell 参数;PowerShell 支持强大的参数绑定引擎。
接下来:更改 Get-PresentValue 函数以返回 .NET 对象而不是简单文本。
PowerShell 基于 .NET
PowerShell 中的一项重要创新就是以完全类型化对象形式传递数据的能力。 图 5 介绍了创建 PowerShell 对象、设置属性、返回该对象并利用 PowerShell 引擎将其输出的概念。
图 5 从 PowerShell 函数返回完全类型化对象
在第 6 行中,我使用了 cmdlet New-Object,它创建 .NET Framework 对象的实例。 我告知它要创建的对象类型 (PSObject),该类型可为 PowerShell 环境中的所有对象提供一致的视图。 同样是在第 6 行中,我使用了 -Property 参数,该参数采用一个哈希表。 PowerShell 中的哈希表简写语法是 @{}。 哈希表中定义的键/值对(在 -Property 参数中传递)会转换为新 PSObject 的属性和值。
最后,在第 15 行,我调用了这个函数,并且可以在输出窗格(ISE 中为蓝色背景)中查看结果。 请注意,PowerShell“知道”如何输出该对象。 我不必进行任何反射来确定要输出的属性或如何输出这些属性 — 这是 PowerShell 一个重要优点。
PowerShell 范围和管道
接下来,我使用 PowerShell 管道并介绍另外两个 PowerShell cmdlet:ForEach-Object 和 Format-Table(请参见图 6)。
图 6 PowerShell 范围和管道
第 13 行是耐人寻味的部分,您可通过它深入体会 PowerShell 的灵活性和可组合性。 此处有三个部分,定义了两个管道。 第一部分显示范围运算符(由两个句点构成),第二部分是 ForEach,最后一部分包含 Format-Table。 我将逐一讨论每个部分。
第一部分:1000..1010 1000..1010 表示从 1,000 到 1,010(包括这两个数字)的整数数组。 现在,PowerShell 开始将这些整数逐一推送到管道中(每当有一个整数可用,便立即推送)。 与基于 Unix/Linux 的 Shell 一样,PowerShell 也实现了管道,通过该管道可以将一个 cmdlet 的输出作为输入输送到另一个 cmdlet。 对于 PowerShell,该管道由 .NET 对象组成。 通过使用对象,便无需分析来自一个命令的任意文本输出以提取数据,因为所有对象都导出一致的接口(请参见 bit.ly/lVJarT)。
第二部分:ForEach { Get-PresentValue $_ }。此部分使用 ForEach(也可使用别名 %),它采用一个脚本块。 可将脚本块视为匿名方法(在其他语言中有时称为 lambda 表达式)。 有关此方面内容的更多信息,请参见 Bruce Payette 撰写的“PowerShell in Action, Second Edition”一书(Manning Publications 2011 年出版)。
$_ 是一个 PowerShell 自动变量,包含管道中的当前对象。 最终结果(包含 10 个整数的数组)会传递给 Get-PresentValue,一次传递一个整数。 因为我们未命名该参数,所以我将其作为位置参数传递给 $Amount,如图 6 中的输出窗格所示。
最后一部分:Format-Table。顾名思义,Format-Table 将输出设置为表的形式。 我使用了 -AutoSize 参数,因为该参数会根据数据宽度调整列的大小。
请注意,我没有管理对集合的遍历,并且 PowerShell“知道”如何输出通过管道推送的对象以及输出有关这些对象的哪些内容。 这样便会减少代码行的编写,也意味着调试的代码行更少。 因为我将 90% 的时间用于调试,而将另外 10% 用于编写 Bug,所以这样可大大加快我的进度。
GUI 出场 — 第 1 回合
该使用 WPK 了。 图 7 中的脚本生成图 8 中的 GUI。 我向 Get-PresentValue 函数的参数添加了类型信息(请参见图 7 中的第 1 行)。 这可方便使用此函数的其他人检测是否传递了错误数据(例如,传递了字符串而不是数字)。
图 7 WPK New-ListView
图 8 查看 GUI
这里保留了图 6 中原始代码的精髓部分,并向 ListView(这是提供用于显示一组数据项的基础结构的 WPF 控件)的 DataContext 添加了对 Get-PresentValue 的调用。 WPK 部分的其余内容与 WPF 数据绑定集成,并设置 ListView 中的视图,以便可以显示数据。
WPK 遵循 PowerShell 的基本原则,即使用“动词-名词”对的命名方式。 因此,如果我要创建 Window、Grid、Canvas 或 ListBox,则只需向其添加“New-”(即New-Window、New-Grid、New-Canvas 或 New-ListBox),这些框架元素已准备就绪,可供使用。
Import-Module
模块是包含可在 PowerShell 中使用的成员(如 cmdlet、脚本、函数、变量以及其他工具和文件)的包。 在导入某个模块之后,便可以在会话中使用该模块的成员。 如前所述,WPK 是 PowerShell 包的一部分,并且 WPK 包含 700 多个 PowerShell 函数,这些函数简化了在 PowerShell 上层实现 WPF GUI 的操作。
图 7 中的第 17 行和第 18 行来自传统的 WPF 背景,可能看上去有些与众不同。 WPK 通过提供两个表面参数来支持数据绑定:DataContext 和 DataBinding。 DataContext 参数采用一个脚本块,因此这里我传递一行 PowerShell 代码,这行代码创建在图 6 第 13 行中使用的 10 个现值对象。 接下来,我在第 18 行中设置要绑定的 ListView 的 ItemsSource 属性。 DataBinding 参数采用一个哈希表(请注意 @{})。 这些键是要绑定到的 GUI 对象的属性的名称。
WPK 有助于减少代码的编写。 New-GridViewColumn 函数采用一个字符串(例如 Amount,这是从 Get-PresentValue 函数发出的对象的属性名称),将其设置为标题,然后自动为您执行数据绑定。
我从一个简单函数 Get-PresentValue 开始,通过添加参数使其可重用,发出一个 .NET PowerShell PSObject 并利用 PowerShell 对象管道和范围运算符生成现值项,最终得到图 8 中所示的输出。 随后,我导入了 WPK,以便将 PowerShell 对象注入 WPF ListView 控件的 DataContext,绑定 ItemsSource 参数并创建 ListView 视图(以注入对象的属性名作为列名)。 最后,我得到了一个简单的 PowerShell/WPK 现值应用程序。 此应用程序很棒,但它是硬编码的。
接下来,我要与此应用程序进行交互,以便能够通过更改金额、利率和其他参数来考察投资情况。
GUI 出场 — 第 2 回合
要使用当前的 PowerShell/WPK 脚本,我需要更改参数、保存文件并重新运行脚本,这样做欠缺灵活性。
我将对它进行一些改造,以便能够从 GUI 调整五个参数,然后在 ListView 中显示新值。
New-Range 函数。首先,我要添加一个名为 New-Range 的函数(请参见图 9)。
图 9 New-Range 函数
PowerShell 提供的范围运算符不允许更改增量。 换言之,我不能指定 (1..10 by 2)。 而通过 New-Range 函数我可以自己指定增量,从而使“New-Range 1 10 2”输出 1 3 5 7 9。
接下来,我通过在 New-Window 函数中包装 New-ListView 来充实 WPK GUI。 单纯使用 New-ListView 时,WPK 也会提供一个包装窗口。 但指定 New-Window 可以提供更多控制(请参见图 10)。
图 10 New-Window 函数
我还将 DataContext 从 ListView 提升到窗口范围,并应用了新函数 New-Range。 现在我要添加五个文本框、五个标签和一个按钮。 这可以让我在保持应用程序运行的同时,调整 Get-PresentValue 的输出。 我将使用 WPK New-Grid、New-Label、New-TextBox 和 New-Button 函数创建所需的控件。 通过使用 New-Grid 创建网格控件,我可以灵活地调整窗口和控件的位置和大小。
为了对 GUI 进行布局,我打算在网格中嵌套网格及控件。 我仍在 DataContext 中使用 New-Range 和 Get-PresentValue 函数,如图 11 的第 2 行中所示。
图 11 New-Grid 函数
此外,New-ListView 仍存在于图 11 的第 30-37 行中。 我在第 30 行中添加了另外两个参数 –Row 和 –Column,这两个参数向 ListView 指示它应位于第 5 行所定义 New-Grid 中的哪一行和哪一列。 第 5 行中定义的 New-Grid 使用 Rows 和 Columns 对网格进行布局。 我需要两列(每列宽度为 75 像素)和三行(每行高度为 35 像素)。 Rows 参数中第二个 75 之后的星号指示它将占用所有可用空间。
现在,我在窗口中放置五对标签和文本框,并通过 Row 和 Column 参数向这些控件指示要定位到哪个网格象限。 此外,我命名了文本框以便以后进行访问,通过 -Text 参数向其提供默认值,并使用 Margin 和 HorizontalAlignment 参数对控件进行修饰。
最后,我使用 New-Button 函数在网格中放置按钮。 请注意,我在 Calculate 前放置了一个下划线。 这样,我便可以通过按 Alt+C 键来访问该按钮。
现在,这个现值计算器几近完成。 我必须将单击事件挂接到一些 PowerShell 代码,读取文本框中的值,将这些值作为参数传递给 Get-PresentValue 并将其绑定到 ListView。
Do-Calculation 函数
我将添加一个 Do-Calculation 函数,该函数只采用一个参数 $window(请参见图 12)。
图 12 Do-Calculation 函数
我通过 New-Button(内容为“_Calculate”的按钮)的 -On-Click 属性调用 Do-Calculation。
Do-Calculation 十分简单。 该函数获取所有文本框中的信息,将这些信息设置为 PowerShell 变量,然后将其作为参数传递给 New-Range 和 Get-PresentValue 函数。 我传递了参数 $window(请参见图 13 中的第 2 行),其中包含对图 10 中所创建的 New-Window 的引用。 使用此参数可以访问该窗口的所有属性以及子控件,具体而言就是文本框。 因为我命名了每个文本框控件,所以可以将 $window 通过管道输送到 Get-ChildControl,从而通过 .Text 属性传递控件名称并检索文本(请参见图 12 中的第 3-7 行)。
图 13 完成的网格
从文本框收集所有详细信息之后,我将 $window.DataContext 设置为通过管道输送到 Get-PresentValue 的 New-Range 的结果,这会生成一个 PowerShell 对象数组,其中包含 PresentValue 计算的结果(请参见图 12 中的第 9-12 行)。
图 13 演示了如何连接 Calculate 按钮的单击事件,以调用 Do-Calculation 函数并传递 $window 变量(请参见第 28-29 行)。 我添加了 -On_Click 参数,该参数采用一个脚本块。 我在其中调用 Do-Calculation 函数,同时传入在使用 New-Window 函数时创建的 $window 变量。 每次单击按钮时,都会重新计算屏幕。 请注意,在图 13 中还更改了第 2 行,也是为了调用 Do-Calculation 函数。
您可以在附带的源代码示例中下载完整的应用程序,以查看完整脚本。
一个简单的交互式 WPF 应用程序
我在此处展示了一个不足 75 行代码的脚本,这个脚本在 Microsoft 自动化平台 PowerShell 的上层构成了一个交互式 WPF 应用程序。 PowerShell 既是一种脚本语言,也是一个命令行控制台。 它通过与 .NET Framework 和 Windows 的深入集成,创造了激动人心的自动化机会。 当然,这也意味着使用者须花时间学习这一新平台。 好消息在于:您可以先采用简单的方式以提高工作效率,随后在您需要时,再深入研究 PowerShell 提供的大量自动化功能(来自 Microsoft 和一般 PowerShell 社区)。
Ad Hoc 开发模型与 Windows PowerShell 起源
Doug Finke 的文章是关于 Ad Hoc 开发模型的一个出色示例。 PowerShell 在许多方面都与其他编程技术不同:它侧重面向任务的高级抽象;它的自适应类型系统可规范化不同类型系统(.NET、Windows Management Instrumentation [WMI]、XML、ADSI、ADO 等)并支持向类型和实例添加成员;它的数据流引擎可消除开发人员必须编写大量阻抗失配代码的需要;它支持 Ad Hoc 开发模型。
Ad Hoc 模型是通过非正式的方法排解问题的一种途径。 在您决定更多地使用该模型时,需将其转换为非正式的脚本,如果您共享它,则需使其更加正式。 作为工具构建者,我们常常为拥有不同技能集的人员构建工具,因此满足所有目标对象的需求和期望便显得十分重要。 这往往意味着需要提供 GUI。
Doug 的文章从一个无名的硬接线脚本开始,该脚本用于针对特定金额、固定利率和时间计算资金现值。 随后,他将这个脚本转换为一个命名函数,该函数具有命名参数和初始值,并返回一个值。 接下来,他返回一个对象,以便其他工具可以操作该对象。 最后,他将其转换为一个简单的 GUI,然后再加丰富。 他只在需要时才进行改进,每个脚本都在前一个脚本的基础上添加内容。 最后,Doug 将他的脚本共享出来,使其他人也可以使用这个工具,还提供了有关如何改进它的建议(如在建议时,他“类型化”所用的参数,以使他的工具在有人传入字符串时不会发生混乱)。 共享可以让每个人获益。 我获得了一个很棒的工具,这需要感谢 Doug。 我检查了他的代码并提供了一些建议,从而些许表达一下我的感谢之情。 我对 PowerShell 有一点了解,不过仍然从社区提出的有关我脚本的各种建议中获益匪浅。 [Snover 是 Windows PowerShell 的发明者,他以及 Bruce Payette 和 James Truher 都是主要设计人员;请参见 bit.ly/696Jor。—Ed。]
Ad Hoc 开发模型源自 PowerShell 旨在成为优秀交互式 shell 和优秀脚本语言的双重目标。 Bruce Payette(该语言的设计人员之一)曾经说过,99% 的 PowerShell 脚本的生命期从命令提示符开始,以回车符结束。 PowerShell 支持广泛的脚本类型,从命令行提示符下的交互式单行命令,到使用 $args 的 Bash 样式函数,再到更加正式的脚本(其中参数会进行命名、类型化并通过验证、数据绑定和帮助特性进行修饰),不一而足。 我们采用此方法的原因源自我们作为 Unix 开发人员的多年工作经验(在此期间我编写了不计其数的 shell 脚本)。 当人们使用我的脚本并请求获得更多功能时,我发现自己已经丢掉这些脚本,转而采用 Tcl 或 Perl 重新编写了。 最后,我往往也会将这些丢弃,采用 C 完全重新编写。 这让我颇受打击:虽然不同的问题需要不同级别的形式和性能,但竟然没有一种工具可涵盖这么广泛的脚本需求,这实在有点不可思议。 如果有这样一种工具,人们就可以致力于成为该工具的专家,而不必费神去掌握各种各样的不同工具了。 我考虑了一段时间,最终决定创建一种实现此目标的工具。 希望您喜欢 PowerShell。
— Jeffrey Snover,著名工程师,也是 Windows Server 首席架构师
Doug Finke 是 Microsoft Windows PowerShell 方面的 MVP,Lab49(一家为金融服务行业构建高级应用程序的公司)的软件开发人员。 在过去 20 年中,他一直是使用多种技术的开发人员和作者。 您可以通过他的博客“Development in a Blink”(地址为 dougfinke.com/blog)关注他。
衷心感谢以下技术专家对本文的审阅:James Brundage、Sal Mangano、Sivabalan Muthukumar、Marco Shaw、Jeffrey Snover 和 Sylvain Whissell