Macro Madness

 

Duncan Mackenzie
Microsoft Developer Network

June 3, 2003

Summary: Duncan Mackenzie shows you several useful macros for the Visual Studio .NET development environment, including one that prints out all of the code in your project. (10 printed pages)

Applies to:
   Microsoft® Visual Studio® .NET 2003
   Microsoft® Visual Basic® .NET 2003

Download the source code for this article.

Spring Is in the Air...

And a programmer's thoughts turn to macros. Customizing your development environment is one of those tasks that I often forget to spend time on, but really appreciate it when I do. I spend more of my conscious time in Microsoft® Visual Studio® .NET than in my house, at least on weekdays, so it makes sense to try to make that time as enjoyable and productive as possible. In this article I'm going to show a few of the macros that I use to add some very personalized functionality to the IDE. I hope that these samples serve as a jumpstart for your own customizations.

If you are new to developing macros in Visual Studio .NET, then you may be pleasantly surprised to find out two things: First, Visual Studio .NET includes a full IDE for editing macros that is (less surprisingly) very similar to the regular Visual Studio .NET IDE. Second, you don't have to write your macros in a scripting language; you get to work with the full Microsoft® Visual Basic® .NET language. All three of the sample macros from this article are included in the download, but they are not installed into the main \VSMacros directory when you run the .msi. Instead, they are placed into your "My Documents" folder, under MSDN\Coding4FunIssue5, from where you can open them into Visual Studio .NET using the "Load Macro Project" menu command (Tools | Macros | Load Macro Project...).

The first step to writing your own macro is to switch to the Macros IDE. This you can do with either ALT+F11, or by selecting the Tools | Macros | Macros IDE... menu item. Once you are in this slightly scaled down version of the Visual Studio IDE (see Figure 1), you can browse through the loaded Macro projects using the Project Explorer window.

Figure 1. The Macros IDE is a full-featured development environment and is quite similar to the full Visual Studio .NET environment.

To add your own macros, just create a new public Sub in an existing module (likely in the MyMacros project) or create a new public Module to hold your new code. I will show you how to create your own new macros later in this article, but first I'll walk through the code of the Print Code macro.

Printing All of the Code

In Visual Basic 6.0, when you selected Print from the File menu, you could choose to print all of the code in your project, even code files that weren't open in the designer. Many programmers would use this feature when they wanted to produce a hard copy of their work. Now, I'm not sure how often you will be printing out your code (it is hard to run code from paper), but if you want to do it in Visual Studio .NET, then you will need to print out each code file individually. Craig Skibo from the Visual Studio team sent me this relatively simple macro that automates that printing process, demonstrating how to loop through all of the items in a Visual Studio project along the way.

Note   In Visual Basic 6.0, you could also print out the design surface of your Forms, but this macro doesn't provide that functionality. You could likely write something for that purpose, but it wouldn't be simple, since Visual Studio .NET does not provide design-time printing of Windows Forms.

The first step this printing macro does is to obtain the currently selected project (or projects) by using DTE.ActiveSolutionProjects.

    Sub PrintItemsInSelectedProject()
        Dim proj As Project
        Dim objProj As Object()

        objProj = DTE.ActiveSolutionProjects
        If objProj.Length = 0 Then
            Exit Sub
        End If
        proj = DTE.ActiveSolutionProjects(0)
        PrintItemsInSelectedProject(proj.ProjectItems)
    End Sub

DTE is a special object available to all of your macros. It represents the IDE itself, providing access to the current set of projects, the active document, and more. In this case, although the code grabs the collection of all active projects, there is only one project we are interested in, so we pull out the first item in the collection. The final line in this routine could be a little confusing because it appears to be calling itself, but in reality it is calling another overload of the same routine. Using a fully functional language for your macro development provides access to this type of advanced functionality.

