Workspaces

A workspace is how Visual Studio represents any collection of files in Open Folder, and it's represented by the IWorkspace type. By itself, the workspace doesn't understand the contents or features related to files within the folder. Rather, it provides a general set of APIs for features and extensions to produce and consume data that others can act upon. The producers are composed through the Managed Extensibility Framework (MEF) using various export attributes.

Workspace providers and services

Workspace providers and services provide the data and functionality to react to the contents of a workspace. They might provide contextual file information, symbols in source files, or build functionality.

Both concepts use a factory pattern and are imported through MEF by the workspace. All export attributes implement IProviderMetadataBase or IWorkspaceServiceFactoryMetadata, but there are concrete types that extensions should use for exported types.

One difference between providers and services is their relation to the workspace. A workspace can have many providers of a particular type, but only one service of a particular type is created per workspace. For example, a workspace has many file scanner providers but the workspace has only one indexing service per workspace.

Another key difference is consumption of data from providers and services. The workspace is the entry point to get data from providers for a couple reasons. First, providers typically have some narrow set of data they create. The data might be symbols for a C# source file or build file contexts for a CMakeLists.txt file. The workspace will match a consumer's request to the providers whose metadata align with the request. Second, some scenarios allow for many providers to contribute to a request while others scenarios use the provider with highest priority.

In contrast, extensions can get instances of and interact directly with workspace services. Extension methods on IWorkspace are available for the services provided by Visual Studio, such as GetFileWatcherService. Your extension may offer a workspace service for components within your extension or for other extensions to consume. Consumers should use GetServiceAsync or an extension method you provide on the IWorkspace type.

Warning

Do not author services that conflict with Visual Studio. It can lead to unexpected issues.

Disposal on workspace closure

On closure of a workspace, extenders might need to dispose but call asynchronous code. The IAsyncDisposable interface is available to make writing this code easy.

Workspace settings

Workspaces have an IWorkspaceSettingsManager service with simple but powerful control over a workspace. For a basic overview of settings, see Customize build and debug tasks.

Settings for most SettingsType types are .json files, such as VSWorkspaceSettings.json and tasks.vs.json.

The power of workspace settings centers around "scopes", which are simply paths within the workspace. When a consumer calls GetAggregatedSettings, all the scopes that include the requested path and type of setting are aggregated. Scope aggregation priority is as follows:

  1. "Local settings", which is typically the workspace root's .vs directory.
  2. The requested path itself.
  3. The parent directory of the requested path.
  4. All further parent directories up to and including the workspace root.
  5. "Global settings", which is in a user directory.

The result is an instance of IWorkspaceSettings. This object holds the settings for a particular type, and can be queried for setting key names stored as string. The GetProperty methods and WorkspaceSettingsExtensions extension methods expect the caller to know the type of the setting value being requested. As most settings files are persisted as .json files, many invocations will use string, bool, int, and arrays of those types. Object types are also supported. In those cases, you can use IWorkspaceSettings itself as the type argument. For example:

{
  "intValue": 1,
  "stringValue": "s",
  "boolValue": true,
  "stringArray": [
    "s1",
    "s2"
  ],
  "nestedIWorkspaceSettings": {
    "nestedString": "ns"
  }
}

Assuming these settings were in a user's VSWorkspaceSettings.json, the data can be accessed as:

using System.Collections.Generic;
using Microsoft.VisualStudio.Workspace;
using Microsoft.VisualStudio.Workspace.Settings;

private static void ReadSettings(IWorkspace workspace)
{
    IWorkspaceSettingsManager settingsManager = workspace.GetSettingsManager();
    IWorkspaceSettings settings = settingsManager.GetAggregatedSettings(SettingsTypes.Generic);

    // result == WorkspaceSettingsResult.Success
    WorkspaceSettingsResult result = settings.GetProperty("intValue", out int intValue);
    result = settings.GetProperty("stringValue", out string stringValue);
    result = settings.GetProperty("boolValue", out bool boolValue);
    result = settings.GetProperty("stringArray", out string[] stringArray);
    result = settings.GetProperty("nestedIWorkspaceSettings", out IWorkspaceSettings nestedIWorkspaceSettings);
    result = nestedIWorkspaceSettings.GetProperty("nestedString", out string nestedString);

    // Extension method alternative using default values.
    int intValueOrDefault = settings.Property("intValue", /* default */ 42);

    // Missing key. result == WorkspaceSettingsResult.Undefined
    result = settings.GetProperty("missing", out string missing);

    // Wrong type for a key. result == WorkspaceSettingsResult.Error
    result = settings.GetProperty("intValue", out IWorkspaceSettings notSettings);

    // Special ability to union "stringArray" across all scopes.
    IEnumerable<string> allStringArray = settings.UnionPropertyArray<string>("stringArray");
}

