8 Feature Service API
Amadeusz Wieczorek редактировал(а) эту страницу 2019-02-28 09:42:17 -08:00

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 IPropertyOwners. Currently, Completion checks its availability in their respective ITextViews, which implement IPropertyOwner. 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-wide IFeatureService
  • GetOrCreate(IPropertyOwner scope) returns IFeatureService applicable to given scope, for example ITextView

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 a IFeatureServiceFactory.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.
  • 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.
  • 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 value
  • FeatureName

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);