Private Sub PrintItemsInSelectedProject( _
    ByVal projitems As ProjectItems)
    Dim projitem As ProjectItem

    For Each projitem In projitems
        If (IsPrintableFile(projitem) = True) Then
            If (projitem.IsOpen( _
                    EnvDTE.Constants.vsViewKindTextView)) Then
                projitem.Document.PrintOut()
            Else
                Dim doc As Document
                doc = projitem.Open( _
                    EnvDTE.Constants.vsViewKindTextView).Document
                doc.PrintOut()
                doc.Close(vsSaveChanges.vsSaveChangesNo)
            End If
        End If
        PrintItemsInSelectedProject(projitem.ProjectItems)
    Next
End Sub

The actual printing occurs in this overload of PrintItemsInSelectedProject. The code loops through each item in the project, checking if it is a file that can be printed by looking at the file's extension. (IsPrintableFile is a routine included in the project. See below.)

Function IsPrintableFile( _
        ByVal projItem As ProjectItem) As Boolean
    Dim fileName As String
    Dim extensions As _
        New System.Collections.Specialized.StringCollection
    ' If you add a file to your project that is of 
    ' a type that can be printed, 
    ' then add the extension of that 
    ' file type to this list.
    Dim exts As String() = {".cs", ".vb", _
        ".aspx", ".xsd", ".xml", ".xslt", _
        ".config", ".htm", ".html", ".css", _
        ".js", ".vbs", ".wsf", ".txt", ".cpp", _
        ".c", ".h", ".idl", ".def", ".rgs", ".rc"}

    extensions.AddRange(exts)
    fileName = projItem.FileNames(1)
    Return extensions.Contains( _
        System.IO.Path.GetExtension(fileName).ToLower())
End Function

If it is a printable file, the code checks if the document is already open and prints it out if it is. If it isn't open, the Open method of the Item object is used to open the document, passing in vsViewKindTextView to specify that we want to view the code or text version of the file, not the visual designer view. The document is printed and then, because the macro had to open it, it is closed without saving any changes. Finally, once the current project item has been printed, the same routine is called against any sub items of the current item. That is all there is to this macro, yet the results are quite useful, saving you a lot of clicking around to ensure that each and every file from within your project has been printed.

This macro is contained within the VSUtil.vsmacros file, which is included in the download for this article. Use the Tools | Macros | Load Macro Project ... menu item to load this project into your own Macros IDE and Macro Explorer.

Although this Printing macro does a lot, it doesn't edit or even interact with any text. To illustrate working with your code, next I'm going to cover three macros that I use on a regular basis inside Visual Studio .NET.

Inserting and Retrieving Text

This first macro saves me a great deal of time, but it exposes my failure to comply with suggested naming standards. I always use the Hungarian notation preface of "m_" to indicate that a variable is my internal "member" variable corresponding to the property of the same name (minus the prefaced "m_"), and I usually start out my classes by typing in a list of declarations for those internal variables—before starting the mind-numbing task of creating individual property declarations for every one of those internal member variables. After doing the manual process for over six months, I created this macro to take the currently selected list of internal members and create individual property routines for each one of them. This macro may not be directly applicable to your own code, but it illustrates a few general techniques that will be useful when you are writing your own Visual Studio .NET macros. To start with, it grabs the currently selected text using the aptly named GetCurrentlySelectedText routine.

Private Function GetCurrentlySelectedText() As String
    If Not DTE.ActiveDocument Is Nothing Then
        Dim txt As TextSelection
        txt = CType(DTE.ActiveDocument.Selection, TextSelection)
        Return txt.Text
    Else
        Return String.Empty
    End If
End Function

If there is no selected text available, a blank string is returned. Next, using Regular Expressions, the selected text is parsed line by line to determine the appropriate property names and data types. As the text is parsed, a StringBuilder class is used to build up the new property procedures, to be inserted as a single block of text once the parsing is complete.

