Controls command visibility, enablement, and hiding in Visual Studio extensions using declarative constraints and dynamic BeforeQueryStatus callbacks.
How this skill is triggered — by the user, by Claude, or both
Slash command
/vs-extensibility-skills:controlling-command-visibilityThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Command visibility determines **when** a command appears in menus, toolbars, or context menus. There are two layers:
Command visibility determines when a command appears in menus, toolbars, or context menus. There are two layers:
<VisibilityConstraints> in .vsct (Toolkit/VSSDK) or VisibleWhen in code (VisualStudio.Extensibility).BeforeQueryStatus (Toolkit/VSSDK) or command state callbacks that run every time VS queries the command status.Best practice: use declarative constraints for the initial visibility so the package is not loaded just to hide a command, then use BeforeQueryStatus for fine-grained runtime logic after the package is already loaded.
Visibility control prevents menu clutter and keeps the VS UI relevant to the user's current context. The key architectural insight is that declarative constraints work without loading your extension — VS evaluates them from .vsct metadata or VisibleWhen attributes at startup.
When to use this vs. alternatives:
No .vsct file is needed. Visibility and enablement constraints are declared directly in CommandConfiguration using activation constraints.
NuGet package: Microsoft.VisualStudio.Extensibility
Key namespaces: Microsoft.VisualStudio.Extensibility, Microsoft.VisualStudio.Extensibility.Commands
VisibleWhenusing Microsoft.VisualStudio.Extensibility;
using Microsoft.VisualStudio.Extensibility.Commands;
namespace MyExtension.Commands;
[VisualStudioContribution]
internal class AnalyzeCSharpCommand : Command
{
public AnalyzeCSharpCommand(VisualStudioExtensibility extensibility)
: base(extensibility) { }
public override CommandConfiguration CommandConfiguration => new("Analyze C# File")
{
Placements = [CommandPlacement.KnownPlacements.ToolsMenu],
Icon = new(ImageMoniker.KnownValues.CSFileNode, IconSettings.IconAndText),
// Visible only when the active file matches *.cs
VisibleWhen = ActivationConstraint.ClientContext(
ClientContextKey.Shell.ActiveSelectionFileName, @"\.cs$"),
};
public override async Task ExecuteCommandAsync(IClientContext context, CancellationToken ct)
{
await this.Extensibility.Shell().ShowPromptAsync(
"Analyzing C# file…", PromptOptions.OK, ct);
}
}
EnabledWhenpublic override CommandConfiguration CommandConfiguration => new("Debug Command")
{
Placements = [CommandPlacement.KnownPlacements.ToolsMenu],
// Enabled only when a solution is fully loaded
EnabledWhen = ActivationConstraint.SolutionState(SolutionState.FullyLoaded),
};
public override CommandConfiguration CommandConfiguration => new("Run Tests")
{
Placements = [CommandPlacement.KnownPlacements.ToolsMenu],
VisibleWhen = ActivationConstraint.SolutionState(SolutionState.FullyLoaded)
& ActivationConstraint.ClientContext(
ClientContextKey.Shell.ActiveSelectionFileName, @"\.(cs|vb)$"),
};
Override the command state callback to toggle enabled state at runtime:
public override async Task ExecuteCommandAsync(IClientContext context, CancellationToken ct)
{
// After executing, disable the command
this.DisableCommand();
// ...
this.EnableCommand();
}
VisualStudio.Extensibility does not use .vsct files or ProvideUIContextRule. All visibility logic is expressed through ActivationConstraint in CommandConfiguration. There is no equivalent of Command.Supported or BeforeQueryStatus in this model — the framework handles visibility declaratively.
Visibility is controlled in three layers:
.vsct <VisibilityConstraints> — hides/shows commands before the package loads, using a UIContext GUID.[ProvideUIContextRule] on the package class — defines the rule that activates or deactivates that UIContext GUID.BeforeQueryStatus — imperative logic that runs after the package is loaded for dynamic enable/disable.NuGet package: Community.VisualStudio.Toolkit
Key namespace: Community.VisualStudio.Toolkit
<Symbols>Store the GUID for your UIContext rule in the .vsct file so it is the single source of truth. The Toolkit's build-time code generator produces a PackageGuids class from the .vsct symbols, so you do not need to duplicate the GUID value in C# code.
<Symbols>
<GuidSymbol name="guidMyExtensionPackage" value="{YOUR-PACKAGE-GUID}" />
<GuidSymbol name="guidMyExtensionCmdSet" value="{YOUR-CMDSET-GUID}">
<IDSymbol name="MyMenuGroup" value="0x1020" />
<IDSymbol name="RunTestsCommandId" value="0x0100" />
</GuidSymbol>
<!-- UIContext GUID — single source of truth for the rule -->
<GuidSymbol name="guidCSharpFileActiveContext" value="{11111111-2222-3333-4444-555555555555}" />
</Symbols>
<VisibilityConstraints> in the .vsct fileThe <VisibilityConstraints> block tells the VS shell to show or hide a command based on the active state of a UIContext — without loading the package.
Add the <CommandFlag>DynamicVisibility</CommandFlag> to the button so VS knows the visibility can change, and add <CommandFlag>DefaultInvisible</CommandFlag> so the command starts hidden.
<Commands package="guidMyExtensionPackage">
<Groups>
<Group guid="guidMyExtensionCmdSet" id="MyMenuGroup" priority="0x0600">
<Parent guid="guidSHLMainMenu" id="IDM_VS_MENU_TOOLS"/>
</Group>
</Groups>
<Buttons>
<Button guid="guidMyExtensionCmdSet" id="RunTestsCommandId" priority="0x0100" type="Button">
<Parent guid="guidMyExtensionCmdSet" id="MyMenuGroup"/>
<Icon guid="ImageCatalogGuid" id="RunOutline"/>
<CommandFlag>IconIsMoniker</CommandFlag>
<CommandFlag>DynamicVisibility</CommandFlag>
<CommandFlag>DefaultInvisible</CommandFlag>
<Strings>
<ButtonText>Run Tests</ButtonText>
</Strings>
</Button>
</Buttons>
</Commands>
<VisibilityConstraints>
<!-- Show the command when the UIContext guidCSharpFileActiveContext is active -->
<VisibilityItem guid="guidMyExtensionCmdSet" id="RunTestsCommandId"
context="guidCSharpFileActiveContext"/>
</VisibilityConstraints>
Important command flags:
| Flag | Purpose |
|---|---|
DynamicVisibility | Allows the command's visibility to change at runtime. Required for <VisibilityConstraints> and BeforeQueryStatus visibility toggling to work. |
DefaultInvisible | The command starts hidden until its UIContext becomes active (or BeforeQueryStatus sets Visible = true). |
DefaultDisabled | The command starts disabled (grayed out). Useful when you want the button visible but not clickable until a condition is met. |
Use [ProvideUIContextRule] on the package class to define the boolean expression that activates the UIContext. Reference the GUID from the .vsct file via the generated PackageGuids class — no need to duplicate the GUID string.
using System;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Community.VisualStudio.Toolkit;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
[PackageRegistration(UseManagedResourcesOnly = true, AllowsBackgroundLoading = true)]
[InstalledProductRegistration(Vsix.Name, Vsix.Description, Vsix.Version)]
[ProvideMenuResource("Menus.ctmenu", 1)]
[Guid(PackageGuids.guidMyExtensionPackageString)]
[ProvideUIContextRule(
PackageGuids.guidCSharpFileActiveContextString, // The UIContext GUID from the .vsct symbols
name: "C# File Active",
expression: "CSharpFile", // Boolean expression using term names
termNames: new[] { "CSharpFile" },
termValues: new[] { "HierSingleSelectionName:.cs$" })] // Regex match on active selection
public sealed class MyExtensionPackage : ToolkitPackage
{
protected override async Task InitializeAsync(CancellationToken cancellationToken, IProgress<ServiceProgressData> progress)
{
await this.RegisterCommandsAsync();
}
}
The PackageGuids.guidCSharpFileActiveContextString constant is auto-generated from the .vsct <GuidSymbol name="guidCSharpFileActiveContext"> entry. This keeps the GUID in one place.
ProvideUIContextRule term values| Term value pattern | Matches when… |
|---|---|
HierSingleSelectionName:.cs$ | A single item with name ending in .cs is selected |
ActiveProjectCapability:CSharp | The active project has the C# capability |
ActiveProjectCapability:VB | The active project has the VB capability |
SolutionHasProjectCapability:CSharp | Any project in the solution has C# capability |
ActiveEditorContentType:CSharp | The active editor has C# content type |
SolutionExistsAndNotBuildingAndNotDebugging | A solution is loaded and no build/debug is running |
[ProvideUIContextRule(
PackageGuids.guidMyContextString,
name: "C# or VB project loaded",
expression: "(CSharp | VB) & SolutionLoaded",
termNames: new[] { "CSharp", "VB", "SolutionLoaded" },
termValues: new[] {
"SolutionHasProjectCapability:CSharp",
"SolutionHasProjectCapability:VB",
"SolutionExistsAndNotBuildingAndNotDebugging"
})]
BeforeQueryStatus for post-load logicOnce the package has loaded, you can use BeforeQueryStatus for fine-grained, imperative visibility or enablement logic. This runs every time VS queries the command status (e.g., when the user opens the menu).
using Community.VisualStudio.Toolkit;
using Microsoft.VisualStudio.Shell;
namespace MyExtension.Commands;
[Command(PackageIds.RunTestsCommandId)]
internal sealed class RunTestsCommand : BaseCommand<RunTestsCommand>
{
protected override void BeforeQueryStatus(EventArgs e)
{
// Example: only enable when the active document is a .cs file
ThreadHelper.JoinableTaskFactory.Run(async () =>
{
var doc = await VS.Documents.GetActiveDocumentViewAsync();
Command.Enabled = doc?.FilePath?.EndsWith(".cs", StringComparison.OrdinalIgnoreCase) == true;
});
}
protected override async Task ExecuteAsync(OleMenuCmdEventArgs e)
{
await VS.MessageBox.ShowAsync("Running tests…");
}
}
You can also toggle visibility in BeforeQueryStatus:
protected override void BeforeQueryStatus(EventArgs e)
{
Command.Visible = SomeCondition();
Command.Enabled = SomeOtherCondition();
}
Command.SupportedBy default, once the package loads and the command handler is initialized, VS delegates all visibility/enablement decisions to BeforeQueryStatus. If you want VS to continue respecting the <VisibilityConstraints> UIContext rule even after the package has loaded, set Command.Supported = false in the AfterInitializeAsync override.
Setting Supported to false tells VS: "this command handler does not manage its own visibility — defer to the declarative UIContext rule."
using Community.VisualStudio.Toolkit;
using Microsoft.VisualStudio.Shell;
namespace MyExtension.Commands;
[Command(PackageIds.RunTestsCommandId)]
internal sealed class RunTestsCommand : BaseCommand<RunTestsCommand>
{
protected override Task AfterInitializeAsync()
{
// Delegate visibility/enablement back to the UIContext rule
// defined in ProvideUIContextRule on the package class.
Command.Supported = false;
return Task.CompletedTask;
}
protected override async Task ExecuteAsync(OleMenuCmdEventArgs e)
{
await VS.MessageBox.ShowAsync("Running tests…");
}
}
When Command.Supported = false:
<VisibilityConstraints> UIContext rule remains in control of visibility at all times, even after the package loads.BeforeQueryStatus is still called but cannot override the UIContext visibility decision.BeforeQueryStatus.The .vsct file and [ProvideUIContextRule] work identically to the Toolkit approach. The difference is in how commands are registered and how BeforeQueryStatus is wired up.
NuGet package: Microsoft.VisualStudio.SDK
Key namespaces: Microsoft.VisualStudio.Shell, Microsoft.VisualStudio.Shell.Interop, System.ComponentModel.Design
Use the same .vsct structure shown in section 2. The <VisibilityConstraints>, DynamicVisibility, DefaultInvisible flags, and <GuidSymbol> for the UIContext GUID are all identical.
using System;
using System.Runtime.InteropServices;
using System.Threading;
[PackageRegistration(UseManagedResourcesOnly = true, AllowsBackgroundLoading = true)]
[InstalledProductRegistration("#110", "#112", "1.0")]
[ProvideMenuResource("Menus.ctmenu", 1)]
[Guid("YOUR-PACKAGE-GUID")]
[ProvideUIContextRule(
"11111111-2222-3333-4444-555555555555",
name: "C# File Active",
expression: "CSharpFile",
termNames: new[] { "CSharpFile" },
termValues: new[] { "HierSingleSelectionName:.cs$" })]
public sealed class MyExtensionPackage : AsyncPackage
{
// UIContext GUID must match the GuidSymbol value in the .vsct Symbols block
private const string UIContextGuid = "11111111-2222-3333-4444-555555555555";
protected override async Task InitializeAsync(CancellationToken cancellationToken, IProgress<ServiceProgressData> progress)
{
await RunTestsCommand.InitializeAsync(this);
}
}
Note: In raw VSSDK you must keep the UIContext GUID string in sync manually between the
.vsct<GuidSymbol>and the[ProvideUIContextRule]attribute. Unlike the Toolkit, there is no auto-generatedPackageGuidsclass (unless you set up your own T4/code-gen). Declare the GUID as aconstin the package and reference it from both places.
BeforeQueryStatusUse OleMenuCommand (not MenuCommand) to get access to BeforeQueryStatus.
using System;
using System.ComponentModel.Design;
using Microsoft.VisualStudio.Shell;
namespace MyExtension.Commands;
internal sealed class RunTestsCommand
{
public static readonly Guid CommandSet = new("YOUR-CMDSET-GUID");
public const int CommandId = 0x0100;
private readonly AsyncPackage _package;
private RunTestsCommand(AsyncPackage package, OleMenuCommandService commandService)
{
_package = package ?? throw new ArgumentNullException(nameof(package));
commandService = commandService ?? throw new ArgumentNullException(nameof(commandService));
var menuCommandId = new CommandID(CommandSet, CommandId);
var menuItem = new OleMenuCommand(Execute, menuCommandId);
menuItem.BeforeQueryStatus += OnBeforeQueryStatus;
commandService.AddCommand(menuItem);
}
public static async Task InitializeAsync(AsyncPackage package)
{
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(package.DisposalToken);
var commandService = await package.GetServiceAsync(typeof(IMenuCommandService)) as OleMenuCommandService;
_ = new RunTestsCommand(package, commandService);
}
private void OnBeforeQueryStatus(object sender, EventArgs e)
{
ThreadHelper.ThrowIfNotOnUIThread();
if (sender is OleMenuCommand command)
{
// Example: enable only when a .cs file is active
var dte = (EnvDTE.DTE)Package.GetGlobalService(typeof(EnvDTE.DTE));
var activeDoc = dte?.ActiveDocument;
command.Enabled = activeDoc?.Name.EndsWith(".cs", StringComparison.OrdinalIgnoreCase) == true;
}
}
private void Execute(object sender, EventArgs e)
{
ThreadHelper.ThrowIfNotOnUIThread();
VsShellUtilities.ShowMessageBox(
_package,
"Running tests…",
"Run Tests",
OLEMSGICON.OLEMSGICON_INFO,
OLEMSGBUTTON.OLEMSGBUTTON_OK,
OLEMSGDEFBUTTON.OLEMSGDEFBUTTON_FIRST);
}
}
SupportedSame concept as the Toolkit: set Supported = false on the OleMenuCommand so the shell defers to the <VisibilityConstraints> UIContext rule even after the package loads.
private RunTestsCommand(AsyncPackage package, OleMenuCommandService commandService)
{
_package = package ?? throw new ArgumentNullException(nameof(package));
commandService = commandService ?? throw new ArgumentNullException(nameof(commandService));
var menuCommandId = new CommandID(CommandSet, CommandId);
var menuItem = new OleMenuCommand(Execute, menuCommandId);
// Delegate visibility back to the UIContext rule.
// The shell will continue using the VisibilityConstraints even after load.
menuItem.Supported = false;
commandService.AddCommand(menuItem);
}
When Supported = false:
<VisibilityConstraints> UIContext rule for visibility.BeforeQueryStatus is still invoked but the UIContext rule governs visibility.| Goal | Before package load | After package load |
|---|---|---|
| Show/hide command based on file type or project capability | <VisibilityConstraints> + [ProvideUIContextRule] (Toolkit/VSSDK) or VisibleWhen (VS.Extensibility) | BeforeQueryStatus setting Command.Visible |
| Enable/disable command | DefaultDisabled flag in .vsct | BeforeQueryStatus setting Command.Enabled |
| Keep UIContext rule in control after load | Set Command.Supported = false in AfterInitializeAsync (Toolkit) or on the OleMenuCommand instance (VSSDK) | UIContext rule continues to govern visibility |
| Complex boolean conditions | [ProvideUIContextRule] with compound expressions | BeforeQueryStatus with arbitrary C# logic |
.vsct <Symbols> block. For Toolkit extensions, the build generates PackageGuids / PackageIds from the .vsct, keeping the GUID in one place. For raw VSSDK, declare a const string and reference it from both .vsct and the [ProvideUIContextRule] attribute.DynamicVisibility and DefaultInvisible on any button whose visibility is controlled by a UIContext or BeforeQueryStatus.Command.Supported = false means "this command does not claim to manage its own visibility — defer to the UIContext rule." Despite the name, it does not mean the command is unsupported.<VisibilityConstraints> / VisibleWhen) for initial visibility. This avoids loading the package just to hide a command.BeforeQueryStatus only for conditions that cannot be expressed declaratively (e.g., checking runtime state, inspecting file contents).DefaultInvisible in .vsct. Without it, the command starts visible and then hides when BeforeQueryStatus runs — causing a visible flash on startup.DynamicVisibility and the correct VisibilityConstraints entry are present. Without DynamicVisibility, VS ignores runtime visibility changes entirely.BeforeQueryStatus never fires: The command needs DynamicVisibility in .vsct. Also verify the package is loaded — BeforeQueryStatus only runs after the package is initialized.Command.Supported = false doesn't hide the command: Supported doesn't mean "visible." Setting Supported = false delegates visibility back to the UIContext rule associated with the command. Setting Visible = false actually hides it.[ProvideUIContextRule]. Terms must match the termNames and termValues arrays exactly. A typo in the GUID or an invalid operator causes silent failure.Do NOT use
BeforeQueryStatusas the sole visibility mechanism — it forces package load. Use declarative<VisibilityConstraints>(Toolkit/VSSDK) orVisibleWhen(Extensibility) for initial show/hide; reserveBeforeQueryStatusfor runtime logic.
Do NOT confuse
Command.Supported = falsewithCommand.Visible = false—Supported = falsedefers visibility to the UIContext rule (command may still be visible);Visible = falseactually hides it.
Do NOT forget
DynamicVisibilityandDefaultInvisiblecommand flags in.vsct— withoutDynamicVisibility, VS ignores runtime visibility changes; withoutDefaultInvisible, the command flashes visible then hides.
Do NOT hard-code UIContext GUIDs in multiple places — store in
.vsct<Symbols>and reference from there. Duplicate GUIDs that drift apart cause silent failures.
Do NOT use
DTE.CommandsorEnvDTE.CommandEventsfor visibility — legacy COM APIs that don't integrate with the modern UIContext/activation constraint system.
npx claudepluginhub madskristensen/vs-agent-plugins --plugin vs-extensibility-skillsAdds commands (menu items, toolbar buttons) to Visual Studio extensions using VisualStudio.Extensibility, VSIX Community Toolkit, or legacy VSSDK. Covers .vsct files, command handlers, and icon placement.
Provides behavioral guidelines to reduce common LLM coding mistakes, focusing on simplicity, surgical changes, assumption surfacing, and verifiable success criteria.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.