Visitor pattern navigation support with ReSharper

I love the visitor pattern, it enables open/closed principle which is a great fundamental part of maintainability and clean code. You can read more about the pattern here. There is one down side of this pattern, and that is navigation. Consider this code.

_cqsClient.ExecuteCommand(new MyCommand());

If you navigate to ExecuteCommand you will just end up at some close to the metal code that executes your command handlers. And if you try to find all usages for the Command you will only find usages of its constructor (becasuse of the new keyword).

With vanilla ReSharper you need to first navigate to the class and then do a find all usages on the class declaration and navigate to the command handler from there. Very counter productive. But ReSharper is extendable!

The ReSharper SDK docs are very sparse, but the docs for setting up the project is pretty straight forward. You can read up on it here. Once you have it up and running and enabled debugging for it (very important since its alot of trial and error) its time to get cracking.

To create a navigation plugin you implement INavigateFromHereProvider. Also its important to add the [ContextNavigationProvider] attribute to your navigation class. The interface has a single method.

public IEnumerable CreateWorkflow(IDataContext dataContext)

ReSharper has alot of helper methods to navigate the code tree, and docs are sparse so its mostly trial and error here. I do not think my way is the best way, but it works.

var referenceName = dataContext.GetSelectedTreeNode<IReferenceName>();
var declaration = (referenceName?.Reference.Resolve().DeclaredElement ?? dataContext.GetSelectedTreeNode()?.DeclaredElement) as ITypeElement;
if (declaration != null && !(declaration is ICompiledElement))

First I get the selected class/struct reference if any using GetSelectedTreeNode. It returns null if you do not stand on a reference. Then I resolve that referenece declared element and try to cast it to a ITypeElement. If all goes well and its not null I know we have a type I can work with. I do a final check if the type is compiled, if it is I ignore it. This is not optimal, but searching the entire loaded solution included external dependencies is too slow with the method I have found to work.

Since this code run every time you bring up the context menu I do not want to start to find and filter references here. So at this point I just display the “Goto visitor” action in the context menu.

yield return new ContextNavigation("Goto &Visitor", null, NavigationActionGroup.Blessed, () => {});

If a user chooses to use the navigation action we start the actual filtering.

yield return new ContextNavigation("Goto &Visitor", null, NavigationActionGroup.Blessed, () =>
    var solution = dataContext.GetData(ProjectModelDataConstants.SOLUTION).NotNull();

    var foundMethods = solution
        .Select(r => ((r.GetTreeNode().Parent as IUserTypeUsage)?
            .Parent as IRegularParameterDeclaration)?
            .Parent as IFormalParameterList)
        .Where(list => list != null && list.ParameterDeclarations.Count == 1)
        .Select(m => m.Parent as IMethodDeclaration)
        .Where(m => m != null)

    if (!foundMethods.Any())
        solution.GetComponent<DefaultNavigationExecutionHost>().ShowToolip(dataContext, "No visitors found");

    var occurrences = foundMethods.Select(x => new LinkedTypesOccurrence(x.DeclaredElement.NotNull(), OccurrenceType.Occurrence)).ToList<IOccurrence>();
    ShowOccurrencePopupMenu(new[] { declaration }, occurrences, solution, dataContext.GetData(UIDataConstants.PopupWindowContextSource));

I find all references thats a method and that only take one parameter. If I do not find anything I present a tooltip to the user. Otherwise I use the standard ReSharper occurrence UI to display the occurrences, if only one is found it will navigate directly to that declaration.

Full code here

Install using ReSharper extensionmanager:

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s