Dim line, originalCode As String
originalCode = GetCurrentlySelectedText()
If Not originalCode = String.Empty Then
    Dim variableName As String
    Dim publicName As String
    Dim dataType As String
    Dim propertyProcedures As New System.Text.StringBuilder
    Dim lines() As String
    lines = Split(originalCode, vbLf)
    Dim r As Regex
    r = New Regex( _
        "(Dim|Private)\s*(?<varname>\S*)" & _
        "\s*(As|As New)\s*(?<typename>\S*)", _
        RegexOptions.IgnoreCase Or _
        RegexOptions.ExplicitCapture)

    For Each line In lines
        line = line.Trim
        If Not line = String.Empty Then
            Dim mtch As Match
            mtch = r.Match(line)
            If mtch.Success Then
                variableName = _
                  mtch.Groups("varname").Value.Trim
                dataType = _
                  mtch.Groups("typename").Value.Trim
                'this assumes a consistent use of a 
                '2-char prefix on private variables
                'in my case "m_"
                publicName = variableName.Substring(2)

                propertyProcedures.AppendFormat( _
                       "{0}Public Property {1} As {2}{0}" _
                     & "    Get{0}" _
                     & "        Return {3}{0}" _
                     & "    End Get{0}" _
                     & "    Set(ByVal Value As {2}){0}" _
                     & "        {3} = Value{0}" _
                     & "    End Set{0}" _
                     & "End Property{0}", _
                     vbCrLf, publicName, _
                     dataType, variableName)
            End If
        End If
    Next
End If

Before inserting the new text, the selection point is positioned at the end of the current selection, which places it just after the set of internal variables, and a new UndoContext object is created.

DTE.UndoContext.Open("ConvertProperties")
Dim txt As TextSelection
txt = CType(DTE.ActiveDocument.Selection, TextSelection)
txt.Insert(propertyProcedures.ToString, _
    vsInsertFlags.vsInsertFlagsInsertAtEnd _
    Or vsInsertFlags.vsInsertFlagsContainNewText)
txt.SmartFormat()
DTE.UndoContext.Close()

Creating a new UndoContext object before making any changes, and then closing it off with UndoContext.Close() allows this entire macro action to be undone by the user. Without the UndoContext, each individual act in a macro would have to be undone individually. In this particular example, the difference wouldn't be very noticeable, because I do the entire insert as a single action. In a macro that performs many different steps, however, you really want to set up an UndoContext.

Note   Opening an UndoContext when one is already open will result in an exception. You can check for an existing UndoContext with the IsOpen method of the UndoContext object (DTE.UndoContext.IsOpen()).

The finished macro turns this:

Dim m_fred As String
Dim m_counter As Integer

Into this:

Public Property fred() As String
    Get
        Return m_fred
    End Get
    Set(ByVal Value As String)
        m_fred = Value
    End Set
End Property

Public Property counter() As Integer
    Get
        Return m_counter
    End Get
    Set(ByVal Value As Integer)
        m_counter = Value
    End Set
End Property

I use another simple macro to insert the MSDN boilerplate copyright statement into the very top of my code file. The StartOfDocument method allows me to move to the very top of the current document, where I can insert a prepared String full of text.

'full string in the code download
Dim sCopyright As String _
    = "'Copyright (C) {1} Microsoft Corporation{0}" & _
      "'All rights reserved.{0}'{0}" & _
      "'THIS CODE AND INFORMATION IS PROVIDED """ & _
      "AS IS"" WITHOUT WARRANTY OF ANY KIND, EITHER{0}" & _
      "'EXPRESSED OR IMPLIED...."

Sub PasteCopyrightHeading()
    Dim crText As String
    '{0} = end of line
    '{1} = year
    '{2} = month
    crText = String.Format(sCopyright, _
        vbCrLf, CStr(Now.Year), _
        String.Format("{0:Y}", Now))

    Dim objTextSelection As TextSelection
    objTextSelection = _
        CType(DTE.ActiveDocument.Selection, _
              TextSelection)
    DTE.UndoContext.Open("PasteCopyright")
    objTextSelection.StartOfDocument(False)
    objTextSelection.Insert(crText)
    DTE.UndoContext.Close()
End Sub

