Wednesday, December 12, 2007

CreateStubForHandler - How does it work?

Our good friend Koen Hoefkens suggests that there might be some interest around the inner workings of the newly refactored CreateStubForHandler.

There are 2 versions of the plugin. One for DXCore 2.x (for VS2003) and another for DXCore 3.x (for VS2005 and VS2008).

This was done for 2 reasons:
Koen (the original author) has a few projects kicking around in VS2003, and so we needed to maintain a version of the code capable of producing a version of the plugin that could likewise run under the DXCore 2.x and hence under VS2003. I have a virtual machine setup to run VS2003 under XP and so I am in a position to test this project at my end if need be.

DevExpress have recently released a beta of DXCore 3.0 along with companion copies of RefactorPro and CodeRush. Now these new betas (like the full versions which will follow) only run inside of VS2005 or better but provide some interesting features which are not available in previous versions. Therefore I thought it would be good to create a version of the code which would take advantage of these new features where possible.

So to meet both of these requirements, we have divided the project into 2 parts and therefore 2 directories. The main trunk of the project contains directories called 'VS2003' and 'VS2008'. These house the Solutions and further directories and Projects suited to those environments.

Triggering the plugin
Each Project contains a plugin class created from a template drawn from the appropriate DXCore. These Plugin classes each use techniques and technology again suited to the version of the DXCore that they support. The DXCore 2.x uses an Action and a RefactoringProvider to trigger the plugins main functions, while the VS2008 version uses the same action logic together with a CodeProvider to achieve a similar effect.

The inner workings are the same for each due to their sharing of a common "PluginLogic.vb" file which lives in the VS2003 folder structure and is linked to from the VS2008 solution.

How does it work?
Essentially what happens is that whether activated by Action, RefactoringProvider or CodeProvider, much the same thing happens.

We have a main function "CreateMethodStub" which is passed 3 parameters:

  1. CaretElement: A LanguageElement which represents the element of code next to the caret at the time of activation. We can use a LanguageElement here because we're talking about an object on the sourcecode.
  2. InsertionPoint: A SourcePoint which tells the plugin where the newly generated method should be placed.
  3. CompleteStatement: A boolean indicating whether or not the AddHandler/RemoveHandler statement requires completion.

This function is responsible for organising the various stages of the plugin's operation. You can think of it as the core of this plugin. It delegates to other functions to do almost everything and then triggers the expansion of the code into the Active Document.

Build the New Method
The first step in generating code using the DXCore is to choose the correct root LanguageElement. That would be "Method" in this case since we are trying to create a method.

Next it's all about adding other suitable LanguageElements to that Method in the correct places and waving the "DXCore Magic Code Generation Wand".

Creating a method is easy:
Dim Method As New Method(MethodName)
Method.MethodType = MethodTypeEnum.Void
..Next we need to add some parameters which is normally easy also:
Dim SomeParam as New Param("System.String",ParamNameHere)

Constructing the correct signature
The difficulty here is that we can't just create any old parameters. We have to create parameters suited to the Event for which we are creating our handler method.

So how do work out what those are then?

If you need a quick refresher on why we're using IElement derived interfaces then take a quick peek here

Well we ask the DXCore for the declaration of the Event which we are attempting to attach to like this....
Dim TheDelegate As IDelegateElement = CType(EventDec.Type.GetDeclaration, IDelegateElement)
... the iterate the parameters of said delegate duplicating the params on the method...
For Each Param As IParameterElement In TheDelegate.Parameters
    Dim NewParam As New Param(Param.Type.FullSignature, Param.Name)
    NewParam.Direction = Param.Direction

Add Some Content
Next we need to add some default content to that Method. We have decided to continue the original plan for now, and generate a statement to Throw a NotImplementedException.

So we offload the creation of the ThrowStatement on another simple utility function just so that our code can remain tidy.

Now we could just add this ThrowStatement to our Method.
this would be done by calling...
Dim TheThrow as ThrowStatement = GetThrowStatement("System.NotImplementedException")
...but We'd like to be a bit more clever with our plugin.

Although the Throw statement is a good default, in 90% of cases the first thing you'd like to do once your plugin has finished doing it's thing, is to erase the throw and replace it with something more useful.

So how about we select the ThrowStatement ready to be overtyped, just like in a template.

In order to do this you need to Wrap the code in special Template Elements. If you've every created your own templates, you'll be familiar with «Caret» and «BlockAnchor». They are placed one at either end of your code, and once "Expanded", will cause CodeRush to select the text in the document

There are 2 problems with this.

  • We don't have any code to wrap
  • We don't have any LanguageElements to represent Template Elements.

Hmmmm.....what to do?

Ok it turns out that there is a special LanguageElement for inserting arbitrary text into your code. It's called SnippetCodeElement

So the first implementation of this looked a bit like....
Method.AddNode(New SnippetCodeElement("«Caret»")
Method.AddNode(New SnippetCodeElement("«BlockAnchor»"))
But this had the annoying side effect of inserting a CRLF after the Throw and before the «BlockAnchor». In essence the CRLF got selected as well and this just wasn't right.

So a new approach had to be found. So I asked (as I did for much of this) AlexZ (of Developer Express), if he could explain how they did this and I received and embarrassingly simple solution.

Alex suggested pre-generating the code for the ThrowStatement, deliberately removing the trailing CRLF, wrapping the resultant code in the raw Template Element text and finally adding the whole lot to the method as a single SnippetCodeElement thus...

Dim ThrowStatement As [Throw] = GetThrowStatement("System.NotImplementedException")
Dim ThrowWithoutCRLF As String = GenerateWithoutCRLF(ThrowStatement)
Dim NewSnippetCodeElement As New SnippetCodeElement("«Caret»" & ThrowWithoutCRLF & "«BlockAnchor»" & ControlChars.CrLf)

Producing the Code
Finally we wave that "DXCore Magic Code Generation Wand" I mentioned earlier by calling...
...and the inserting that code in the correct location in our original document...
CodeRush.Documents.ActiveTextDocument.ExpandText(InsertionPoint, ControlChars.CrLf & MethodCode)

And now we're done. :)

Full source is available via SVN

Updated: the previously existing repository is still intact however the code has been copied to a new repository where future work will be continued from. this new repo can be found here here

No comments: