| name | adding-solution-explorer-nodes |
| description | Add custom nodes to Solution Explorer in Visual Studio extensions using MEF. Use when the user asks about adding custom tree nodes, virtual items, workspace files, or attached collections to the Solution Explorer tree view. Covers the IAttachedCollectionSourceProvider MEF pattern used by both VSSDK and VSIX Community Toolkit (in-process). VisualStudio.Extensibility (out-of-process) does not support this scenario. |
Adding Custom Nodes to Solution Explorer
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:
- Add virtual (non-file) child nodes under existing Solution Explorer items → Custom nodes (this skill)
- Query or select existing projects/files in Solution Explorer → vs-solution-explorer
- Add context menu actions on existing nodes → vs-context-menu
- Show extension-specific content in a separate panel → vs-tool-window
VisualStudio.Extensibility (out-of-process) — Not Supported
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.
VSSDK and VSIX Community Toolkit (in-process, MEF)
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
Architecture overview
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.
- Node classes — Classes implementing
IAttachedCollectionSource, ITreeDisplayItem, ITreeDisplayItemWithImages, and IInteractionPatternProvider that represent your custom nodes.
- Relationships —
IAttachedRelationship instances for Contains (parent→children) and optionally ContainedBy (child→parent, needed for search support).
Step 1: Create the source provider
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)
{
if (item is IVsHierarchyItem hierarchyItem
&& HierarchyUtilities.IsSolutionNode(hierarchyItem.HierarchyIdentity))
{
yield return Relationships.Contains;
}
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)
{
if (item is IVsHierarchyItem hierarchyItem
&& HierarchyUtilities.IsSolutionNode(hierarchyItem.HierarchyIdentity))
{
return _rootNode ??= new MyRootNode(hierarchyItem);
}
else if (item is MyItemNode node)
{
return node;
}
}
else if (relationshipName == KnownRelationships.ContainedBy)
{
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;
}
}
Step 2: Define the relationship helpers
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;
}
}
Step 3: Create the ContainedBy collection
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;
}
Step 4: Create the root node
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();
}
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;
public ImageMoniker IconMoniker => KnownMonikers.LinkedFolderOpened;
public ImageMoniker ExpandedIconMoniker => KnownMonikers.LinkedFolderOpened;
public ImageMoniker OverlayIconMoniker => default;
public ImageMoniker StateIconMoniker => default;
public string StateToolTipText => string.Empty;
public object SourceItem => this;
public bool HasItems => _children.Count > 0;
public IEnumerable Items => _children;
public object ParentItem => _solutionHierarchyItem;
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;
}
public int Priority => 0;
public int CompareTo(object obj) => 1;
public object GetBrowseObject() => null;
public event PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private void LoadChildren()
{
_children.Add(new MyItemNode(this, "Item 1"));
_children.Add(new MyItemNode(this, "Item 2"));
RaisePropertyChanged(nameof(HasItems));
}
}
Step 5: Create the child node
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;
}
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;
public ImageMoniker IconMoniker => KnownMonikers.StatusInformation;
public ImageMoniker ExpandedIconMoniker => KnownMonikers.StatusInformation;
public ImageMoniker OverlayIconMoniker => default;
public ImageMoniker StateIconMoniker => default;
public string StateToolTipText => string.Empty;
public object SourceItem => this;
public bool HasItems => _children?.Count > 0;
public IEnumerable Items => _children ??= [];
public object ParentItem { get; }
public IContextMenuController ContextMenuController { get; } = new MyContextMenuController();
public IInvocationController InvocationController { get; } = new MyInvocationController();
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;
}
public int Priority => 0;
public int CompareTo(object obj) => 0;
public object GetBrowseObject() => null;
public event PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Step 6: Implement interaction controllers (optional)
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 = ;
shell.ShowContextMenu(
0,
ref menuGroup,
,
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>())
{
}
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();
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();
Key interfaces reference
| 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 |
Important notes
- The
[Order(Before = HierarchyItemsProviderNames.Contains)] attribute on the source provider ensures your nodes are processed before the default hierarchy provider.
- Use
ObservableCollection<T> (or BulkObservableCollection<T> for batch updates) for children so the tree updates automatically.
- Implement
INotifyPropertyChanged and raise PropertyChanged for HasItems and Items when the children collection changes.
- Use
KnownMonikers from Microsoft.VisualStudio.Imaging for standard icons, or register custom image monikers.
- All UI-thread access must go through
ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync() or ThreadHelper.ThrowIfNotOnUIThread().
Troubleshooting
- Custom nodes don't appear at all: Verify the
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.
- Nodes appear under wrong parent: Your
HasItems method isn't correctly identifying the parent item type. Use is type checks and verify the hierarchy item represents the expected node kind.
- Tree doesn't update when data changes: Use
ObservableCollection<T> for the children collection and raise PropertyChanged for HasItems and Items. Static List<T> won't trigger tree refresh.
- Icons are missing or show generic placeholder: Verify
KnownMonikers usage with ImageCatalogGuid. Custom image monikers need registration via IImageServiceProvider.
- Clicking a custom node throws on UI thread access: Your
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.
What NOT to do
Do NOT use this with VisualStudio.Extensibility (out-of-process) — IAttachedCollectionSourceProvider requires in-process access; the out-of-process model has no equivalent.
Do NOT block in HasItems or Items accessors — they run on the UI thread during tree rendering. Load data asynchronously and update the ObservableCollection when ready.
Do NOT forget [Order(Before = HierarchyItemsProviderNames.Contains)] on your source provider — without it, your nodes may not be processed.
See also
Additional resources