在 Visual Studio 中创建自定义 UI 属性页/表 - 第 2 部分






4.76/5 (7投票s)
从 ProjectSchemaDefinitions Rule XML 生成命令行选项。
引言
这是关于如何将第三方工具集成到 Visual Studio 配置子系统系列文章的第二篇。在我的上一篇文章中,我解释了如何创建显示在 Visual Studio“属性”对话框中的自定义属性表。在本文中,我将解释如何检索设置数据并根据工具要求对其进行格式化。
背景
通过创建包含项目架构定义的 XML 文件,其中包含 Rule 和 Property 元素,将属性表引入配置子系统。表上的每个 UI 控件都将有一个定义它的 <...Property>
元素。因此,如果您的 XML 看起来像这样
<?xml version="1.0" encoding="utf-8"?>
<Rule ...="">
...
<BoolProperty Name="StdCall" DisplayName="__stdcall" ...=""/>
<StringProperty Name="PrecompiledHeaderFile" ...="" />
</Rule>
在您的 Target 中,您将拥有从属性变量检索数据并将其格式化为命令行选项的代码。也许是这样的
<Target Name="Build">
<PropertyGroup>
<options>/One"$(StdCall)" /Two:$(PrecompiledHeaderFile) /build:$(Configuration) ... </options>
<options>$(options)/Three"$(SomeProp)" /Four:$(SomeOtherProp) ... </options>
</PropertyGroup>
<Exec Command="tool.exe $(options)" ... />
</Target>
这是一种直接但费力且不那么灵活的方法。我想向您展示如何使用 XmlPeek 任务在没有任何硬编码的情况下完成此过程。
用户界面
创建 UI 属性表时(有关更多信息,请参阅),您有几个模板可供选择。如果您将属性表的 Rule 定义为使用“tool”模板
<?xml version="1.0" encoding="utf-8"?>
<Rule Name="ConfigUI"
DisplayName="Sample"
PageTemplate="tool"
xmlns="http://schemas.microsoft.com/build/2009/properties">
<Rule.Categories>
<Category Name="General" DisplayName="General" />
...
<Category Name="Command Line" DisplayName="Command Line" Subtype="CommandLine" />
</Rule.Categories>
<StringProperty Name="AdditionalOptions" DisplayName="Additional Options"
Description="Additional Options." Category="Command Line"
F1Keyword="vc.project.AdditionalOptionsPage" />
</Rule>
Visual Studio 将允许您添加一个带有 Subtype
"CommandLine
" 的特殊类别
<Category Name="Command Line" DisplayName="Command Line" Subtype="CommandLine" />
此类别打开一个只读视图,显示从您的配置生成的命令行选项集
这些开关和参数是根据您在单个 <...Property>
元素上设置的属性创建的。我们将简要介绍这些属性
IncludeInCommandLine
此 Boolean
属性(请参阅参考资料)提示命令行生成器是否将此属性包含在命令行中。
SwitchPrefix
SwitchPrefix
属性(请参阅参考资料)包含开关的前言,例如上面示例中的 /
,由引擎生成。此属性可以在 Rule 元素或任何属性元素上设置。
Switch
Switch
属性(请参阅参考资料)保存开关本身的表示形式。在上面的示例中,它们是 SLP
、SLP-V
、SLP-VS
。
ReverseSwitch
ReverseSwitch 属性(请参阅参考资料)仅存在于 <BoolPreperty...> 元素上,并在属性值为 "false"
时保存开关的表示形式。显然,Switch 保存值 "true"
的表示形式。
分隔符
Separator 属性(请参阅参考资料)保存用于分隔开关及其值的标记。
CommandLineValueSeparator
CommandLineValueSeparator 属性(请参阅参考资料)保存用于分隔列表属性中的值的分隔符。因此,当 <StringListProperty ... > 包含 Val1;Val2;Val3
时,如果 CommandLineValueSeparator 指定为逗号,则命令行如下所示:/p"Val1","Val2","Val3"
如果未指定,则命令行如下所示:/p"Val1" /p"Val2" /p"Val3"
。设置 Separator 属性会将其更改为:/p:"Val1","Val2","Val3"
和 /p:"Val1" /p:"Val2" /p:"Val3"
。
示例
<BoolProperty ReverseSwitch="Zc:wchar_t-"
Name="TreatWChar" Switch="Zc:wchar_t" ...>
<StringProperty Name="ProgramDataBaseFileName" Switch="Fd" ... >
<StringListProperty Name="PreprocessorDefinitions" Switch="D " ...>
...
以上属性的组合将生成以下命令行选项
/Zc:wchar_t- /Fd"Debug\vc120.pdb" /D "WIN32" /D "_DEBUG" /D "_CONSOLE" ...
生成命令行
获取属性
这些规则和属性在常规 XML 文件中定义的事实使我们能够在代码中处理它们。我们所要做的就是查询文件并获取所有 IncludeInCommandLine 属性未设置为“false
”的属性。我们可以使用 XmlPeek 任务查询任何 XML 文件。(有关如何调用任务的详细参考,请参阅 MSDN。)
<XmlPeek XmlInputPath="sample.xml"
Namespaces="Namespace Prefix='x'
Uri='http://schemas.microsoft.com/build/2009/properties'"
Query="/x:Rule/node()[not(contains(@IncludeInCommandLine, 'false'))]">
<Output TaskParameter="Result" ItemName="Peeked" />
</XmlPeek>
此查询将返回加载到 Peeked
Item 中的 XML 文件中的所有配置属性
<BoolProperty Name="ExpandAttributedSource" DisplayName="Expand Attributed Source" ... />
...
<EnumProperty Name="AssemblerOutput" DisplayName="Assembler Output" ... />
<EnumValue Name="NoListing" Switch="" DisplayName="No Listing" ... />
<EnumValue Name="AssemblyCode" Switch="FA" DisplayName="Assembly-Only Listing" ... />
<EnumValue Name="AssemblyAndMachineCode" Switch="FAc" ... />
<EnumValue Name="AssemblyAndSourceCode" Switch="FAs" ... />
<EnumValue Name="All" Switch="FAcs" DisplayName="Assembly, Machine Code" ... />
</EnumProperty>
...
<StringProperty Name="sampleSProperty" DisplayName="Simple String" ... />
一旦我们将所有属性加载到 Item 中,我们就可以对它们做一些有用的事情。
处理属性
我们将使用 Transforming 和 Batching 的组合来执行所有相关属性的解析,并根据这些属性创建最终的命令行选项(本文中不介绍 Transforming 和 Batching,这些概念已在其他地方充分介绍)。首先,我们需要解析所有相关数据。
解析
解析是通过遍历每个元素并分析数据来完成的。每个元素都存储为一个简单的 string
,因此我们将使用 Regex.Match 来提取值并将它们存储到每个项的 Metadata 中。代码应如下所示(注意:示例代码已为演示目的进行简化)
<ItemGroup >
<Properties Include="@(Peeked -> '%(Identity)')">
<Type>$([Regex]::Match(%(Identity), (?<=<)(.*?)(?=\s) ))</Type>
<Name>$([Regex]::Match(%(Identity), (?<=Name=")(.*?)(?=") ))</Name>
<Prefix>$([Regex]::Match(%(Identity), (?<=SwitchPrefix=")(.*?)(?=") ))</Prefix>
<Switch>$([Regex]::Match(%(Identity), (?<=Switch=")(.*?)(?=") ))</Switch>
<ReverseSwitch>$([Regex]::Match(%(Identity),(?<=ReverseSwitch=")(.*?)(?=") ))</ReverseSwitch>
<Separator>$([Regex]::Match(%(Identity), (?<=\sSeparator=")(.*?)(?=") ))</Separator>
<Divider>$([Regex]::Match(%(Identity), (?<=ValueSeparator=")(.*?)(?=") ))</Divider>
<Children>$([Regex]::Matches(%(Identity), EnumValue\s(.*?)>) )</Children>
<Subtype>$([Regex]::Match(%(Identity), (?<=Subtype=")(.*?)(?=") ))</Subtype>
</Properties>
</ItemGroup>
在此示例中,我们使用构造 "@(Peeked -> '%(Identity)')"
来告诉 MSBuild 创建一个名为 <Properties>
的 Item,遍历 <Peeked>
中的每个元素,并将数据添加回 <Properties> 而不进行修改。我们还将 Metadata <Type>
、<Name>
、<Prefix>
等添加到 <Properties>
中的每个元素,其中包含从原始属性元素解析的数据。
完成后,我们应该让 <Properties>
填充原始数据,并将解析后的值存储为 Metadata。
生成选项
现在我们可以按类型遍历每个单独的属性,提取数据并形成输出开关,并应用适当的格式。我们将使用 Item <options>
来收集生成的输出。
StringProperty
我们从 StringProperty
元素开始
<options Condition="'%(Properties.Type)' == 'StringProperty'
And '$(%(Properties.Name))'!=''"
Include="%(Properties.Prefix)%(Properties.Switch)%(Properties.Separator)'$(%(Properties.Name))'"
/>
在此批处理中,我们遍历属性集合中的每个 StringProperty
元素并将其转换为
%(Properties.Prefix)%(Properties.Switch)%(Properties.Separator)$(%(Properties.Name))
序列。注意最后一个元素 $(%(Properties.Name))
。它包含变量的引用,其名称保存在
%(Properties.Name)
。换句话说,它类似于 global['name']
。使用上面显示的示例,输出将包含
/Fd"Debug\vc120.pdb"
IntProperty
转换 IntProperty 与 StringProperty 非常相似,只是值周围没有引号。
<options Condition="'%(Properties.Type)' == 'StringProperty'
And '$(%(Properties.Name))'!=''"
Include="%(Properties.Prefix)%(Properties.Switch)%(Properties.Separator)$(%(Properties.Name))"
/>
BoolProperty
转换 BoolProperty
需要两次运行。第一次用于所有“true
”值,第二次用于所有“false
”值。
<options Condition="'$(%(Properties.Name))'=='true'
And '%(Properties.Type)'=='BoolProperty'"
Include="%(Properties.Prefix)%(Properties.Switch)" />
<options Condition="'$(%(Properties.Name))'=='false'
And '%(Properties.Type)'=='BoolProperty'"
Include="%(Properties.Prefix)%(Properties.ReverseSwitch)" />
此时,<options>
列表应包含
/Fd"Debug\vc120.pdb"
/Zc:wchar_t-
StringListProperty
StringListProperty
元素的处理有点棘手。如果设置了 CommandLineValueSeparator,我们所要做的就是用 CommandLineValueSeparator 中包含的符号替换分隔符(注意:示例已简化)
<options Condition="'%(Properties.Type)' == 'StringListProperty' And
'%(Properties.Divider)'!=''"
Include="%(Properties.Prefix)%(Properties.Preamble)" \
$(Replace($(%(Properties.Name)), ';', '%(Properties.Divider)'))"" />
如果 CommandLineValueSeparator,我们需要为变量中包含的每个列表值输出单独的开关
Prefix - Switch - Value1,Value2,Value3
必须变成
Prefix - Switch - Value1
Prefix - Switch - Value2
Prefix - Switch - Value3
我们可以通过对 Item 的值和列表执行 Outer Join 并使用新生成的列表创建输出来完成此操作
<list-outer-join Condition="'%(Properties.Type)'=='StringListProperty' And
'%(Properties.CommandLineValueSeparator)'==''"
Include="$(%(Properties.Name))" >
<Prefix>%(Properties.Prefix)%(Properties.Switch)%(Properties.Separator)</Prefix>
</list-outer-join>
<options Include="%(list-outer-join.Prefix)"%(list-outer-join.Identity)"" />
第一个命令创建 outer join
,第二个命令以适当的格式输出元素。运行这些行后,最终输出集合应包含
/Fd"Debug\vc120.pdb"
/Zc:wchar_t-
/D "WIN32"
/D "_DEBUG"
/D "_CONSOLE"
DynamicEnumProperty
DynamicEnumProperty 与 StringProperty
非常相似。您所要做的就是正确格式化前缀、开关和值。
EnumProperty
EnumProperty 元素有点棘手。属性中保存的数据不能直接用于生成开关。它保存子 <EnumValue> 元素的名称,该元素具有开关信息。因此,要获取实际的开关值,我们必须解析 EnumProperty 的子元素。
<enum-values Condition="'%(Properties.Type)'=='EnumProperty'"
Include="%(Properties.Children)" >
<Name>%(Properties.Name)</Name>
<Prefix>%(Properties.Prefix)</Prefix>
</enum-values>
现在,我们可以生成存储在 EnumProperty 元素中的命令行选项。
<options Condition="'$(%(enum-values.Name))'=='$([Regex]::Match(%(Identity),
(?<=Name=")(.*?)(?=")))'"
Include="%(enum-values.Prefix)$([Regex]::Match(%(Identity), (?<=Switch=")(.*?)(?=") ))" />
完成!
Item <options>
现在包含我们在 XML 文件中配置的所有开关的列表。最终值可以作为 @(options, ' ')
检索,以获取由空格分隔的 options
字符串。
实现
我到目前为止讨论的所有内容仅用于演示目的。我一直在努力解释操作原理和事件序列。我还试图演示如何使用 MSBuild 来实现这些序列。
在 XML 处理方面,没有什么比 XSLT 更好的了。我包含了使用 XSLT 转换实现的相同算法的超高效(与 MSBuild 相比)实现。它没有什么革命性的,所以我不会详细讨论它。
Using the Code
示例文件包含算法的 MSBuild 实现,应仅用于教育目的。在生产环境中使用时,请下载并使用 ConfGen.targets。
该文件包含一个名为 GetXmlConfg
的 Target,它执行所有必要的操作。
它要求在调用时传递包含 XML 文件路径的属性 PropertyPageSchema
。
它还可以接受可选参数 Name
,该参数指定 Rule
元素的名称,如果文件中定义了多个 Rule
。
该目标可以通过 MSBuild 任务执行,如下所示
<MSBuild Projects="$(MSBuildProject)" Properties="PropertyPageSchema=UI.xml" Targets="GetXmlConfig" >
<Output PropertyName="out-options" TaskParameter="TargetOutputs"/>
</MSBuild>
<Message Text="TargetOutput : $(out-options)" Importance="high"/>
历史
- 2015 年 3 月 9 日 - 发布
- 2015 年 3 月 15 日 - 更新 ConfGen.targets