Search code examples
c#visual-studio-2019mefvisual-studio-extensions

How to get active IWpfTextView in VS2019 extension (MEF)


I'm trying to get the active C# Editor IWpfTextView in VS2019 extension. I'm using a small MEF service to inject a view into the VSAsyncPackage. But it is not very reliable - sometimes the injected view is wrong (e.g. from another view) or missing. Here is the service:

public interface IActiveViewAccessor
{
    IWpfTextView? ActiveView { get; }
}

[Export(typeof(IWpfTextViewConnectionListener))]
[Export(typeof(IActiveViewAccessor))]
[ContentType("text")]
[TextViewRole(PredefinedTextViewRoles.Document)]
internal sealed class ActiveViewConnectionListener : IWpfTextViewConnectionListener, IActiveViewAccessor
    {
    public IWpfTextView? ActiveView { get; private set; }

    public void SubjectBuffersConnected(IWpfTextView textView, ConnectionReason reason, Collection<ITextBuffer> subjectBuffers)
    {
        this.ActiveView = textView;
    }

    public void SubjectBuffersDisconnected(IWpfTextView textView, ConnectionReason reason, Collection<ITextBuffer> subjectBuffers)
    {
        this.ActiveView = null;
    }
}

This service is being injected into the VSPackage as:

this.viewAccessor = this.exportProvider.GetExportedValue<IActiveViewAccessor>();

and it is being used as:

var view = this.viewAccessor?.ActiveView;

Is there a better and more stable way to get IWpfTextView in the async VSPackage?

So far, here are some related questions but not exactly what I'd expected:

  1. How to get IWpfTextView from command Visual Studio Extension 2017 (2017)
  2. How can I get IWpfTextView for EnvDte.ActiveDocument? (2013)
  3. Access current code pane in Visual Studio Extension (2012)

Solution

  • After a bit of debugging and exploring, I concluded that my initial approach is very naive. Because IWpfTextViewConnectionListener is only firing once per editor window in a simple case, and it's not firing during switching between already connected views.

    After experimenting with it and sneaking into VsVim > VsAdapter.cs I've changed IActiveViewAccessor to the this:

    public interface IActiveViewAccessor
    {
        IWpfTextView? ActiveView { get; }
    }
    
    [Export(typeof(IActiveViewAccessor))]
    internal sealed class ActiveViewAccessor : IActiveViewAccessor
    {
        private readonly SVsServiceProvider serviceProvider;
        private readonly IVsEditorAdaptersFactoryService editorAdaptersFactoryService;
    
        [ImportingConstructor]
        public ActiveViewAccessor(
            SVsServiceProvider vsServiceProvider,
            IVsEditorAdaptersFactoryService editorAdaptersFactoryService)
        {
            this.serviceProvider = vsServiceProvider;
            this.editorAdaptersFactoryService = editorAdaptersFactoryService;
        }
    
        public IWpfTextView? ActiveView
        {
            get
            {
                IVsTextManager2 textManager =
                    serviceProvider.GetService<SVsTextManager, IVsTextManager2>();
    
                if (textManager == null)
                {
                    return null;
                }
    
                int hr = textManager.GetActiveView2(
                    fMustHaveFocus: 1,
                    pBuffer: null,
                    grfIncludeViewFrameType: (uint)_VIEWFRAMETYPE.vftCodeWindow,
                    ppView: out IVsTextView vsTextView);
    
                if (ErrorHandler.Failed(hr))
                {
                    return null;
                }
    
                return editorAdaptersFactoryService.GetWpfTextView(vsTextView);
            }
        }
    }