Platform extensibility - Part 2
In my previous blog post on platform extensibility, I introduced you to the data-driven nature of the property pages UI. I explained how a collection of xml files drives the UI but stopped short of describing the contents of these files. I'll continue from where I stopped in that post and describe the format of one such xml file so that you have enough information to confidently author your own. The contents of this post will also come in handy when I discuss Build Customizations in my upcoming post.
To recall, every top level node under Configuration Properties in the property pages UI (such as C/C++, Linker, etc) is called a Rule. Each Rule generally has multiple Category nodes under it and each Category node houses a bunch of properties, frequently displayed as a property grid on the right pane in the UI. Underlying every Rule, there is an xml file from which the UI sources its data and some rendering information. This xml file represents a UI-independent data model for a Rule and its properties, although as we will see a few very common UI terms do make their way into this file.
I'll use the %ProgramFiles%\MSBuild\Microsoft.Cpp\v4.0\cl.xml file to realize my goal for this post. You may want to open it in notepad or, to get the nice color formatting, in VS. The first thing to know about this xml file is that it is in the XAML format. At this stage, XAML is just a convenient xml format to express information and has no other significance. The property page UI is built using WinForms and not WPF, hence we do not utilize the XAML-driven WPF architecture although that is the direction we would like to head in the future.
If you stripped cl.xml of all data, you will end up with the following skeleton:
<?xml version="1.0" encoding="utf-8"?>
<Rule>
<Rule.DataSource />
<Rule.Categories>
<Category />
...
</Rule.Categories>
<BoolProperty />
<EnumProperty />
<IntProperty />
<StringProperty />
<StringListProperty />
...
</Rule>
In other words, a property page xml generally contains a Rule declaration which in turn includes the declarations for a bunch of categories, a data source and most importantly a collection of properties of various types. The snippet above shows the five possible types a property can have. In what follows, we will explain each of these major elements while showing some of the metadata that can be attached to them.
1. Rule: Rule is generally the lone root node in the xml file. A Rule can have many attributes defined on it. The following snippet from cl.xml shows many of them.
<Rule Name="CL" PageTemplate="tool" SwitchPrefix="/" Order="10"
xmlns="https://schemas.microsoft.com/build/2009/properties"
xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sys="clr-namespace:System;assembly=mscorlib">
<Rule.DisplayName>
<sys:String>C/C++</sys:String>
</Rule.DisplayName>
I'll briefly describe the attributes you see above:
a. Name: The Name attribute is an id for the Rule. It needs to be unique among all the property page xml files for a project.
b. PageTemplate: The value of this attribute is used by the UI to choose from a collection of UI templates. The "tool" template renders the properties in a standard grid format. Other in-built values for this attribute are "debugger" and "generic". See the Debugging node and General node, respectively, to see the UI format resulting from specifying these values. The UI for "debugger" page template uses a drop-down box to switch between the properties of different debuggers whereas the "generic" template displays different property categories all in one page as opposed to having multiple category sub-nodes under the Rule node. This attribute is just a suggestion to the UI; the xml file is designed to be UI independent. So, a different UI could use this attribute for different purposes.
c. SwitchPrefix: This is the prefix used in the command line for the switches. A value of "/" would result in switches that look like /ZI, /nologo, /W3, etc.
d. Order: This is a suggestion to a prospective UI client on the relative location of this Rule compared to all other Rules in the system.
e. The namespaces: This is a standard XAML element. You can see three namespaces listed. These correspond to the namespaces for the XAML deserialization classes, XAML schema and system namespace, respectively.
f. DisplayName: This is the name that is shown on the property page UI for the Rule node. This value is localized. We created DisplayName as a child element of Rule rather than as an attribute (like Name or SwitchPrefix) because of internal localization tool requirements. From XAML's perspective, both are equivalent. So, you can just make it an attribute to reduce clutter or leave it as it is.
g. DataSource: This is a very important property that tells the project system the location from which the property value should read from and written to, and its grouping (explained below). For cl.xml, we have
<DataSource Persistence="ProjectFile" ItemType="ClCompile" Label="" HasConfigurationCondition="true" />
Persistence="ProjectFile" tells the project system that all properties for the Rule should be written to the project file or the property sheet file (depending on which node was used to spawn the property pages). The other possible value is "UserFile" which will write the value to the .user file.
ItemType="ClCompile" says that the properties will be stored as ItemDefinition metadata or item metadata (the latter only if the property pages were spawned from a file node in solution explorer) of this item type. If this field is not set, then the property is written as a common property in a PropertyGroup.
Label="" indicates that when the properties are written as ItemDefinition metadata, the label of the parent ItemDefinitionGroup will be empty (every MSBuild element can have a Label). In VS 2010 we use labeled groups to delineate the VC++ project file. I'll go over the ordered layout design of the .vcxproj file in a later post. Getting back to the Label attribute, note that the groups housing most Rule properties do not have a label (or in other words, they have the empty string as the label).
HasConfigurationCondition="true" tells the project system to affix a configuration condition to the value so that it takes effect only for the current project configuration (the condition could be affixed to the parent group or the value itself). E.g. open the property pages off the project node and set the value of the property Treat Warnings As Error under Configuration Properties > C/C++ > General to Yes. The following value is written to the project file. Notice the configuration condition attached to the parent ItemDefinitionGroup.
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<ClCompile>
<TreatWarningAsError>true</TreatWarningAsError>
</ClCompile>
</ItemDefinitionGroup>
If this value were set in the property pages spawned off a file, say stdafx.cpp, then we would have the property written under the stdafx.cpp item in the project file as shown below. Notice how the configuration condition is directly attached to the metadata itself.
<ItemGroup>
<ClCompile Include="stdafx.cpp">
<TreatWarningAsError Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">true</TreatWarningAsError>
</ClCompile>
</ItemGroup>
Another attribute of DataSource not listed above is PersistedName. If you wish to represent a property in the project file using a different name, this attribute will hold that different name. By default this attribute is set to the property's Name since most properties are likely to be stored as themselves in the project files.
I'll end the description of the DataSource attribute by mentioning that an individual property can override its parent Rule's DataSource to point to a different data source. In that case, the location for that particular property's value will be different from other properties in the Rule.
There are other attributes of a Rule such as Description, SupportsFileBatching, etc that are not shown here. The full set of attributes applicable to a Rule or on any other element can be obtained by browsing the msdn documentation for these types (currently, the descriptions included in these pages are sparse, but they should be accrue over time). Alternately, they can be gleaned by examining the public properties on the types in the Microsoft.Build.Framework.XamlTypes namespace living in the Microsoft.Build.Framework .dll assembly (you can use ildasm.exe for this purpose if you wish).
DisplayName, PageTemplate, Order are few UI related properties that are present in this otherwise UI-independent data model. That is fine since these are not tied to a particular UI technology. Further, these are almost certain to be used by any UI that is used to display the property pages.
DisplayName and Description are two properties that are present on almost all elements in the xml file. And these are the only two properties that are localized (localization of these strings will be explained in a later post).
2. Category: A Rule can have multiple Categories. The order in which the categories are listed in the xml file is a suggestion to the UI to display the categories in the same order. E.g. the order of the categories under the C/C++ node as seen in the UI - General, Optimization, Preprocessor, ... - is the same as that in cl.xml. A sample category looks like this:
<Category Name="Optimization">
<Category.DisplayName>
<sys:String>Optimization</sys:String>
</Category.DisplayName>
</Category>
The above snippet shows the Name and DisplayName attributes that have been described before. Once again, there are other attributes a Category can have that are not used above. You can know about them by reading the documentation or by examining the assemblies using ildasm.exe.
3. Properties: This is the meat of the xml file and contains the list of all properties in this Rule. Each property can be one of five possible types shown in the XAML skeleton above. Of course, you could have only a few of those types in your file. A property has a number of attributes that allow it to be described richly. I'll explain only the StringProperty here. The rest are very similar.
<StringProperty Subtype="file" Name="ObjectFileName" Category="Output Files" Switch="Fo">
<StringProperty.DisplayName>
<sys:String>Object File Name</sys:String>
</StringProperty.DisplayName>
<StringProperty.Description>
<sys:String>Specifies a name to override the default object file name; can be file or directory name.(/Fo[name])</sys:String>
</StringProperty.Description>
</StringProperty>
Most of the attributes in the snippet have been described before. The new ones are Subtype, Category and Switch.
a. Subtype: Subtype is an attribute available only for StringProperty and StringListProperty; the rest of the attributes described below are applicable to any type of property. Subtype gives contextual information on this property. E.g. the value of "file" indicates that not only is this property a string, but it also represents a file path. Such contextual information can be and is used to enhance the editing experience by providing a windows explorer as the property's editor that allows the user to choose the file visually rather than type its path (which is still possible).
b. Category: This declares the category under which this property falls. Try to find this property under the Output Files category in the UI.
c. Switch: When a Rule represents a tool - such as the compiler tool in this case - most properties of the Rule are passed as switches to the tool executable during build time. The value of this attribute indicates the switch literal to be used. The property above specifies that its switch should be Fo. Combined with the SwitchPrefix attribute on the parent Rule, this property is passed to the executable as /Fo"Debug\" (visible in the command line for C/C++ in the property page UI).
I would like to mention a few other attributes for a property that are not illustrated here. These are:
d. Visible: If for some reason, you don't want your property to show up in the property pages (but probably still available during build time), set this attribute to false.
e. ReadOnly: If you want to provide a read-only view of this property's value in the property pages, set this attribute to true.
f. IncludeInCommandLine: Some properties may not need to be passed to a tool during build time. Setting this attribute to false will prevent it from being passed.
I hope the above description of the format of a property page XAML file has given you enough details to help you write one of your own.
Comments
Anonymous
June 18, 2009
Thanks for these posts, while lots of stuff can be figured out from looking at the xml files themselves, there are always many useful details in your posts. Right now I am writing a Flex (lexical scanner) rule as an exercise :-) Few remarks: the xml files and your samples contain Subtype, but you mention SubType (capital T) - can be a bit confusing as to what is the right way to spell it. And in 3.d. and 3.f., didn't you mean "false" instead of "true" (otherwise these attributes would only make common sense if named "Invisible" and "DontIncludeInCommandLine")?Anonymous
June 19, 2009
I probably need a hand - I created $(VCTargetsPath)flex.xml and specified ItemType="Flex" in its Rule.DataSource, filled it with tons of options from the flex manpage. Then hand-edited my .vcxproj, added <ItemGroup><Flex Include="input.l"/><ItemGroup>. Then before that into the project file, <ItemGroup><PropertyPageSchema Include="$(VCTargetsPath)flex.xml"/></ItemGroup>. Then I edited the vcxproj.filters file, added new filter, somehow tried to "hand-generate" new UniqueIdentifier for it and called the filter "Flex Files" with extensions "l;lex", and added the file "input.l" to have the "Flex Files" filter. And have created the input.l file in the project directory. Still can't see the input.l file in the solution explorer (but can see empty "Flex Files" folder there), and no Flex property schema loaded in property pages. Am I doing something wrong? Or is this not yet supported? Thanks.Anonymous
June 19, 2009
Boris, this is a reply to your post on Friday, June 19, 2009 5:36 AM. Thanks for your feedback. I am glad you find the posts useful. As for the 3 mistakes you pointed to, yes, you are right. I corrected them in the post directly. In general, always go with what is in the xml file on your system since we know that it works. Of course, that is no excuse to having inconsistencies in the blog post :)Anonymous
June 19, 2009
Boris, this is a reply to your post on Friday, June 19, 2009 9:25 AM. You are almost there. The reason solution explorer does not show the input.l file is because the system does not know that the item type "Flex" represents an item type for a source file. Do the following three steps and everything should work. Step 1
Add the below snippet after the final </Rule> tag. It tells the system about the new ContentType you are introducing. I will explain ContentType in a later post. You may want to look at at %ProgramFiles%MSBuildMicrosoft.Cppv4.0ProjectItemsSchema.xml to see the definitions for well known content types. <ItemType Name="Flex" DisplayName="Flex Scanner" /> <FileExtension Name=".l" ContentType="Flex" /> <FileExtension Name=".lex" ContentType="Flex" /> <ContentType Name="Flex" DisplayName="Flex Scanner" ItemType="Flex" /> Step2
Add an enclosing <ProjectSchemaDefinitions> ... </ProjectSchemaDefinitions> root element for the whole file. This should enclose the previous <Rule>…</Rule> section and whatever you added in Step1 (but not the XML declaration element) Step3
Move the namespaces (and only the namespaces) from the <Rule> element to the just added <ProjectSchemaDefinitions> element. Now, restart the IDE and open your project. You should have your files as well as the property pages. With the files now appearing in sln explorer, there is no need to modify the filters file manually. When you load the project, create a filter in the sln explorer, set its "Filter" property appropriately and move your Flex files into that filter. But, if you already have it manually added in the filters file, you can leave it as it is (hand generating the UniqueIdentifier guid is fine).
Anonymous
June 19, 2009
Pavan, thank you very much, after applying your suggestions, I was successful! One remark though - I specified Switch="-", but the property page UI still shows "/". Looking forward to your next posts! Thanks, BorisAnonymous
June 29, 2009
Boris, this is a reply to your comment on Saturday, June 20, 2009 5:11 AM. Sorry for the delay in responding. Boris, you should change the "SwitchPrefix" attribute to "-" rather than the "Switch" attribute as you mention. Note: If you want to change the switch prefix for a particular property only, change/define SwitchPrefix on that particular property. If you want it changed on all properties in the Rule, change SwitchPrefix on the Rule itself (and make sure no other property overrides it locally to some other value)Anonymous
June 29, 2009
Hello Pavan, I checked and I made a typo in my post, but not in the flex file itself. In the file itself, I have correctly <Rule Name="Flex" PageTemplate="tool" SwitchPrefix="-" Order="10">. The "SwitchPrefix" string is occuring only once in the whole file, and that is in the Rule element as specified above. I posted the file at http://cid-4b1a2a5233d113a1.skydrive.live.com/self.aspx/.Public/flex.xml in case you want to take a look. I don't know what causes the UI to not honor the SwitchPrefix, maybe that's a post-Beta 1 feature? Thanks, Boris