65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.76/5 (7投票s)

2015 年 3 月 9 日

CPOL

6分钟阅读

viewsIcon

25217

downloadIcon

577

从 ProjectSchemaDefinitions Rule XML 生成命令行选项。

引言

这是关于如何将第三方工具集成到 Visual Studio 配置子系统系列文章的第二篇。在我的上一篇文章中,我解释了如何创建显示在 Visual Studio“属性”对话框中的自定义属性表。在本文中,我将解释如何检索设置数据并根据工具要求对其进行格式化。

背景

通过创建包含项目架构定义的 XML 文件,其中包含 RuleProperty 元素,将属性表引入配置子系统。表上的每个 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 属性(请参阅参考资料)保存开关本身的表示形式。在上面的示例中,它们是 SLPSLP-VSLP-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 中,我们就可以对它们做一些有用的事情。

处理属性

我们将使用 TransformingBatching 的组合来执行所有相关属性的解析,并根据这些属性创建最终的命令行选项(本文中不介绍 TransformingBatching,这些概念已在其他地方充分介绍)。首先,我们需要解析所有相关数据。

解析

解析是通过遍历每个元素并分析数据来完成的。每个元素都存储为一个简单的 string ,因此我们将使用 Regex.Match 来提取值并将它们存储到每个项的 Metadata 中。代码应如下所示(注意:示例代码已为演示目的进行简化)

<ItemGroup >
  <Properties Include="@(Peeked -> '%(Identity)')">
    <Type>$([Regex]::Match(%(Identity),         (?&lt;=&lt;)(.*?)(?=\s) ))</Type>
    <Name>$([Regex]::Match(%(Identity),         (?&lt;=Name=")(.*?)(?=") ))</Name>
    <Prefix>$([Regex]::Match(%(Identity),       (?&lt;=SwitchPrefix=")(.*?)(?=") ))</Prefix>
    <Switch>$([Regex]::Match(%(Identity),       (?&lt;=Switch=")(.*?)(?=") ))</Switch>
    <ReverseSwitch>$([Regex]::Match(%(Identity),(?&lt;=ReverseSwitch=")(.*?)(?=") ))</ReverseSwitch>
    <Separator>$([Regex]::Match(%(Identity),    (?&lt;=\sSeparator=")(.*?)(?=") ))</Separator>
    <Divider>$([Regex]::Match(%(Identity),      (?&lt;=ValueSeparator=")(.*?)(?=") ))</Divider>
    <Children>$([Regex]::Matches(%(Identity),   EnumValue\s(.*?)&#62;) )</Children>
    <Subtype>$([Regex]::Match(%(Identity),      (?&lt;=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

转换 IntPropertyStringProperty 非常相似,只是值周围没有引号。

<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)&#34; \
                  $(Replace($(%(Properties.Name)), ';', '%(Properties.Divider)'))&#34;" />

如果 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)&#34;%(list-outer-join.Identity)&#34;" />

第一个命令创建 outer join ,第二个命令以适当的格式输出元素。运行这些行后,最终输出集合应包含

/Fd"Debug\vc120.pdb" 
/Zc:wchar_t- 
/D "WIN32" 
/D "_DEBUG" 
/D "_CONSOLE"
DynamicEnumProperty

DynamicEnumPropertyStringProperty 非常相似。您所要做的就是正确格式化前缀、开关和值。

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), 
                                                                (?&lt;=Name=&#34;)(.*?)(?=&#34;)))'"
  Include="%(enum-values.Prefix)$([Regex]::Match(%(Identity), (?&lt;=Switch=&#34;)(.*?)(?=&#34;) ))" />

完成!

Item <options> 现在包含我们在 XML 文件中配置的所有开关的列表。最终值可以作为 @(options, ' ') 检索,以获取由空格分隔的 options 字符串。

实现

我到目前为止讨论的所有内容仅用于演示目的。我一直在努力解释操作原理和事件序列。我还试图演示如何使用 MSBuild 来实现这些序列。

在 XML 处理方面,没有什么比 XSLT 更好的了。我包含了使用 XSLT 转换实现的相同算法的超高效(与 MSBuild 相比)实现。它没有什么革命性的,所以我不会详细讨论它。

Using the Code

示例文件包含算法的 MSBuild 实现,应仅用于教育目的。在生产环境中使用时,请下载并使用 ConfGen.targets
该文件包含一个名为 GetXmlConfgTarget,它执行所有必要的操作。

它要求在调用时传递包含 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
© . All rights reserved.