.NET Tools
Essential productivity kit for .NET and game developers
A Super Simple ReSharper Plugin – MVC Verbs
ReSharper has many extension points. Some are obvious and well known, such as the element problem analyzer “squigglies”, the Quick Fix “lightbulb” menu items, unit testing support or code cleanup modules. But there are plenty more, perhaps not as obvious, but all equally available to plugin writers to do interesting things with.
I want to demonstrate this with a super simple plugin that does just one little thing. This isn’t intended as a tutorial, more of an annotated sample. I’m not going to explain everything, but hopefully enough that it still makes sense. The code is available on JetBrains’ GitHub site, together with a download so you can actually use it.
Here’s the problem, based on user feedback. When working in an MVC controller class, hitting Ctrl-F12 (Alt- in Visual Studio key bindings) the “Go to File Member” dropdown is displayed, listing all members:
As you can see in this example, there are two methods for the “Manage” action. One handles GETs, the other POSTs, but without knowing what the parameters mean, it’s very hard to tell which is which. Let’s fix that.
Fortunately, we can do this in a single class, by implementing the IOccurencePresenter interface. All of the “Go to” menus share an implementation, and find and display a list of IOccurence instances, which represent a file member, a type, a symbol or a file. The menu needs to convert this occurrence into the rich text description shown in the screenshot above, and iterates through a collection of IOccurencePresenter objects until it finds one that can handle a given IOccurence.
IOccurencePresenter contains two methods:
public interface IOccurencePresenter { bool IsApplicable(IOccurence occurence); bool Present(IMenuItemDescriptor descriptor, IOccurence occurence, OccurencePresentationOptions occurencePresentationOptions); }
IsApplicable is pretty straightforward – we look at the occurrence, and decide if it’s something we’re interested in presenting. For this plugin, we’re interested in methods that have attributes that derive from MVC’s ActionMethodSelectorAttribute.
We can do this by first checking to see if the occurrence is an instance of DeclaredElementOccurence, and assuming it is, checking that the declared element is a method (a declared element is a ReSharper object that represents a code construct (C# and VB, but also HTML and CSS) that we have source code for, and that is part of an AST). If it is a method, we can get its attributes, and see if any of those derive from ActionMethodSelectorAttribute.
That gives us code such as this:
public override bool IsApplicable(IOccurence occurence) { return GetActionMethodSelectorAttributes(occurence).Any(); } private static IEnumerable<IAttributeInstance> GetActionMethodSelectorAttributes(IOccurence occurence) { var declaredElementOccurence = occurence as DeclaredElementOccurence; if (declaredElementOccurence == null) return EmptyList<IAttributeInstance>.InstanceList; var method = declaredElementOccurence.DisplayElement .GetValidDeclaredElement() as IMethod; if (method == null) return EmptyList<IAttributeInstance>.InstanceList; return from attribute in method.GetAttributeInstances(true) where DerivesFrom(attribute.AttributeType, "System.Web.Mvc.ActionMethodSelectorAttribute") select attribute; } private static bool DerivesFrom(IDeclaredType type, string superClassName) { return type.GetSuperTypes() .Any(superType => superType.GetClrName().FullName == superClassName); }
The Present method is a little more complicated, since we need to format the information in the IOccurence as rich text.
Fortunately, help is at hand in the shape of the DeclaredElementOccurencePresenter base class. If we derive from this, ReSharper’s default implementation of Present handles the normal formatting for us – it will happily convert a method declaration into the rich text displayed in the screenshot above. It handles displaying the main text, “container” (“in AccountController” in the screenshot) and location, such as related filenames, or the project the file appears in. And it implements this as virtual methods that we can override:
protected override void DisplayMainText(IMenuItemDescriptor descriptor, IOccurence occurence, OccurencePresentationOptions options, IDeclaredElement declaredElement) { base.DisplayMainText(descriptor, occurence, options, declaredElement); var text = GetText(occurence); if (text.Length > 0) { var textStyle = TextStyle.FromForeColor(Color.FromArgb(78, 201, 176)); var richText = new RichText(string.Format("[{0}] ", text), textStyle); richText.Append(descriptor.Text); descriptor.Text = richText; } } private static string GetText(IOccurence occurence) { var verbs = from attribute in GetActionMethodSelectorAttributes(occurence) let verb = attribute.GetClrName().ShortName select verb.Replace("Attribute", string.Empty); return string.Join(", ", verbs.ToArray()); }
We let the original method do the heavy lifting to create the main text, then do the same check for attributes we did in IsApplicable , this time concatenating them into a string. If there are any attributes, we add them to the start of the main text (stored in the menu descriptor), with a nice bit of colour.
Just one last thing is needed to make this all work – we need to tell ReSharper about our IOccurencePresenter.
One of the reasons ReSharper is so extensible is because it’s built on top of its own IoC container. The ReSharper codebase is made up of components, and each component specifies its dependencies as constructor parameters. When the container is resolving a component into an instance object (the equivalent of “new”), it will first resolve all of the components in the constructor parameters. If the constructor parameter is an IEnumerable<T>, the container will find all implementations of type T and pass it in. So, we could get all IOccurencePresenter instances by declaring a constructor parameter of IEnumerable<IOccurencePresenter>. And by simply advertising a new implementation of IOccurencePresenter to the container, we can extend ReSharper.
Advertising a component is very easy – you just mark your type with an attribute. Which attribute we use depends on what we’re trying to advertise, but for IOcurrencePresenter instances, we want to use OccurencePresenterAttribute:
[OccurencePresenter] public class OccurrencePresenter : DeclaredElementOccurencePresenter { ... }
And finally, that gives us something like this:
And now we can clearly distinguish the “Manage” actions based on what verbs they handle.
One thing to note is that the GET action method doesn’t display the “HttpGet” attribute in the drop down. This is simply because the method doesn’t have the HttpGetAttribute applied to the method; MVC implies GET if no verb is specified. One nice improvement to the plugin might be to add this implied “HttpGet” for action methods that don’t have a verb attribute. If you’re interested in that, head over to the GitHub issues page and raise an issue, or feel free to implement it yourself and submit a pull request.
All of the code for this plugin is available in JetBrains’ “resharper-plugins” GitHub repository, as is a download for you to use the plugin. If you’re interested in diving a little deeper, the repo also contains code to ensure the colour of the attributes matches the colour used in the code editor, and is a useful example of retrieving colours from ReSharper, and dealing with threading (we need to retrieve colours from the main thread, since ReSharper needs to talk to Visual Studio to get them).
Hopefully this has been a useful demonstration of a simple, less well-known way to extend ReSharper, with the added benefit of providing a working plugin that solves a real customer’s feature request. There are plenty more extension points to explore, and we’ll have some more simple plugins to demonstrate. In the meantime, check out the plugin dev guide, and remember that dotPeek is very handy for navigating around the SDK.