In the real code, I don't break and concatenate the copyright string across multiple lines, and you shouldn't do it in your code either. It would be inefficient. (See this article for more information on why.) I only do it in the sample code for formatting purposes. Note that I don't check if the copyright string is already there; I just go ahead and add it. This casual style of programming has worked for me in this case, because I am writing for only one user and he is fairly accepting of flaws in these macros. Depending on your audience, you might need to walk the document and check whether your new text already exists.

Inserting Text

The final macro I am going to show you is a simple one, but it illustrates an important concept in automating the code editor. I like to use Regions to tidy up my code, grouping together all of my property routines into a single Region, or grouping together the save/load routines for my application's settings. So I created a macro to make this process a little easier. Rather than type in the start of the Region, then go down to the end of the desired code and add the ending Region statement, I wanted to be able to just select a section of code and hit an Add Region button. Not very tricky, but I found the code interesting. The Insert method of the TextSelection object is quite full featured, although its lack of Microsoft® IntelliSense® support can make it hard to discover all of the available features, which allow you to insert text at the beginning or end of the selection as well as control how your insert affects the selected area.

Public Sub InsertRegion()
    Dim regionName As String
    regionName = InputBox("Region Name?")
    If Not regionName.Trim = String.Empty Then
        DTE.UndoContext.Open("InsertRegion", False)

        Dim activeSelection As TextSelection
        Dim codeInQuestion As String
        activeSelection = CType(DTE.ActiveDocument.Selection, _
            TextSelection)
        With activeSelection
            .Insert(String.Format("#Region ""{0}""{1}", _
                    regionName, vbCrLf), _
                vsInsertFlags.vsInsertFlagsContainNewText Or _
                vsInsertFlags.vsInsertFlagsInsertAtStart)
            .Insert(String.Format("{0}#End Region{0}", vbCrLf), _
                vsInsertFlags.vsInsertFlagsInsertAtEnd Or _
                vsInsertFlags.vsInsertFlagsContainNewText)
        End With
        DTE.UndoContext.Close()
    End If
End Sub

By specifying vsInsertFlagsInsertAtStart when I insert the "#Region" line, it is placed in front of the currently selected text. Including the vsInsertFlagsContainNewText flag means that the current selection (highlighted area of text) is automatically expanded to include this new line.

Learning More about Visual Studio .NET 2003

My three little macros are very simple examples of the customization and automation possible inside Visual Studio .NET 2003, but I hope they are useful in getting you started. If you want to learn more about writing macros or other forms of customization for Visual Studio .NET, I would suggest the following resources:

  • DevHawk has posted his own macro for inserting regions, and his is much more detailed and also handles multiple languages.
  • CodeSmith by Eric Smith allows you to generate .NET code using Microsoft® ASP.NET style templates.
  • This MSDN article detailing the creation and use of a Visual Studio .NET plug-in.
  • Craig Skibo, Brian Johnson, and Marc Young have written Inside Visual Studio .NET from Microsoft Press, and it includes an amazing amount of information about customizing and extending the IDE.

Coding Challenge

At the end of some of my Coding4Fun columns, I will have a little coding challenge—something for you to work on if you are interested. For this article, the challenge is to create a macro, add-in, or other Visual Studio .NET-related code. Macros will be in Visual Basic .NET of course, but any managed code will do for an add-in or other tool. Just post whatever you produce to GotDotNet and send me an e-mail message (at duncanma@microsoft.com) with an explanation of what you have done and why you feel it is interesting. You can send me your ideas whenever you like, but please just send me links to code samples, not the samples themselves (my inbox thanks you in advance).

Have your own ideas for hobbyist content? Let me know at duncanma@microsoft.com, and happy coding!

 

Coding4Fun

Duncan Mackenzie is the Microsoft Visual Basic .NET Content Strategist for MSDN during the day and a dedicated coder late at night. It has been suggested that he wouldn't be able to do any work at all without his Earl Grey tea, but let's hope we never have to find out. For more on Duncan, see his site.