In CPS, the role of XAML rules is to describe to CPS what and how properties, items, and metadata from msbuild matter. There are 4 ways to add XAML rules to the project for CPS to pick up. These are:
- Including the .xaml file via MSBuild
PropertyPageSchema
items - Embedding the xaml in your assembly and exposing it via a MEF export
- Adding either the xaml file or Rule object programatically via a CPS API
- Implement
IRuleObjectProvider
and expose it via a MEF export
This is the recommended approach, and the most flexible as you can take full advantage of MSBuild to detetermine when and how the rule is included.
This is the only option that does not require adding components to the MEF composition or access to the CPS API, and as such is the only option in scenarios where those are not available (e.g., distributing a rule as part of a NuGet package).
One disadvantage is that the PropertyPageSchema
items need to go into a .props or .targets file that
is imported into the user's project. If you do not already have such a .props/.targets file, one of
the other approaches may be simpler.
Another disadvantage is the difficulty around localization. If your rule contains text that needs to be translated into the user's locale you will need to provide multiple copies of the .xaml file (one per locale) and add MSBuild logic to pick the appropriate one based on the culture/locale settings.
Distribution/deployment is also a consideration, especially when localization is needed. You need to ensure that all your XAML files are properly included in your final distributable, whether that be a NuGet package, VS extension, etc.
This is a good option if you have access to the MEF composition, don't need the flexibility provided
by PropertyPageSchema
items, and prefer to define rules in XAML.
Localization is still a disadvantage, as you will still need to produce locale-specific XAML and embed them properly into the expected satellite assemblies.
Embedded XAML files cannot override or extend rules defined in XAML files. See extending rules for more information.
You can use the IAdditionalRuleDefinitionsService
to dynamically add and remove rule files and objects.
This is generally the most complicated approach, and is only recommended when you need a high degree
of control over when a rule is available in the project or if you need to dynamically generate the contents
(i.e., properties, categories, or metadata) of a rule.
This approach is only available starting in VS 2022 Update 1 (Dev17.1).
This is a good option if you have access to the MEF composition, don't need the flexibility provided
by PropertyPageSchema
items, and prefer to define rule objects in code rather than XAML.
One significant advantage of this approach is localization, as the code generating the rule can load localized text from a resource file or similar. This avoids the need to create and distribute localized versions of a .xaml file.
Rule objects defined in this way can only override or extend rules from other IRuleObjectProvider
s.
See extending rules for more information
This is the recommended way of adding rules to CPS. A xaml rule can be simply included via msbuild evaluation.
<ItemGroup>
<PropertyPageSchema Include="my_rule.xaml">
<!-- The Context determines what it applies to. See below for more details -->
<Context>File;BrowseObject;</Context>
</PropertyPageSchema>
</ItemGroup>
This method is recommended when your rule is "private" to your implementation, like backing an
IDebugLaunchProvider
. With the MEF export method, CPS will handle adding/removing the rule
the rule for you.
-
Reference the ProjectSystem SDK package: https://dev.azure.com/azure-public/vside/_artifacts/feed/vssdk/NuGet/Microsoft.VisualStudio.ProjectSystem.Sdk.Tools
-
Include the rule as
XamlPropertyRule
in your project. This will embed the rule in your assembly (namedXamlRuleToCode:{rule_name}.xaml
) and optionaly generate a partial class for easy access to the rule.
<ItemGroup>
<XamlPropertyRule Include="my_rule.xaml">
<Namespace>MyNameSpace</Namespace> <!-- optional -->
<DataAccess>IRule</DataAccess> <!-- None or IRule. IRule adds APIs for accessing the properties -->
<RuleInjectionClassName>ProjectProperties</RuleInjectionClassName> <!-- Name of the generated class. -->
<RuleInjection>ProjectLevel</RuleInjection> <!-- None or ProjectLevel. None means no class is generated. -->
</XamlPropertyRule>
</ItemGroup>
- Add a MEF export. CPS uses only the metadata from this export, we will never evaluate the export:
[ExportPropertyXamlRuleDefinition("YourAssemblyName", "XamlRuleToCode:my_rule.xaml", "{Context}")]
[AppliesTo(MyUnconfiguredProject.UniqueCapability)]
private object MyRule { get { throw new NotImplementedException(); } }
- Import
IAdditionalRuleDefinitionsService
. This is in theUnconfiguredProject
scope. - Add the rule via
AddRuleDefinition(string path, string context)
orAddRuleDefinition(Rule rule, string context)
[Import]
IAdditionalRuleDefinitionsService AdditionaRuleDefinitionsService { get; }
/// <summary>
/// You are responsible for making sure this is called. Can be via an auto or dynamic load component.
/// </summary>
void RegisterMyRules()
{
this.AdditionaRuleDefinitionsService.AddRuleDefinition(@"path\to\my_rule.xaml", "{Context}");
this.AdditionaRuleDefinitionsService.AddRuleDefinition(this.GetMyXamlRule(), "{Context}");
}
Microsoft.Build.Framework.XamlType.Rule GetMyXamlRule()
{
// return a fully constructed Microsoft.Build.Framework.XamlType.Rule instance
}
- You can also remove the rules added via
RemoveRuleDefinition
.
Define and export an IRuleObjectProvider
as follows:
[ExportRuleObjectProvider(name: "MyRuleProviderName", context: "Project")]
[Order(0)]
[AppliesTo(MyUnconfiguredProject.UniqueCapability)]
internal class MyRuleProvider : IRuleObjectProvider
{
public IReadOnlyCollection<Rule> GetRules()
{
var rule = new Rule();
rule.BeginInit();
rule.Name = "rule_name";
rule.PageTemplate = "generic";
rule.Properties.Add(new StringProperty
{
Name = "AStringProperty"
});
// Add additional categories, properties, metadata, etc.
rule.EndInit();
// Add additional rules.
return ImmutableList<Rule>.Empty
.Add(rule);
}
}
Notes:
- The name given in the
ExportRuleObjectProvider
must be unique to that specific implementation ofIRuleObjectProvider
. Duplicating the name across multiple implementations may cause them to be ignored entirely. - Rule objects from providers with higher
Order
numbers can override or extend those from providers with lower numbers.
Rule Context
is what determines which catalog the rule shows up in CPS. There are a few options:
/// <summary>
/// Well known property page (rule) contexts as they may appear in .targets files.
/// </summary>
public static class PropertyPageContexts
{
/// <summary>
/// Rules that apply at a per-item level, or at the project level to apply defaults to project items.
/// </summary>
public const string File = "File";
/// <summary>
/// Rules that apply only at the project level.
/// </summary>
public const string Project = "Project";
/// <summary>
/// Rules that apply only to property sheets.
/// </summary>
public const string PropertySheet = "PropertySheet";
/// <summary>
/// Rules that are invisible except for purposes of programmatic subscribing to project data.
/// </summary>
public const string ProjectSubscriptionService = "ProjectSubscriptionService";
/// <summary>
/// A special rule catalog for purposes of programmatic subscribing to project data.
/// </summary>
public const string Invisible = "Invisible";
/// <summary>
/// Rules that describe properties that appear in the Properties tool window
/// while an item is selected in Solution Explorer.
/// </summary>
public const string BrowseObject = "BrowseObject";
/// <summary>
/// Rules that describe configured project properties.
/// This context currently only supports the Xaml rule to define configuration related project level properties.
/// </summary>
public const string ConfiguredBrowseObject = "ConfiguredBrowseObject";
}