Feature Service is an API that offers a standardized way to disable various Visual Studio features.
Possible uses are:
- Inline rename to disable interactive popups
- Multi caret to disable completion
- Alternative completion engine to disable VS completion
Previously, extenders wishing to disable a Visual Studio features resorted to solutions that caused unwanted side effects and created a need from VS engineers to maintain these hacks.
Feature Service provides an extensible way to disable features or groups of features in specific scope or throughout the application.
- By disabling a group of features (e.g. popups), the extender does not need to revisit their code in the future when Visual Studio introduces a new feature which is also a popup.
- The scope is indicated by mutually agreed upon
IPropertyOwner
s. Currently, Completion checks its availability in their respectiveITextView
s, which implementIPropertyOwner
. In the future, we plan to check availability for respective content types. - The API is defined in
Microsoft.VisualStudio.Utilities
FeatureDefinition
FeatureDefinition
is a MEF part that registers a feature, later accessed by string. Applicable metadata:
- Required
Name(string)
, used to disable the feature or check its status - Optional
BaseDefinition(string)
, which means that feature is disabled if its base feature is disabled
We provide a few feature names in Microsoft.VisualStudio.Utilities.PredefinedEditorFeatureNames
:
Editor
, Popup
, InteractivePopup
, Completion
and AsyncCompletion
. This list will grow as we onboard more features.
IFeatureServiceFactory
IFeatureServiceFactory
is a MEF part used to obtain instances of IFeatureService
GlobalFeatureService
returns VS-wideIFeatureService
GetOrCreate(IPropertyOwner scope)
returnsIFeatureService
applicable to given scope, for exampleITextView
IFeatureController
IFeatureController
is an empty interface used to uniquely identifies someone who attempts to disable a feature. Its role is to prevent enabling of feature disabled by another controller.
IFeatureService
IFeatureService
is used to disable\restore a feature and to check its availability.
-
The service tracks disabled features in its scope (if obtained via
IFeatureServiceFactory.GetOrCreate(IPropertyOwner scope)
, unless this is aIFeatureServiceFactory.GlobalFeatureService
- In implementation of each feature, we test if it is available in its closest scope, e.g. an
ITextView
. If it is available, we query the parent services, until the global feature service is reached. Only if feature is available in all scopes, VS answers that it is available.
- In implementation of each feature, we test if it is available in its closest scope, e.g. an
-
IsEnabled(string featureName)
returns whether a feature or its base is disabled in this scope or parent scope.- This traverses the (small) hierarchy of scopes to produce an answer
-
Disable(string featureName, IFeatureController controller)
disables a feature- Returns a disposable
IFeatureDisableToken
. Disposing it cancels the request to disable.
- Returns a disposable
-
EventHandler<FeatureUpdatedEventArgs> StateUpdated
notifies when a feature or its base was disabled or restored in this scope or parent scope.- We don't recommend using it, though, as it is used for internal messaging and is raised even if the state of the feature ultimately has not changed (because of base features or other scopes overriding this scope's state)
-
GetCookie(string featureName)
produces a cookie with O(1) access to the feature state
IFeatureCookie
EventHandler<FeatureChangedEventArgs> StateChanged
notifies when the disabled state of the feature changed.IsEnabled
offers O(1) access to the valueFeatureName
Sample code:
Defining available features:
[Export]
[Name(nameof(MyFeature))] // required
[BaseDefinition(PredefinedEditorFeatureNames.Popup)] // zero or more BaseDefinitions are allowed
public FeatureDefinition MyFeature;
Disabling a feature
In this example, TextViewCreated
disables participating popups in specific ITextView, and OperationWithoutInterference
disables all participating popups for the duration of an operation.
[Export(typeof(IWpfTextViewCreationListener))]
[ContentType("CSharp")]
[TextViewRole(PredefinedTextViewRoles.Editable)]
class PopupDisabler : IWpfTextViewCreationListener, IFeatureController
{
#region Imports
[Import]
private IFeatureServiceFactory FeatureServiceFactory;
#endregion
#region IWpfTextViewCreationListener
public void TextViewCreated(IWpfTextView textView)
{
var token = FeatureServiceFactory.GetOrCreate(textView).Disable(PredefinedEditorFeatureNames.Popup, this);
// All participating popups pertinent to this textView are now disabled
textView.Closed += (sender, args) => token.Dispose(); // Enable popups
}
public void OperationWithoutInterference()
{
using (FeatureServiceFactory.GlobalFeatureService.Disable(PredefinedEditorFeatureNames.Popup, this))
{
// All participating popups are now disabled
}
// All participating popups are now enabled
}
}
Checking feature state:
In this example, we check for availability of Completion. The base feature of Completion is Popup, and we will assume that code from previous example disables the popups.
[Import]
IFeatureServiceFactory FeatureServiceFactory;
IFeatureService localService = FeatureServiceFactory.GetOrCreate(scope); // scope is an IPropertyOwner, e.g. ITextView
var cookie = localService.GetCookie(PredefinedEditorFeatureNames.Completion)
// Interact with the <see cref="IFeatureService"/>:
var test1 = localService.IsEnabled(PredefinedEditorFeatureNames.Completion); // returns false, because Popup is a base definition of Completion and because global scope is a superset of local scope.
var test2 = cookie.IsEnabled; // also returns false. This value is always up to date
cookie.StateChanged(sender, args) => React(args.IsEnabled);