Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / XML

Creating Custom UI Property Pages/Sheets in Visual Studio - Part 2

4.76/5 (7 votes)
8 Mar 2015CPOL6 min read 24.6K   506  
Generating command line options from ProjectSchemaDefinitions Rule XML

Introduction

This is the second article in the series on how to integrate third party tools into configuration subsystem of Visual Studio. In my previous article, I explained how to create custom property sheets which show up in "Properties" dialog of Visual Studio. In this article, I'll explain how to retrieve setup data and format it according to tool's requirements.

Background

Property sheet is introduced into configuration subsystem by creating XML file containing project schema definition with Rule and Property elements. Each UI control on the sheet will have <...Property> element defining it. So if your XML looks something like this:

XML
<?xml version="1.0" encoding="utf-8"?>
<Rule ...="">
  ...
  <BoolProperty Name="StdCall" DisplayName="__stdcall" ...=""/>
  <StringProperty Name="PrecompiledHeaderFile" ...="" />
</Rule>

In your Target, you would have code which would retrieve data from property variables and format it into command line options. May be something like this:

XML
<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>

This is a straightforward but laborious and not so flexible method. I'd like to show you how this process could be done without any hard coding by using XmlPeek task.

User Interface

When UI property sheet is created (See for more info), you have few options which template to use. If you define property sheet's Rule to use "tool" template:

XML
<?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 will allow you to add special category with Subtype "CommandLine":

XML
<Category Name="Command Line" DisplayName="Command Line" Subtype="CommandLine" />

This category opens up a read only view into set of command line options generated from your configuration:

Image 1

These switches and parameters are created based on attributes you set on individual <...Property> elements. We will briefly go over these attributes:

IncludeInCommandLine

This Boolean attribute (See reference) hints to the command-line generator whether to include this property in the command line.

SwitchPrefix

The SwitchPrefix attribute (See Reference) holds the preamble to the switch such as / in the example above, generated by the engine. This property could be set on Rule element or any of the property elements.

Switch

The Switch attribute (See Reference) holds the representation of the switch itself. In the example above, these are SLP, SLP-V, SLP-VS.

ReverseSwitch

The ReverseSwitch attribute (See Reference) is present only on <BoolPreperty...> element and holds representation of the switch when property has value "false". Obviously Switch holds representation of value "true".

Separator

The Separator attribute (See Reference) holds the token used to separate a switch from its value.

CommandLineValueSeparator

The CommandLineValueSeparator attribute (See Reference) holds the separator used to divide values in list property. So when <StringListProperty ... > holds Val1;Val2;Val3, if CommandLineValueSeparator is specified as comma, the command line looks like this: /p"Val1","Val2","Val3" If not specified, the command line looks like this: /p"Val1" /p"Val2" /p"Val3". Setting up Separator attribute changes it into: /p:"Val1","Val2","Val3" and /p:"Val1" /p:"Val2" /p:"Val3" respectively.

Example

XML
<BoolProperty ReverseSwitch="Zc:wchar_t-" 
Name="TreatWChar"  Switch="Zc:wchar_t" ...>
<StringProperty Name="ProgramDataBaseFileName" Switch="Fd" ... >
<StringListProperty Name="PreprocessorDefinitions" Switch="D " ...>
...

The combination of the above properties will generate the following command line options:

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

Generating Command Line

Getting the Properties

The fact that these rules and properties are defined in regular XML files allows us to process them in code. All we have to do is to query the file and get all the properties with IncludeInCommandLine attribute not set to "false". We could query any XML file by using XmlPeek task. (For detailed reference on how to call tasks, see MSDN.)

XML
<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>

This query will return all the configuration properties in the XML file loaded into 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" ... /> 

Once we have all the properties loaded into Item, we could do something useful with them.

Processing Properties

We will be using combination of Transforming and Batching to perform parsing of all the relevant attributes and creating final command line options base of these attributes (I will not be covering Transforming and Batching in this article, these concepts are well covered elsewhere). First, we need to parse all the relevant data.

Parsing

Parsing is done by going over each element and analyzing the data. Each element is stored as a simple string so we will be using Regex.Match to extract values and store them into Metadata for each item. The code should look something like this (note: sample code has been simplified for demonstration purposes):

XML
<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>

In this example, we are using construct "@(Peeked -> '%(Identity)')" to tell MSBuild to create Item called <Properties>, go over each element in <Peeked> and add data from it to <Properties> without modifications. We also add Metadata <Type>, <Name>, <Prefix>, etc. to each element in <Properties> with data parsed from original property elements.
Once it is done, we should have <Properties> filled with original data and parsed values stored as Metadata.

