What Seems Simple, Rarely Is

I recently tackled the problem of reliably detecting code window lifecycle changes in Visual Studio 2005 using the VSIP SDK. For something that seems like it should be very straightforward, it wasn’t at all. There’s even a page in the SDK help that tries to describe how to do this [link] but misses several of the possible scenarios. My goal was to create a class that would be able to fire off “Created”, “GotFocus” and “LostFocus” and “Closed” events for the lifecycle of any code window. Remember, we don’t care if an editor loses focus to a docked tool window, we only care if it loses focus to another tabbed document (in which case it’s hidden).

In order to do this, the class needs to implement two interfaces: IVsRunningDocumentTableEvents and IVsSelectionEvents. The first interface will notify our class about changes to the status of all open documents. The second will notify us when Visual Studio detects a selection change of any kind, more on this later. Our first step is to have our class implement these interfaces and register for callbacks on them, which is described in the SDK help documentation.

Windows vs. Code Editors

When I said that IVsRunningDocumentTableEvents tracks open documents, it’s important to understand what that means because it effects the implementation. Open documents include code editors and designers, but NOT tabbed tool windows. Because of that, we can’t use IVsRunningDocumentTableEvents to reliably tell us when an editor has lost focus because it doesn’t track tabbed tool windows.

Implementation

We’re interested in code editors only. When we get a call to our IVsRunningDocumentTableEvents.OnBeforeDocumentWindowShow implementation, the way to detect a valid code editor is to call VsShellUtilities.GetTextView and pass in the value of the pFrame parameter. If you get a valid IVsTextView object back, we have an editor. Inside this function, we are specifically interested in editor creation. If the editor was just created, the fFirstShow parameter will be 1. When this is the case, we can call our “Created” event to notify that a code editor was created. The OnAfterDocumentWindowHide is only called when a window is closed, not when it loses focus. When this is the called, we can call our “Closed” event to notify that a code editor was closed. At this point, you have a good implementation for detecting the opening and closing of editor windows.

We still want to know when the editors get focus or lose focus. To determine this, we need to implement the IVsSelectionEvents interface, specifically the OnElementValueChanged function. This function will be called if the “selection”, or active object, changes to anything.

To determine a change in editor focus:

  1. Check if the elementID parameter is equal to Constants.SEID_WindowFrame. If not, just return.
  2. Determine if the varValueNew parameter implements IVsWindowFrame. If so, cast it.
  3. We are only interested in when we lose focus to another tabbed document, so get the __VSFPROPID.VSFPROPID_FrameMode property of the window frame. If it is VSFRAMEMODE.VSFM_MdiChild, then it is a tabbed document. If it’s not a tabbed document, then just return.
  4. Call VsShellUtilities.GetTextView and pass in the value of the varValueNew parameter. If you get a valid IVsTextView object back, then an editor window has gotten focus and you can call the “GotFocus” event.
  5. Determine if the varValueOld parameter implements IVsWindowFrame. If so, cast it.
  6. Call VsShellUtilities.GetTextView and pass in the value of the varValueOld parameter. If you get a valid IVsTextView object back, then that editor window has lost focus and you can call the “LostFocus” event.

This can obviously be extended in many ways, but it’s a good basis for code window tracking and something that definitely isn’t immediately intuitive when working with VSIP.