Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Go Back Add-in for VS.NET 2003

0.00/5 (No votes)
19 Mar 2005 1  
Creates a Visual Studio 2003 add-in to return back to a previous location.

New context menu with GoBackAddin installed

Contents

Introduction

When working on large projects, I frequently find myself jumping around to various locations in numerous files (I use the Go To Definition context menu command all the time). Once I have navigated down a particular path I want to get back to my original starting position. While I could use bookmarks to mark my place before I leave to chase something down, I tend to use bookmarks for other purposes (plus bookmarks only work in the context of the containing source file - you can't Edit.NextBookmark between source files). It has been pointed out to me that Visual Studio 7 has the command Navigate Backward which will do this, however, I find that it has some quirks that I don't like.

This add-in provides a more convenient Navigate Backward mechanism. It monitors navigations away from the current document position and provides a means to go back to the previous position or to any position in a chain of navigations. I'm defining a document position as the document (i.e., source code file) file name, and the line and column number of the text insertion cursor in that document.

Background

Visual Studio provides a fast mechanism to jump around in the code with the Go To Definition and Go To Reference context menu commands. There are also a number of other commands that can change the current active editing location in a source file (e.g., Edit.DocumentEnd, Edit.NextBookmark, Edit.GotoNextLocation, etc.). In Visual Basic 6, there was a "Previous Location" context menu command that could be selected to return you right back to where you had been. Unfortunately, the editing context menu in Visual Studio 2002 and 2003 (and the 2005 Beta) do not include that support. There is an Edit.GoToPreviousLocation command defined, but unfortunately that's used for moving to a previously marked location (e.g., from a Find in Files text search).

The Navigate Backward command is the replacement for the "Previous Location" command in VB6. It is an improvement, but has several quirks (that I don't like). Specifically:

  • It is not available on the editing context menu (so it's either move the mouse pointer up to the top of the VS window to the standard toolbar or take your hand off the mouse to type 'Ctrl+-'), which is inconvenient.
  • Every text insertion point is recorded, so the navigation history gets rather long as you click around in a source file (which I don't really consider all that helpful - I want the major moves, not every little minor move).
  • When code navigation opens a source file and then you Navigate Backward, you have to select that command twice to get back to where you really were. This is a consequence of recording every text insertion change (when a new file is opened, the beginning of the file is recorded as a navigation location even though visually you see some other location).
  • A file that was opened due to a navigation is left open, when you return from it (navigate backward).

Using the code

Once you have loaded the add-in, the context menu in the code window is modified to add two additional commands - Previous Location and Choose Location .... These menu items are enabled based on your code navigation actions. My preference is to have consistent menus, so the menu items are enabled/disabled rather than to have them appear/disappear as a function of command availability. NOTE: If you do not set the option to load the add-in at startup, the menu states will appear as enabled. When you select one of those commands, the add-in gets loaded and Visual Studio will do a QueryStatus check on each command and then they will correctly show as being disabled.

No return is possible Return to previous location is possible Return to one of many previous locations is possible

No navigation history exists.

A single navigation location exists.

Two or more navigation locations exist. Go back to the most recent location or choose from a list.

If Previous Location is enabled and then selected, the current edit position is restored back to a remembered position. The document being left will be closed if it was opened as a result of code navigation (and then not modified). My preference is to only have documents open that I'm currently working with and I consider it a convenience to automatically close a document that was only opened up for a "reference" check.

If Choose Location... is enabled and then selected, the PositionDialog form will be displayed (see the picture below). It shows the history of document locations that have been left due to a navigation command. The most recent location is shown first. Clicking on any row results in a selection and a return to that position.

The list of document positions that you have left and can return to.

The PositionDialog also allows you to clear the recorded position history. The "Close Skipped Over Documents" check box determines if any documents that were opened along the way get closed automatically on the way back.

About the code

The skeleton of the add-in was created by the Visual Studio add-in project wizard (specifically the Connect class). There are many articles on how to write an add-in for Visual Studio, so I won't attempt to go over those basics here.

Initially, I thought I could simply use a handler for the Text Editor LineChanged event. The event handler is passed a value indicating the nature of the change (see the MSDN table of values). The vsTextChangedCaretMoved value (the insertion point was moved) suggested to me that this would work nicely. However, the LineChanged event only fires when the text of the line has been changed and you move away from that line. It's probably just as well that this doesn't work the way I hoped - it would end up with a lot of events firing and a navigation history just like the Navigate Backward command.

Reading around news group postings, I found someone else who was trying to tackle a similar problem and was able to get around it by putting in place BeforeExecute and AfterExecute handlers for certain commands. That's the approach I settled on, and for the most part it works out quite well. In the OnConnection method, I iterate through a list of editor commands that I want to be notified when they are executed. For each command I get its corresponding EnvDTE.CommandEvents and register a BeforeExecute and AfterExecute handler.

for( int i = 0; i < _interceptCommandNames.Length; i++ )
{
    EnvDTE.Command cmd = _vsNet.Commands.Item( _interceptCommandNames[i], -1 );
    if( null != cmd )
    {
        _commandEvents[i] = _vsNet.Events.get_CommandEvents( cmd.Guid, cmd.ID );
        _commandEvents[i].BeforeExecute += 
    new _dispCommandEvents_BeforeExecuteEventHandler( BeforeExecute );
        _commandEvents[i].AfterExecute +=
           new _dispCommandEvents_AfterExecuteEventHandler( AfterExecute );
    }
}    

Initially I just used the BeforeExecute handler which just simply recorded the current document position. However there are situations where code navigation is requested but no actual navigation occurs. So the AfterExecute handler is used to verify that, the current document position has changed before the cached position location is recorded. The PositionManager class (the _pm instance variable in the code below) does all the handling of document positions, which are stored internally using System.Collections.Stack. It in turn relies on the DocumentPosition data class which encapsulates the position information.

private void BeforeExecute( string   Guid,
                            int      ID,
                            object   CustomIn,
                            object   CustomOut,
                            ref bool CancelDefault)
{
    // Don't indicate that we've handled this command event.

    CancelDefault = false;

    try
    {
        _pm.CacheCurrentPosition();
    }
    catch( Exception ex )
    {
        DisplayInOutputWindow( ex.ToString() );
    }
}


private void AfterExecute(  string   Guid,
                            int      ID,
                            object   CustomIn,
                            object   CustomOut )
{
    try
    {
        if( _pm.PositionHasChanged )
        {
            _pm.RecordCachedPosition();
        }
        else
        {
            _pm.ClearCachedPosition();
        }
    }
    catch( Exception ex )
    {
        DisplayInOutputWindow( ex.ToString() );
    }
}

The PositionManager.RecordCachedPosition method performs the check to see if the current document location required the opening of a document. If so, it sets the OpenedDocumentName property so that document can be closed on a return.

public void CacheCurrentPosition()
{
    _position = GetCurrentPosition();
    _openedDocumentCount = _vsNet.Documents.Count;
}

public void RecordCachedPosition()
{
    //Since this method should be called after some code navigation

    //    event, any change in the document count means that a new

    //    document was opened as a result of the navigation.  That means

    //    its not a working document and can be closed (if its not later

    //    modified) when returning back to the original location.

    if( _openedDocumentCount != _vsNet.Documents.Count )
    {
        DocumentPosition newPosition = GetCurrentPosition();
        _position.OpenedDocumentName = newPosition.DocumentName;
    }
    _positionStack.Push( _position );
}

A selection for either of the Previous Location and Choose Location ... commands results in a call to the Connect.Exec method which has to make a decision as to which command is actually being processed and then act accordingly. The relevant code from the method is shown below.

bool closeOnReturn = true;
DocumentPosition position = null;
if( COMMAND_PREVIOUS_LOCATION == commandName )
{
    position = _pm.PreviousPosition;
}
if( COMMAND_CHOOSE_LOCATION == commandName )
{
    PositionDialog pd = new PositionDialog();
    pd.InitializeForm( _pm );
    if( DialogResult.OK == pd.ShowDialog() )
    {
        position = pd.SelectedPosition;
        closeOnReturn = pd.CloseSkippedDocuments;
    }
}

if( null != position )
{
    MoveToPosition( position, closeOnReturn );
}
handled = true;

The Connect.MoveToPosition method is pretty straight forward. It handles the closing of a document on return. The only trick has to do with calling MoveToDisplayColumn with the line and column numbers. This results in the cursor being placed in the correct location (otherwise you end up with the column number being treated as the character count from the beginning of the line).

private void MoveToPosition( DocumentPosition position, bool closeDocument )
{
    if( closeDocument && ( 0 < position.OpenedDocumentName.Length ) )
    {
        Document leavingDoc = _vsNet.Documents.Item( position.OpenedDocumentName );
        if( ( null != leavingDoc ) && leavingDoc.Saved )
        {
            leavingDoc.Close( EnvDTE.vsSaveChanges.vsSaveChangesPrompt );
        }
    }

    //Move to the specified position.  First activate the document, and

    //    then move to the original line number and column number.

    Document doc = _vsNet.Documents.Item( position.DocumentName );
    if( null != doc )
    {
        doc.Activate();
        TextSelection ts = (TextSelection)_vsNet.ActiveDocument.Selection;
        ts.MoveToDisplayColumn( position.LineNumber,
                                position.ColumnNumber,
                                false );
    }
}

Limitations

There are code navigation mechanisms in Visual Studio whose associated event is not currently captured. Should you navigate away from your current edit position using one of those mechanisms, there is no intercept event mechanism to capture the current location before moving to the desired position (and therefore this add-in will not be able to return you back to your starting position). This can result in a misleading situation where this add-in's menu items have been enabled by an earlier code navigation event and selecting PreviousPosition would put you back to an earlier document position and not the one you thought you were going back to.

Specifically, the navigation events not (currently) captured are:

  • Clicking on the "Find Next" button of the Find dialog (while the Edit.FindNext command event is captured, that command doesn't seem to be executed with the button click).
  • Selecting a name in the Member Definition combo box.
  • Clicking on a find window result or a task list item.

Lessons Learned

I found that developing a VS.NET add-in user interface is really inconvenient (specifically the toolbar commands - the dialog form was no different than a regular WinForm application). The add-in's user interface controls are created once (during add-in setup) which I was unable to set a breakpoint for. Once created, there is no convenient mechanism to then remove those toolbar buttons. What I ended up having to do was:

  • Use the Add-in Manager and unload the add-in and exit Visual Studio.
  • Delete the add-in DLL file.
  • Restart Visual Studio and click on (each of) the command button(s) on which the add-in is installed. Visual Studio will complain about the add-in not functioning and asks if you want to remove the add-in (click on OK).
  • Re-create the add-in's registry key and values. (HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\7.1\AddIns\GoBackAddin.Connect for this add-in) and then re-run the ReCreateCommands.reg file created by Visual Studio.
  • Restart Visual Studio and load the add-in through the Add-in Manager
  • When that doesn't work, use the command devenv /setup in a command window.

Revision History

  • 2005-03-20:

    Changed the introductory text to reflect feedback from mav.northwind.

  • 2005-03-19:

    Initial version.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here