Generating Options

Now we could go over each individual property type by type, extract data and form output switches applying appropriate formatting. We will be using Item <options> to collect generated output.

StringProperty

We start with StringProperty elements:

XML
<options Condition="'%(Properties.Type)' == 'StringProperty' 
And '$(%(Properties.Name))'!=''"
        Include="%(Properties.Prefix)%(Properties.Switch)%(Properties.Separator)'$(%(Properties.Name))'"
/>

In this batch, we go over every StringProperty element in the properties collection and transform it into
%(Properties.Prefix)%(Properties.Switch)%(Properties.Separator)$(%(Properties.Name)) sequence. Note the last element $(%(Properties.Name)). It contains deference of variable with name held in
%(Properties.Name). In other words, it is similar to global['name']. Using the example shown above, the output will contain:

/Fd"Debug\vc120.pdb"
IntProperty

Transforming IntProperty is very similar to StringProperty with the exception of quote symbols around value.

XML
<options Condition="'%(Properties.Type)' == 'StringProperty' 
And '$(%(Properties.Name))'!=''"
        Include="%(Properties.Prefix)%(Properties.Switch)%(Properties.Separator)$(%(Properties.Name))"
/>
BoolProperty

Transforming BoolProperty requires two runs. First time for all "true" values and second for all "false" values.

XML
<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)" />

At this point, <options> list should contain:

XML
/Fd"Debug\vc120.pdb" 
/Zc:wchar_t-
StringListProperty

Processing of StringListProperty elements is a bit more trickier. If CommandLineValueSeparator is set, all we have to do is to replace divider with symbol contained in CommandLineValueSeparator (note: sample is simplified):

XML
<options Condition="'%(Properties.Type)' == 'StringListProperty' And 
                    '%(Properties.Divider)'!=''"
         Include="%(Properties.Prefix)%(Properties.Preamble)&#34; \
                  $(Replace($(%(Properties.Name)), ';', '%(Properties.Divider)'))&#34;" />

If CommandLineValueSeparator we need to output separate switch for each list value contained in the variable:

Prefix - Switch - Value1,Value2,Value3  

has to become:

Prefix - Switch - Value1
Prefix - Switch - Value2
Prefix - Switch - Value3

We can do it by performing Outer Join on Item's value and the list and use newly generated list to create output:

XML
<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;" />

First command creates outer join and second outputs elements in proper format. After running these lines, the final output collection should contain:

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

The DynamicEnumProperty is very similar to StringProperty. All you have to do is to format prefix, switch and value properly.

EnumProperty

The EnumProperty elements are a little bit tricky. The data held in the property cannot be directly used to generate the switch. It holds name of the child <EnumValue> element which has the switch information. So to get to actual switch values, we have to parse child elements of the EnumProperty.

XML
<enum-values Condition="'%(Properties.Type)'=='EnumProperty'" 
Include="%(Properties.Children)" >
  <Name>%(Properties.Name)</Name>
  <Prefix>%(Properties.Prefix)</Prefix>
</enum-values>

Now, we can generate command line options stored in EnumProperty elements.

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

Done!

Item <options> now contains a list of all the switches we configured in XML file. The final value could be retrieved as @(options, ' ') to get a string of options separated by space.

Implementation

Everything I discussed up to this point is for demonstration purposes only. I've been trying to explain the principle of operation and sequence of events. Also I was trying to demonstrate how MSBuild could be used to implement these sequences.

When it comes to XML processing, nothing beats the XSLT. I've included super efficient (compared to MSBuild) implementation of the same algorithm but using XSLT transform. There is nothing revolutionary about it so I will not be discussing it in details.

Using the Code

The sample file contains MSBuild implementation of the algorithm and should be used for educational purposes only. For use in production environment, download and use ConfGen.targets.
The file contains a single Target called: GetXmlConfg which performs all the necessary operations.

It requires property PropertyPageSchema containing path to XML file to be passed when called.
It can also accept optional parameter Name which specifies name of the Rule element, if more than one Rule is defined in the file.

The target could be executed by MSBuild task like this:

XML
<MSBuild Projects="$(MSBuildProject)" Properties="PropertyPageSchema=UI.xml" Targets="GetXmlConfig" >
  <Output PropertyName="out-options" TaskParameter="TargetOutputs"/>    
</MSBuild>

<Message Text="TargetOutput : $(out-options)"  Importance="high"/>

History

  • 03/09/2015 - Published
  • 03/15/2015 - Updated ConfGen.targets

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)