Visual Studio 2005 extensibility

Security Briefs

Syndication

I was building a data access layer today and got really sick of typing data-holder classes that are really only around to make the GUI guy's life easier when he goes to use databinding (which requires properties). Kind of like this:

public class IReallyHateTypingThese {
    public IReallyHateTypingThese(int a, int b, string foo, System.DateTime bar) {
        this.a = a;
        this.b = b;
        this.foo = foo;
        this.bar = bar;
    }
    public int A { get { return a; } }
    public int B { get { return b; } }
    public string Foo { get { return foo; } }
    public System.DateTime Bar { get { return bar; } }
    #region privates
    readonly int a;
    readonly int b;
    readonly string foo;
    readonly DateTime bar;
    #endregion
}

So much of that is redundant, so I spent a few hours learning the VS extensibility model and wrote a macro that would generate the goop above from a simple class declaration like the one below:

public class IReallyHateTypingThese {
    int a;
    int b;
    string foo;
    DateTime bar;
}

It was a good experience - I learned how to write smart macros that examine the code around the selection point, determine its context, and work from there. To convert the above class, you simply need to place the insertion point anywhere inside the class definition and my macro will figure out the class name and the fields using something called the CodeModel.

The CodeModel is a bit quirky, and I went through a few iterations before I finally felt I'd come close to mastering it. For one thing, it's very much designed with VB code in mind. It has features that are specific to VB syntax and omits features specific to my preferred language, C#. Oh, and if you're writing macros, it looks like you're stuck using VB, although an add-in can do the same things from any language (I stuck with macros because the development cycle of an add-in seems pretty painful - you can't recompile without unloading the thing, and I've not figured out how to unload it without shutting down VS). There are also a few things that don't work quite as advertised. I couldn't get DestructiveInsert to actually destroy selected text, for example. I also found a few places where I got “Not Implemented” exceptions. All of these problems were pretty easy to work around. Even with its warts, the CodeModel is pretty darn useful.

One thing in particular that I learned is that it's best to move the TextSelection around as you're generating code, instead of trying to work with the less flexible EditPoint. Just call TextSelection.MoveToPoint to move it where you want it to go and party on. EditPoints are useful as bookmarks since they seem to stay put even as you're changing code around them, but I couldn't figure out how to even insert a newline using an EditPoint.

If you've never played around with the CodeModel, this example should help you get started. Now that I'm over the learning curve, I think I'll be using this a lot more often. Here's the code that does the transformation above. It includes a couple of subroutines that I thought would be useful in other code.

Imports System
Imports EnvDTE
Imports EnvDTE80
Imports System.Diagnostics
Imports System.IO
Imports System.Collections.Generic