Note

These settings APIs are unrelated to the APIs available in the Microsoft.VisualStudio.Settings namespace. Workspace settings are agnostic of the host and use workspace-specific settings files or dynamic settings providers.

Providing dynamic settings

Extensions can provide IWorkspaceSettingsProviders. These in-memory providers allow extensions to add settings or override others.

Exporting an IWorkspaceSettingsProvider is different than other workspace providers. The factory is not IWorkspaceProviderFactory and there is no special attribute type. Instead, implement IWorkspaceSettingsProviderFactory and use [Export(typeof(IWorkspaceSettingsProviderFactory))].

// Common workspace provider factory pattern
[ExportFeatureProvider(some, args, to, export)]
internal class MyProviderFactory : IWorkspaceProviderFactory<IFeatureProvider>
{
     IFeatureProvider CreateProvider(IWorkspace workspace) => new Provider(workspace);
}

// IWorkspaceSettingsProvider pattern
[Export(typeof(IWorkspaceSettingsProviderFactory))]
internal class MySettingsProviderFactory : IWorkspaceSettingsProviderFactory
{
    // 100 is typically the value used by built-in settings providers. Lower value is higher priority.
    int Priority => 100;

    IWorkspaceSettingsProvider CreateSettingsProvider(IWorkspace workspace) => new MySettingsProvider(workspace);
}

Tip

When implementing methods that return IWorkspaceSettingsSource (like IWorkspaceSettingsProvider.GetSingleSettings), return an instance of IWorkspaceSettings rather than IWorkspaceSettingsSource. IWorkspaceSettings provides more information that can be useful during some settings aggregations.

Workspace suggested practices

  • Return objects from IWorkspaceProviderFactory.CreateProvider or similar APIs that remember their Workspace context when created. Providers interfaces are written expecting this object is kept on creation.
  • Save workspace-specific caches or settings within the "Local settings" path of the workspace. Create a path for your file using Microsoft.VisualStudio.Workspace.WorkspaceHelper.MakeRootedUnderWorkingFolder in Visual Studio 2017 version 15.6 or later. For versions prior to version 15.6, use the following snippet:
using System.IO;
using Microsoft.VisualStudio.Workspace;
using Microsoft.VisualStudio.Workspace.Settings;

private static string MakeRootedUnderWorkingFolder(IWorkspace workspace, string relativePath)
{
    string workingFolder = workspace.GetSettingsManager().GetAggregatedSettings(SettingsTypes.WorkspaceControlSettings).Property<string>("WorkingFolder");
    return Path.Combine(workingFolder, relativePath);
}

Solution events and package auto-load

Loaded packages can implement IVsSolutionEvents7 and invoke IVsSolution.AdviseSolutionEvents. It includes eventing on opening and closing a folder in Visual Studio.

A UI context can be used to auto-load your package. The value is 4646B819-1AE0-4E79-97F4-8A8176FDD664.

Troubleshooting

The SourceExplorerPackage package did not load correctly

Workspace extensibility is heavily MEF-based, and composition errors will cause the package hosting Open Folder to fail to load. For example, if an extension exports a type with ExportFileContextProviderAttribute, but the type only implements IWorkspaceProviderFactory<IFileContextActionProvider>, an error will occur when trying to open a folder in Visual Studio.

Error details can be found in %LOCALAPPDATA%\Microsoft\VisualStudio\16.0_id\ComponentModelCache\Microsoft.VisualStudio.Default.err. Resolve any errors for types implemented by your extension.

  • File contexts - File context providers bring code intelligence for Open Folder workspaces.
  • Indexing - Workspace indexing collects and persists information about the workspace.