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