Public Module KeithsExtensibilityExample
    Sub fleshOutPropertyBagClass()
        Dim ts As TextSelection = DTE.ActiveDocument.Selection
        Dim fcm As FileCodeModel2 = DTE.ActiveDocument.ProjectItem.FileCodeModel

        ' jump into the CodeModel at the current insertion point
        Dim cls As CodeClass2 = fcm.CodeElementFromPoint(ts.TopPoint, _
          vsCMElement.vsCMElementClass)

        ' get a list of all the fields we'll be using,
        ' surrounding them with "privates" region
        Dim fields As New List(Of CodeVariable2)
        Dim child As CodeElement2
        For Each child In cls.Children()
            If child.Kind = vsCMElement.vsCMElementVariable Then
                fields.Add(child)
            End If
        Next
        surroundWithRegion("privates", cls.GetStartPoint(vsCMPart.vsCMPartBody), _
          cls.GetEndPoint(vsCMPart.vsCMPartBody))
        Dim position As Integer = 1
        Dim field As CodeVariable2

        ' build ctor
        Dim ctor As CodeFunction2 = cls.AddFunction(cls.Name, _
        vsCMFunction.vsCMFunctionConstructor, vsCMTypeRef.vsCMTypeRefVoid, _
          0, vsCMAccess.vsCMAccessPublic)
          ts.MoveToPoint(ctor.GetStartPoint(vsCMPart.vsCMPartBody))
        For Each field In fields
            ctor.AddParameter(field.Name, field.Type, position)
            position += 1
            ts.Insert(String.Format("this.{0} = {0};", field.Name))
            ts.NewLine()
        Next

        smartFormat(cls)

        ' add property gets
        position = 1 ' add properties after ctor
        For Each field In fields
            Dim propName As String = Char.ToUpper(field.Name(0)) _
            + field.Name.Substring(1)
            Dim prop As CodeProperty = cls.AddProperty(propName, Nothing, _
              field.Type, position, vsCMAccess.vsCMAccessPublic)
            ' vsCMPartAttributesWithDelimiter not supported on properties, sadly
            ts.MoveToPoint(prop.GetStartPoint(vsCMPart.vsCMPartBody))
            ts.FindPattern("{", vsFindOptions.vsFindOptionsBackwards) ' workaround
            ts.MoveToPoint(prop.GetEndPoint(), True)
            ' for some reason,
            ' DestructiveInsert doesn't work - just inserts at end of selection
            ts.Delete()
            ts.Insert(String.Format("{{ get {{ return {0}; }} }}", field.Name))
            position += 1
        Next

        ' mark all of the original fields readonly
        For Each field In fields
            field.ConstKind = vsCMConstKind.vsCMConstKindReadOnly
        Next

        ' remove whitespace between properties
        For Each child In cls.Children()
            If child.Kind = vsCMElement.vsCMElementProperty Then
                ts.MoveToPoint(child.GetStartPoint())
                ts.DeleteWhitespace(vsWhitespaceOptions.vsWhitespaceOptionsVertical)
            End If
        Next
    End Sub

    Sub surroundWithRegion(ByVal name As String, ByVal startPoint As TextPoint, _
      ByVal endPoint As TextPoint)
        Dim ts As TextSelection = DTE.ActiveDocument.Selection
        ts.MoveToPoint(startPoint)
        ts.Insert(String.Format("#region {0}", name))
        ts.NewLine()
        ts.MoveToPoint(endPoint)
        ts.Insert("#endregion")
        ts.NewLine()
    End Sub

    Sub smartFormat(ByVal element As CodeElement)
        element.GetStartPoint().CreateEditPoint().SmartFormat(element.GetEndPoint())
    End Sub

End Module

 


Posted Jan 08 2006, 03:41 PM by keith-brown
Filed under:

Comments

Sean Chase wrote re: Visual Studio 2005 extensibility
on 01-08-2006 6:39 PM
They should have something like this built-in to Visual Studio (like the typed-DataSet designer) for custom classes. xsd.exe almost gets you there, but the code it gens from an xsd is pretty ugly. :-)
Christopher Steen wrote Link Listing - January 9, 2006
on 01-09-2006 7:58 PM
A C# Set class based on enums - You don´t want to be without
it when constructing a compiler ;-) [Via:...
Jason Haley wrote Interesting Finds
on 01-10-2006 5:50 PM
O S M O S E wrote Code Generation 'in miniature' and in passing and in VS2005...
on 01-11-2006 9:56 AM
ex-Digineerite Matt Milner's PluralSite Compadre, Keith Brown, has a nice little introduction to Visual...
Kevin Kenny wrote re: Visual Studio 2005 extensibility
on 01-25-2006 3:47 AM
Did you ever encounter a problem with the error: An element with the name '<private member name>' already exists at this line:

Dim prop As CodeProperty = cls.AddProperty(propName, Nothing, _
field.Type, position, vsCMAccess.vsCMAccessPublic)

Keith Brown wrote re: Visual Studio 2005 extensibility
on 01-25-2006 7:50 AM
Try mucking with position, or not specifying it at all. I may have run into this but it's been awhile and frankly I don't remember :)
Kevin Kenny wrote re: Visual Studio 2005 extensibility
on 01-29-2006 8:07 AM
Solved...if you're using private members prepended with underscores e.g.:

private string _name;

The code is uppercasing the first character of the member name which is of course '_' and means that you end up with property names with the same name as the private member and of course it chokes.

Adding some checks for this naming convention gets it working.
Poonam wrote re: Visual Studio 2005 extensibility
on 10-12-2006 4:36 AM
Could someone help me in writing the same code in C++?

Thanks
J. Leite wrote re: Visual Studio 2005 extensibility
on 04-13-2008 9:37 AM
Would you happen to know how to overload the constructor through AddFunction (or any other call)?

e.g.

public IReallyHateTypingThese () : this (-1, -1, string.Empty, DateTime.Now) {}

public IReallyHateTypingThese(int a, int b, string foo, System.DateTime bar)
{...}