Recently I spent some time digging into the new Visual Studio Extensibility model, specifically around how to set up nested menu items. The new extensibility model allows us to run a .NET 8/10 extensions out of process (this comes with benefits and limitations when compared with the older SDK model).
This new model is improved, using a modern, declarative approach with C# source generators. It makes defining contributions like menus surprisingly straightforward.
Here's a snippet from a recent test extension I built that shows the pattern for creating a nested menu.
[VisualStudioContribution]
internal class ExtensionEntrypoint : Extension
{
// ... ExtensionConfiguration omitted for brevity ...
[VisualStudioContribution]
public static MenuConfiguration MyChildMenu => new("My Child Menu")
{
Children = new []
{
// This is the actual command the user will execute
MenuChild.Command<InsertGuidCommand>()
}
};
[VisualStudioContribution]
public static MenuConfiguration MyParentMenu => new("My Commands")
{
Children = new[]
{
// The parent menu contains the child menu
MenuChild.Menu(MyChildMenu),
}
};
[VisualStudioContribution]
public static CommandGroupConfiguration MyCommands => new(GroupPlacement.KnownPlacements.ExtensionsMenu)
{
Children = new[]
{
// The command group places the top-level parent menu in the 'Extensions' menu
GroupChild.Menu(MyParentMenu),
},
};
// ... InitializeServices omitted for brevity ...
}
The VisibleWhen Gotcha: A Crucial Nuance
Now, this is where I ran into a subtle but significant issue. When dealing with menu visibility, we typically use the VisibleWhen property on a command to control whether it appears based on the active context (e.g., whether a text editor is open).
The problem I discovered is that if all the child commands within a nested menu have their VisibleWhen predicate evaluate to false when the extension first loads, the parent menus (MyParentMenu in the example) will often have their visibility resolved at that point and will disappear.
The critical takeaway here is that when a child command's VisibleWhen changes from false to true later (say, when the user opens an editor), the child command may re-evaluate, but the parent menu does not. It seems the parent menu's visibility is not dynamically recalculated when the child's visibility changes; it's resolved during the initial load and then keeps that value.
The Workaround: Prioritize EnableWhen
My fix for this was to change my approach. If you have a submenu where all the child commands might legitimately be invisible when the extension first loads, do not rely on VisibleWhen to hide them and instead favor EnableWhen to control their interaction.
By using EnableWhen, you ensure the child commands (and thus the parent menus) are visible from the start, preserving your menu structure. When the condition for the command is met (like opening an editor), it becomes enabled and the user can click it. This guarantees the parent menu remains in the UI, which is what we want for a predictable user experience.
Additional Context
There are a few different Visual Studio Extension templates to start from. One using this model would end up with these referenced packages (version number might be slightly different):
<ItemGroup>
<PackageReference Include="Microsoft.VisualStudio.Extensibility.Sdk" Version="17.14.40608" PrivateAssets="all" />
<PackageReference Include="Microsoft.VisualStudio.Extensibility.Build" Version="17.14.40608" PrivateAssets="all" />
</ItemGroup>