Subscribes to Visual Studio build events (start, completion, failure) using IVsUpdateSolutionEvents, Community Toolkit, DTE, or VSSDK for out-of-process and in-process extensions.
How this skill is triggered — by the user, by Claude, or both
Slash command
/vs-extensibility-skills:handling-build-eventsThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
When your extension needs to react to solution or project builds — for example, running analysis after a successful build, showing a notification on failure, or tracking build timing — you need to subscribe to build events. Visual Studio provides multiple mechanisms across its extensibility models.
When your extension needs to react to solution or project builds — for example, running analysis after a successful build, showing a notification on failure, or tracking build timing — you need to subscribe to build events. Visual Studio provides multiple mechanisms across its extensibility models.
Build events let extensions integrate into the development feedback loop — the moment code compiles, your extension can validate output, update UI, or trigger downstream processes. The critical nuance is that build event handlers run on the UI thread, so long-running work freezes the IDE.
When to use this vs. alternatives:
| Approach | Scope | Key feature |
|---|---|---|
| VisualStudio.Extensibility | Out-of-process | No dedicated build event API yet — use activation constraints or in-proc hybrid |
Community Toolkit VS.Events.BuildEvents | In-process | Simple .NET events wrapping IVsUpdateSolutionEvents2 |
VSSDK IVsUpdateSolutionEvents / IVsUpdateSolutionEvents2 | In-process | Full control — solution-level build begin/done, per-project begin/done, cancel |
DTE BuildEvents (EnvDTE) | In-process (legacy) | COM automation events — OnBuildBegin, OnBuildDone, OnBuildProjConfigBegin/Done |
Recommendation: For in-process extensions, use the Community Toolkit (
VS.Events.BuildEvents) for the simplest subscription model. Fall back to VSSDKIVsUpdateSolutionEvents2when you need to cancel builds or access configuration-level details. The VisualStudio.Extensibility model does not yet have a dedicated build events API.
The VisualStudio.Extensibility SDK does not currently provide a dedicated API for subscribing to build events from out-of-process extensions.
You can use ActivationConstraint.SolutionState or other activation constraints to control when your extension components become active based on solution state, but these are not build event callbacks.
If you need to react to build events from an Extensibility extension, use an in-proc hybrid approach:
[VisualStudioContribution]
internal class MyExtension : Extension
{
public override ExtensionConfiguration? ExtensionConfiguration => new()
{
RequiresInProcessHosting = true,
};
}
Then use the Toolkit or VSSDK APIs described in sections 2 and 3 below.
The Toolkit wraps IVsUpdateSolutionEvents2 into simple .NET events accessible via VS.Events.BuildEvents.
NuGet package: Community.VisualStudio.Toolkit
Key namespace: Community.VisualStudio.Toolkit
public sealed class MyExtensionPackage : ToolkitPackage
{
protected override async Task InitializeAsync(
CancellationToken cancellationToken, IProgress<ServiceProgressData> progress)
{
await this.RegisterCommandsAsync();
// Solution-level events
VS.Events.BuildEvents.SolutionBuildStarted += OnSolutionBuildStarted;
VS.Events.BuildEvents.SolutionBuildDone += OnSolutionBuildDone;
VS.Events.BuildEvents.SolutionBuildCancelled += OnSolutionBuildCancelled;
// Per-project events
VS.Events.BuildEvents.ProjectBuildStarted += OnProjectBuildStarted;
VS.Events.BuildEvents.ProjectBuildDone += OnProjectBuildDone;
// Clean events
VS.Events.BuildEvents.ProjectCleanStarted += OnProjectCleanStarted;
VS.Events.BuildEvents.ProjectCleanDone += OnProjectCleanDone;
// Configuration change events
VS.Events.BuildEvents.ProjectConfigurationChanged += OnProjectConfigChanged;
VS.Events.BuildEvents.SolutionConfigurationChanged += OnSolutionConfigChanged;
}
private void OnSolutionBuildStarted(object sender, EventArgs e)
{
// Solution build has started
}
private void OnSolutionBuildDone(bool succeeded)
{
// Solution build complete — 'succeeded' is true if no projects failed
if (!succeeded)
{
// Show a notification about build failure
}
}
private void OnSolutionBuildCancelled()
{
// User cancelled the build
}
private void OnProjectBuildStarted(Project? project)
{
// A specific project started building
string? name = project?.Name;
}
private void OnProjectBuildDone(ProjectBuildDoneEventArgs args)
{
// A specific project finished building
string? name = args.Project?.Name;
bool success = args.IsSuccessful;
}
private void OnProjectCleanStarted(Project? project)
{
// A project clean operation started
}
private void OnProjectCleanDone(ProjectBuildDoneEventArgs args)
{
// A project clean operation completed
}
private void OnProjectConfigChanged(Project? project)
{
// Active project configuration changed (e.g., Debug → Release)
}
private void OnSolutionConfigChanged()
{
// Active solution configuration changed
}
}
| Event | Signature | When fired |
|---|---|---|
SolutionBuildStarted | EventHandler | Before any build actions begin |
SolutionBuildDone | Action<bool> | After all builds complete (true = all succeeded) |
SolutionBuildCancelled | Action | When the user cancels the build |
ProjectBuildStarted | Action<Project?> | When a specific project begins building |
ProjectBuildDone | Action<ProjectBuildDoneEventArgs> | When a specific project finishes building |
ProjectCleanStarted | Action<Project?> | When a project clean begins |
ProjectCleanDone | Action<ProjectBuildDoneEventArgs> | When a project clean finishes |
ProjectConfigurationChanged | Action<Project?> | When a project's active configuration changes |
SolutionConfigurationChanged | Action | When the solution configuration changes |
VS.Events.BuildEvents.SolutionBuildDone += async (bool succeeded) =>
{
if (!succeeded)
{
var model = new InfoBarModel(
"Build failed. Check the Error List for details.",
KnownMonikers.StatusError,
isCloseButtonVisible: true);
InfoBar infoBar = await VS.InfoBar.CreateAsync(model);
await infoBar.TryShowInfoBarUIAsync();
}
};
private System.Diagnostics.Stopwatch? _buildTimer;
VS.Events.BuildEvents.SolutionBuildStarted += (s, e) =>
{
_buildTimer = System.Diagnostics.Stopwatch.StartNew();
};
VS.Events.BuildEvents.SolutionBuildDone += async (bool succeeded) =>
{
_buildTimer?.Stop();
string status = succeeded ? "succeeded" : "failed";
await VS.StatusBar.ShowMessageAsync(
$"Build {status} in {_buildTimer?.Elapsed.TotalSeconds:F1}s");
};
Use IVsUpdateSolutionEvents2 with IVsSolutionBuildManager to receive build events. This gives you full control, including the ability to cancel a build in UpdateSolution_Begin.
NuGet package: Microsoft.VisualStudio.SDK
Key namespaces: Microsoft.VisualStudio.Shell, Microsoft.VisualStudio.Shell.Interop
public sealed class MyPackage : AsyncPackage, IVsUpdateSolutionEvents2
{
private uint _buildEventsCookie;
private IVsSolutionBuildManager2? _buildManager;
protected override async Task InitializeAsync(
CancellationToken cancellationToken,
IProgress<ServiceProgressData> progress)
{
await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
_buildManager = (IVsSolutionBuildManager2)
await GetServiceAsync(typeof(SVsSolutionBuildManager));
_buildManager.AdviseUpdateSolutionEvents(this, out _buildEventsCookie);
}
protected override void Dispose(bool disposing)
{
ThreadHelper.ThrowIfNotOnUIThread();
if (_buildEventsCookie != 0 && _buildManager != null)
{
_buildManager.UnadviseUpdateSolutionEvents(_buildEventsCookie);
_buildEventsCookie = 0;
}
base.Dispose(disposing);
}
// --- Solution-level events ---
public int UpdateSolution_Begin(ref int pfCancelUpdate)
{
// Called before any build actions. Set pfCancelUpdate = 1 to cancel.
return VSConstants.S_OK;
}
public int UpdateSolution_Done(int fSucceeded, int fModified, int fCancelCommand)
{
// fSucceeded: 1 if no builds failed
// fModified: 1 if any build succeeded
// fCancelCommand: 1 if the build was cancelled
return VSConstants.S_OK;
}
public int UpdateSolution_Cancel()
{
// Build was cancelled by the user
return VSConstants.S_OK;
}
// --- Per-project events ---
public int UpdateProjectCfg_Begin(
IVsHierarchy pHierProj, IVsCfg pCfgProj,
IVsCfg pCfgSln, uint dwAction, ref int pfCancel)
{
ThreadHelper.ThrowIfNotOnUIThread();
// dwAction values:
// 0x010000 = Build
// 0x100000 = Clean
// 0x410000 = Rebuild (clean + build)
pHierProj.GetProperty(
VSConstants.VSITEMID_ROOT,
(int)__VSHPROPID.VSHPROPID_Name,
out object nameObj);
string projectName = nameObj as string ?? "Unknown";
return VSConstants.S_OK;
}
public int UpdateProjectCfg_Done(
IVsHierarchy pHierProj, IVsCfg pCfgProj,
IVsCfg pCfgSln, uint dwAction, int fSuccess, int fCancel)
{
// fSuccess: 1 if the project built successfully
return VSConstants.S_OK;
}
// --- Events not typically needed ---
public int OnActiveProjectCfgChange(IVsHierarchy pIVsHierarchy)
{
// Active project or solution configuration changed
// pIVsHierarchy == null means solution-level config change
return VSConstants.S_OK;
}
public int UpdateSolution_StartUpdate(ref int pfCancelUpdate)
{
return VSConstants.S_OK;
}
}
The older EnvDTE.BuildEvents interface provides COM-style events. Keep a strong reference to the BuildEvents object to prevent garbage collection of the COM event sink.
public sealed class MyPackage : AsyncPackage
{
private EnvDTE.BuildEvents? _buildEvents; // Must keep a reference!
protected override async Task InitializeAsync(
CancellationToken cancellationToken,
IProgress<ServiceProgressData> progress)
{
await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
DTE2 dte = (DTE2)await GetServiceAsync(typeof(EnvDTE.DTE));
_buildEvents = dte.Events.BuildEvents;
_buildEvents.OnBuildBegin += OnBuildBegin;
_buildEvents.OnBuildDone += OnBuildDone;
_buildEvents.OnBuildProjConfigBegin += OnBuildProjConfigBegin;
_buildEvents.OnBuildProjConfigDone += OnBuildProjConfigDone;
}
private void OnBuildBegin(
EnvDTE.vsBuildScope scope, EnvDTE.vsBuildAction action)
{
// scope: vsBuildScopeSolution, vsBuildScopeProject, vsBuildScopeBatch
// action: vsBuildActionBuild, vsBuildActionRebuildAll, vsBuildActionClean, vsBuildActionDeploy
}
private void OnBuildDone(
EnvDTE.vsBuildScope scope, EnvDTE.vsBuildAction action)
{
// Build completed
}
private void OnBuildProjConfigBegin(
string project, string projectConfig, string platform, string solutionConfig)
{
// A specific project configuration started building
}
private void OnBuildProjConfigDone(
string project, string projectConfig, string platform,
string solutionConfig, bool success)
{
// A specific project configuration finished building
// 'success' indicates whether it built successfully
}
}
Important: Always keep a field reference to the
BuildEvents(or any DTE event) object. The COM runtime will garbage-collect the event sink if the only reference is local, causing events to silently stop firing.
UnadviseUpdateSolutionEvents) in Dispose when using the VSSDK approach to prevent leaks.ThreadHelper.ThrowIfNotOnUIThread() in event handlers that access IVsHierarchy properties.EnvDTE.BuildEvents in new extensions — prefer the Toolkit or IVsUpdateSolutionEvents2.VS.Events.BuildEvents (Toolkit) for the simplest subscription model.IVsUpdateSolutionEvents2 over IVsUpdateSolutionEvents — it adds per-project UpdateProjectCfg_Begin/Done methods.dwAction parameter in UpdateProjectCfg_Begin/Done distinguishes between Build (0x010000), Clean (0x100000), and Rebuild (0x410000).BuildEvents object (DTE) or the advise cookie (IVsSolutionBuildManager) in a class-level field.COMException or RPC_E_WRONG_THREAD: You're accessing IVsHierarchy or other COM objects from a background thread. Add ThreadHelper.ThrowIfNotOnUIThread() at the top of your handler.UpdateSolution_Done reports success but a project actually failed: Check fSucceeded and fCanceled parameters carefully. fSucceeded == 1 means no errors; fSucceeded == 0 with fCanceled == 0 means there were failures.IVsUpdateSolutionEvents (v1). Switch to IVsUpdateSolutionEvents2 which adds UpdateProjectCfg_Begin and UpdateProjectCfg_Done.IVsSolutionBuildManager returns stale state: Build state queries during event callbacks may not reflect the final state. If you need post-build results, defer your logic with await Task.Yield() or queue it after the callback returns.Do NOT perform long-running work synchronously in build event handlers — they run on the UI thread. Queue background work with
JoinableTaskFactory.RunAsyncand return immediately.
Do NOT use
EnvDTE.BuildEvents— legacy COM API that doesn't integrate with the modern threading model. UseVS.Events.BuildEvents(Toolkit) orIVsUpdateSolutionEvents2(VSSDK).
Do NOT forget to unsubscribe (
UnadviseUpdateSolutionEvents) when your package is disposed — leaked subscriptions cause memory leaks and crashes.
Do NOT hold only a local reference to DTE event objects — COM GC silently stops delivering events. Store in a class-level field.
npx claudepluginhub madskristensen/vs-agent-plugins --plugin vs-extensibility-skillsDetects solution open/close/load and project add/remove events in Visual Studio extensions. Covers IVsSolutionEvents, VS.Events.SolutionEvents, and workspace notifications across out-of-process and in-process APIs.
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.