Add custom tree nodes to Solution Explorer in Visual Studio extensions using the IAttachedCollectionSourceProvider MEF pattern. Works with VSSDK and VSIX Community Toolkit (in-process).
How this skill is triggered — by the user, by Claude, or both
Slash command
/vs-extensibility-skills:adding-solution-explorer-nodesThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Extensions can add custom nodes (virtual items) to the Solution Explorer tree using the `IAttachedCollectionSourceProvider` MEF pattern. This allows injecting nodes under the solution node, project nodes, or other existing hierarchy items.
Extensions can add custom nodes (virtual items) to the Solution Explorer tree using the IAttachedCollectionSourceProvider MEF pattern. This allows injecting nodes under the solution node, project nodes, or other existing hierarchy items.
Custom nodes let your extension surface non-file data directly in Solution Explorer — such as database tables, API endpoints, cloud resources, or dependency details — where developers already navigate. Without custom nodes, users must switch to a separate tool window or external tool to see this information, breaking their workflow.
When to use this vs. alternatives:
The new out-of-process extensibility model does not support adding custom nodes to Solution Explorer. The Project Query API provides read/write access to projects and files but cannot inject custom tree nodes into the Solution Explorer UI. Use the in-process MEF approach described below instead.
Both VSSDK and the Community Toolkit use the same MEF-based approach. The toolkit doesn't add any specific helpers for this scenario, so the implementation is identical regardless of which package you reference. The examples below use Community.VisualStudio.Toolkit helpers where convenient (e.g., VS.GetRequiredService, ThreadHelper), but the core MEF contracts come from VSSDK assemblies.
NuGet packages: Microsoft.VisualStudio.SDK (VSSDK) or Community.VisualStudio.Toolkit (which includes the SDK)
Key namespace: Microsoft.Internal.VisualStudio.PlatformUI
Adding custom nodes requires three pieces:
IAttachedCollectionSourceProvider — A MEF export that tells Solution Explorer which items support your custom nodes and creates the collection sources.IAttachedCollectionSource, ITreeDisplayItem, ITreeDisplayItemWithImages, and IInteractionPatternProvider that represent your custom nodes.IAttachedRelationship instances for Contains (parent→children) and optionally ContainedBy (child→parent, needed for search support).The source provider is the MEF entry point. It determines which existing items get your custom children and creates the collection sources.
using System.Collections.Generic;
using System.ComponentModel.Composition;
using Microsoft.Internal.VisualStudio.PlatformUI;
using Microsoft.VisualStudio.Utilities;
[Export(typeof(IAttachedCollectionSourceProvider))]
[Name(nameof(MyNodeSourceProvider))]
[Order(Before = HierarchyItemsProviderNames.Contains)]
internal class MyNodeSourceProvider : IAttachedCollectionSourceProvider
{
private MyRootNode _rootNode;
public IEnumerable<IAttachedRelationship> GetRelationships(object item)
{
// Add children under the solution node
if (item is IVsHierarchyItem hierarchyItem
&& HierarchyUtilities.IsSolutionNode(hierarchyItem.HierarchyIdentity))
{
yield return Relationships.Contains;
}
// Custom nodes can also have children and a parent (for search)
else if (item is MyItemNode)
{
yield return Relationships.Contains;
yield return Relationships.ContainedBy;
}
else if (item is MyRootNode)
{
yield return Relationships.Contains;
yield return Relationships.ContainedBy;
}
}
public IAttachedCollectionSource CreateCollectionSource(object item, string relationshipName)
{
if (relationshipName == KnownRelationships.Contains)
{
// When Solution Explorer asks for children of the solution node,
// return the root node (which is itself an IAttachedCollectionSource)
if (item is IVsHierarchyItem hierarchyItem
&& HierarchyUtilities.IsSolutionNode(hierarchyItem.HierarchyIdentity))
{
return _rootNode ??= new MyRootNode(hierarchyItem);
}
else if (item is MyItemNode node)
{
return node; // Node is its own collection source
}
}
else if (relationshipName == KnownRelationships.ContainedBy)
{
// ContainedBy enables Solution Explorer search to trace back to parents
if (item is MyItemNode node)
{
return new ContainedByCollection(node, node.ParentItem);
}
else if (item is MyRootNode rootNode)
{
return new ContainedByCollection(rootNode, rootNode.ParentItem);
}
}
return null;
}
}
You can also attach nodes under project nodes instead of (or in addition to) the solution node by checking for project hierarchy items:
public IEnumerable<IAttachedRelationship> GetRelationships(object item)
{
if (item is IVsHierarchyItem hierarchyItem
&& HierarchyUtilities.IsProjectNode(hierarchyItem.HierarchyIdentity))
{
yield return Relationships.Contains;
}
}
using Microsoft.Internal.VisualStudio.PlatformUI;
internal static class Relationships
{
public static IAttachedRelationship Contains { get; } = new ContainsRelationship();
public static IAttachedRelationship ContainedBy { get; } = new ContainedByRelationship();
private sealed class ContainsRelationship : IAttachedRelationship
{
public string Name => KnownRelationships.Contains;
public string DisplayName => KnownRelationships.Contains;
}
private sealed class ContainedByRelationship : IAttachedRelationship
{
public string Name => KnownRelationships.ContainedBy;
public string DisplayName => KnownRelationships.ContainedBy;
}
}
This simple collection enables the ContainedBy relationship for search support — it returns the parent(s) of a given child item.
using System.Collections;
using Microsoft.Internal.VisualStudio.PlatformUI;
internal sealed class ContainedByCollection(object child, object parent) : IAttachedCollectionSource
{
private readonly object[] _items = parent != null ? [parent] : [];
public object SourceItem { get; } = child;
public bool HasItems => _items.Length > 0;
public IEnumerable Items => _items;
}
The root node appears as a top-level item under the solution. It implements IAttachedCollectionSource so it can provide children, and display interfaces so Solution Explorer can render it.
using System.Collections;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows;
using Microsoft.Internal.VisualStudio.PlatformUI;
using Microsoft.VisualStudio.Imaging;
using Microsoft.VisualStudio.Imaging.Interop;
internal class MyRootNode :
IAttachedCollectionSource,
ITreeDisplayItem,
ITreeDisplayItemWithImages,
IInteractionPatternProvider,
IPrioritizedComparable,
IBrowsablePattern,
INotifyPropertyChanged
{
private readonly ObservableCollection<MyItemNode> _children = [];
private readonly IVsHierarchyItem _solutionHierarchyItem;
public MyRootNode(IVsHierarchyItem solutionHierarchyItem)
{
_solutionHierarchyItem = solutionHierarchyItem;
LoadChildren();
}
// ITreeDisplayItem
public string Text => "My Custom Items";
public string ToolTipText => "Custom items added by my extension";
public object ToolTipContent => null;
public FontWeight FontWeight => FontWeights.Normal;
public FontStyle FontStyle => FontStyles.Normal;
public bool IsCut => false;
// ITreeDisplayItemWithImages — use KnownMonikers for standard icons
public ImageMoniker IconMoniker => KnownMonikers.LinkedFolderOpened;
public ImageMoniker ExpandedIconMoniker => KnownMonikers.LinkedFolderOpened;
public ImageMoniker OverlayIconMoniker => default;
public ImageMoniker StateIconMoniker => default;
public string StateToolTipText => string.Empty;
// IAttachedCollectionSource
public object SourceItem => this;
public bool HasItems => _children.Count > 0;
public IEnumerable Items => _children;
// Parent for ContainedBy relationship (points to solution node)
public object ParentItem => _solutionHierarchyItem;
// IInteractionPatternProvider — declare which patterns this node supports
private static readonly HashSet<Type> _supportedPatterns =
[
typeof(ITreeDisplayItem),
typeof(ITreeDisplayItemWithImages),
typeof(IBrowsablePattern),
];
public TPattern GetPattern<TPattern>() where TPattern : class
{
return _supportedPatterns.Contains(typeof(TPattern)) ? this as TPattern : null;
}
// IPrioritizedComparable — controls sort order among sibling nodes
public int Priority => 0;
public int CompareTo(object obj) => 1; // Appear at end
// IBrowsablePattern
public object GetBrowseObject() => null;
// INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private void LoadChildren()
{
// Populate _children with your custom items
_children.Add(new MyItemNode(this, "Item 1"));
_children.Add(new MyItemNode(this, "Item 2"));
RaisePropertyChanged(nameof(HasItems));
}
}
Each child node also implements IAttachedCollectionSource (if it can have children) plus the display and interaction interfaces.
using System.Collections;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows;
using Microsoft.Internal.VisualStudio.PlatformUI;
using Microsoft.VisualStudio.Imaging;
using Microsoft.VisualStudio.Imaging.Interop;
internal class MyItemNode :
IAttachedCollectionSource,
ITreeDisplayItem,
ITreeDisplayItemWithImages,
IPrioritizedComparable,
IBrowsablePattern,
IInteractionPatternProvider,
IContextMenuPattern,
IInvocationPattern,
INotifyPropertyChanged
{
private ObservableCollection<MyItemNode> _children;
public MyItemNode(object parent, string displayText)
{
ParentItem = parent;
Text = displayText;
}
// ITreeDisplayItem
public string Text { get; }
public string ToolTipText => Text;
public object ToolTipContent => null;
public FontWeight FontWeight => FontWeights.Normal;
public FontStyle FontStyle => FontStyles.Normal;
public bool IsCut => false;
// ITreeDisplayItemWithImages
public ImageMoniker IconMoniker => KnownMonikers.StatusInformation;
public ImageMoniker ExpandedIconMoniker => KnownMonikers.StatusInformation;
public ImageMoniker OverlayIconMoniker => default;
public ImageMoniker StateIconMoniker => default;
public string StateToolTipText => string.Empty;
// IAttachedCollectionSource — node can have children
public object SourceItem => this;
public bool HasItems => _children?.Count > 0;
public IEnumerable Items => _children ??= [];
// Parent for ContainedBy support
public object ParentItem { get; }
// IContextMenuPattern — show context menu on right-click
public IContextMenuController ContextMenuController { get; } = new MyContextMenuController();
// IInvocationPattern — handle double-click / Enter
public IInvocationController InvocationController { get; } = new MyInvocationController();
// IInteractionPatternProvider
private static readonly HashSet<Type> _supportedPatterns =
[
typeof(ITreeDisplayItem),
typeof(ITreeDisplayItemWithImages),
typeof(IBrowsablePattern),
typeof(IContextMenuPattern),
typeof(IInvocationPattern),
];
public TPattern GetPattern<TPattern>() where TPattern : class
{
return _supportedPatterns.Contains(typeof(TPattern)) ? this as TPattern : null;
}
// IPrioritizedComparable
public int Priority => 0;
public int CompareTo(object obj) => 0;
// IBrowsablePattern
public object GetBrowseObject() => null;
// INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Context menu controller — shows a VS context menu when the user right-clicks the node:
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using Microsoft.Internal.VisualStudio.PlatformUI;
using Microsoft.VisualStudio;
using Microsoft.VisualStudio.Shell.Interop;
internal class MyContextMenuController : IContextMenuController
{
public bool ShowContextMenu(IEnumerable<object> items, Point location)
{
ThreadHelper.ThrowIfNotOnUIThread();
var nodes = items.OfType<MyItemNode>().ToList();
if (nodes.Count == 0) return false;
IVsUIShell shell = VS.GetRequiredService<SVsUIShell, IVsUIShell>();
Guid menuGroup = /* your package command group GUID */;
shell.ShowContextMenu(
0,
ref menuGroup,
/* your menu ID */,
new POINTS[] { new() { x = (short)location.X, y = (short)location.Y } },
pCmdTrgtActive: null);
return true;
}
}
Invocation controller — handles double-click or Enter on a node:
using System.Collections.Generic;
using System.Linq;
using Microsoft.Internal.VisualStudio.PlatformUI;
internal class MyInvocationController : IInvocationController
{
public bool Invoke(IEnumerable<object> items, InputSource inputSource, bool preview)
{
foreach (MyItemNode item in items.OfType<MyItemNode>())
{
// Handle activation — open a document, show a tool window, etc.
}
return true;
}
}
Drag-and-drop source controller — enables dragging nodes out of the tree:
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Windows;
using Microsoft.Internal.VisualStudio.PlatformUI;
internal class MyDragDropSourceController : IDragDropSourceController
{
public bool DoDragDrop(IEnumerable<object> items)
{
var nodes = items.OfType<MyItemNode>().ToArray();
if (!nodes.Any()) return false;
var dataObj = new DataObject();
var paths = new StringCollection();
// Add data to the DataObject based on your node type
// paths.AddRange(nodes.Select(n => n.FilePath).ToArray());
// dataObj.SetFileDropList(paths);
DependencyObject source = Application.Current.MainWindow;
DragDrop.DoDragDrop(source, dataObj, DragDropEffects.Copy | DragDropEffects.Move);
return true;
}
}
To enable drag-drop, add IDragDropSourcePattern to your node's interface list and _supportedPatterns, then expose the controller:
public IDragDropSourceController DragDropSourceController { get; } = new MyDragDropSourceController();
| Interface | Purpose |
|---|---|
IAttachedCollectionSourceProvider | MEF entry point — maps items to relationships and creates collection sources |
IAttachedCollectionSource | Provides Items and HasItems for a node in the tree |
ITreeDisplayItem | Display text, tooltip, font weight/style, cut state |
ITreeDisplayItemWithImages | Icons (collapsed, expanded, overlay, state) via ImageMoniker |
IInteractionPatternProvider | Declares which interaction patterns the node supports |
IPrioritizedComparable | Sort order among sibling nodes |
IBrowsablePattern | Properties window integration |
IContextMenuPattern | Right-click context menu |
IInvocationPattern | Double-click / Enter activation |
IDragDropSourcePattern | Drag items out of the tree |
IDragDropTargetPattern | Drop items onto the node |
IRefreshPattern | Enables the node to be refreshed |
ISupportDisposalNotification | Notification when node is disposed |
INotifyPropertyChanged | Standard WPF change notification for UI updates |
[Order(Before = HierarchyItemsProviderNames.Contains)] attribute on the source provider ensures your nodes are processed before the default hierarchy provider.ObservableCollection<T> (or BulkObservableCollection<T> for batch updates) for children so the tree updates automatically.INotifyPropertyChanged and raise PropertyChanged for HasItems and Items when the children collection changes.KnownMonikers from Microsoft.VisualStudio.Imaging for standard icons, or register custom image monikers.ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync() or ThreadHelper.ThrowIfNotOnUIThread().MefComponent asset type is in .vsixmanifest. Check the MEF error log at %LOCALAPPDATA%\Microsoft\VisualStudio\17.0_<id>\ComponentModelCache\Microsoft.VisualStudio.Default.err for composition failures.HasItems method isn't correctly identifying the parent item type. Use is type checks and verify the hierarchy item represents the expected node kind.ObservableCollection<T> for the children collection and raise PropertyChanged for HasItems and Items. Static List<T> won't trigger tree refresh.KnownMonikers usage with ImageCatalogGuid. Custom image monikers need registration via IImageServiceProvider.ITreeDisplayItem property accessors are called on the UI thread. Ensure they don't do async work or block — pre-compute values and return cached data.Do NOT use this with VisualStudio.Extensibility (out-of-process) —
IAttachedCollectionSourceProviderrequires in-process access; the out-of-process model has no equivalent.
Do NOT block in
HasItemsorItemsaccessors — they run on the UI thread during tree rendering. Load data asynchronously and update theObservableCollectionwhen ready.
Do NOT forget
[Order(Before = HierarchyItemsProviderNames.Contains)]on your source provider — without it, your nodes may not be processed.
npx claudepluginhub madskristensen/vs-agent-plugins --plugin vs-extensibility-skillsProgrammatically interacts with Solution Explorer in Visual Studio extensions for selecting items, expanding/collapsing nodes, filtering, and querying projects/files.
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.