WindowsDevCenter.com
oreilly.comSafari Books Online.Conferences.

advertisement


AddThis Social Bookmark Button

Refactoring with Visual Studio Macros

by James Avery
06/20/2005
Use ClickOnce to Deploy Windows Applications

Refactoring is a method of improving your code without breaking or modifying the external functionality of your application. Refactoring has been growing in popularity because it is one of the key practices of extreme programming, and because it goes hand in hand with test-driven development. In refactoring, you make a plethora of different small changes (or "refactorings") to your code. These changes are small enough to quickly test and have a low risk factor, but in total, they increase the overall quality of your code base or application. For a large list of refactorings and more information on refactoring in general, see www.refactoring.com.

IDE vendors and third party add-in companies have started to build support for refactoring into IDEs. Visual Studio 2005 includes a number of different refactoring features; the C# features are built into the IDE and the VB.NET features are provided through a free add-in. While the new add-ins and features cover some of the more popular refactorings, there are dozens that are not covered, and even more that are too specific to be handled in a generic tool. This is where macros come in. We can use macros to perform specific refactoring operations on our applications, something Microsoft or third-party vendors are never going to do for us. By writing macros we can make performing these refactorings easier and quicker than if we did them manually, always a sure-fire way to ensure that more of them will be done.

In this article I am going to cover a couple of hypothetical refactoring situations. I will identify the problem, the refactoring to be used, and then we will walk through creating the macro to make it easier to complete that refactoring.

General Purpose Refactoring

First let's write a macro for a general purpose refactoring, replacing a magic number with a constant. Magic numbers are literal numbers in your application that are hard set, for instance you might have an age limit for a certain action, or perhaps a rate that is "set in stone". Here is an example of code using a magic number:


public bool CheckAge(Person person)
{
  if(person.Age > 21)
  {
    return true;
  }
  else
  {
    return false;
  }
}

In this piece of code, we compare the person's age against a magic number of 21. In this refactoring, we will create a constant for this magic number and then replace the literal value with the constant. When our macro is complete, you can highlight the magic number you want to replace and execute the macro to have the number moved to a constant on the same class and the magic number replaced with the constant.

To create the macro, navigate to Tools -> Macros -> Macro IDE in Visual Studio 2003. The Macro IDE is the development environment specifically built for macros; using it we can quickly create and work with macros.

In the Macro IDE, create a new module and add a new Sub to that module, using the name you want for the macro:


Imports EnvDTE
Imports System.Diagnostics

Public Module Module1

  Public Sub ExtractToConstant()

  End Sub

End Module

This is all you need to do to create a macro called ExtractToConstant(). You can now see and run the macro through the Macro Explorer--it doesn't do anything, but it's there. Now we need to add some code to actually create the new constant and replace the selected number. The main object we will work with is the TextSelection object. This object gives us a reference to the currently selected text and allows us to make other changes in the same document. First we need to get a reference to the current TextSelection object and then get the value of the currently selected text:


'Get the selected text
Dim sel As TextSelection

sel = DTE.ActiveDocument.Selection

Dim selectedText As String

selectedText = sel.Text

Now we have an instance of the TextSelection object and a string that includes the text that was selected. Now we need to prompt the user for what they want to call the new constant. This is done using the InputBox method:


'Ask for the name of the constant
Dim constName As String = InputBox("Enter a name for the constant")

'Replace the selected text with the constant name
sel.Text = constName

After we get the name for the new constant from the user, we need to replace the selected text with the name of the constant. Now that the magic number has been replaced, we just need to add the constant to the top of the current class. To do this, we have to manually walk through the text and find where the class starts. The easiest way to do this is to look for the opening bracket, and then look for the next opening bracket; this should be the class in our document.


'Move to the top of the document
sel.StartOfDocument()
sel.FindText("{")
sel.FindText("{")
sel.LineDown()
sel.NewLine()
sel.LineUp()
sel.Indent()

In the above code, we move to the top of the document, find the first opening bracket, find the next opening bracket, move down a line, create a new line, move back up to that new line, and finally indent the line.

Now that we have the correct position, we simply need to create our text and place it into the document:


sel.Text = "public const int " & constName & " = " & selectedText & ";"

This line of code inserts the new constant into the file. As you can tell, this is a C# version of the macro but you could also easily write a VB version. When you select the number and run this macro, it replaces the magic number with the name of the constant and then inserts the constant, including the old value, at the top of the class. This is a fairly simple macro, but it shows the power of using macros to aid in the refactoring process. In just a short amount of time we have created a macro that we could use on a fairly frequent basis, saving us a little bit of time every time we use it.

Application Specific Refactoring

Next, let's automate a more application-specific refactoring. Look at the following method:


public Person GetPerson(int personID, SqlConnection sqlConn)
{
  //Do some work here
}

Here we have a common object retrieval method, something you would normally find in your data access layer. You will notice that you are passing the SqlConnection in as a parameter. You may decide later in the development process that you no longer want to pass the SqlConnection into the method, but instead want to retrieve it in the method itself. There could be a number of reasons for this--you may want to add different database types and only want to modify the data access layer when this happens, or you may have decided it would better fit your architecture to determine the connection in your data access layer.

Whatever the reason for making this change, it would be very time-consuming. You could not easily do it with find and replace, you would basically need to do a lot of deleting and pasting. First let's look at what we want our result to be:


public Person GetPerson(int personID)
{
  SqlConnection sqlConn = GetSqlConnection();

  //Do some work here
}

We are simply going to remove the SqlConnection parameter and move the same declaration to the internals of the method and call an application-specific method to get the SqlConnection. This kind of macro is going to be specific to your project, and most likely specific to a single task, but will be a lot easier and faster than manually making all the changes yourself.

As in the first macro example, we first need to retrieve the selected text. We will use the selected text later as the declaration, so we won't have to change the rest of the method since the variable name will be the same.


'Get the selected text
Dim sel As TextSelection

sel = DTE.ActiveDocument.Selection

Dim selectedText As String

selectedText = sel.Text

Next we need to clear the variable from the parameter list. We will assume that the parameter is always going to be second or later in the list, which means we also need to delete the space and comma before the parameter. This means we need to delete the currently selected text, and then delete each of the characters to the left.


'First we need to delete the selected text and the space and comma before it
sel.Delete()
sel.CharLeft()
sel.Delete()
sel.CharLeft()
sel.Delete()

Next we need to make room at the top of the method and insert the new declaration including the call to the new method.


sel.FindText("{")
sel.LineDown()
sel.NewLine()
sel.LineUp()
sel.Indent()
sel.Text = selectedText + " = GetSqlConnection();"

This code is again somewhat similar to the first method; we need to first find the opening bracket, create a new line, and indent. Then, we can insert the original declaration followed by our new method.

When the user selects the parameter and runs this macro, the parameter is replaced and a new line created at the top of the method declaring the same variable and setting it to our new method. If all of our parameters are named consistently, then we can even further automate this refactoring using the FindText method. Instead of requiring that we select the parameter we can instead search through the entire active document and each time we find that parameter perform our refactoring on it.

Public Sub DemoteSQLConnectionForDoc() 
'Get the selected text 

Dim sel As TextSelection
sel = DTE.ActiveDocument.Selection
sel.StartOfDocument()
sel.FindText("SqlConnection sqlConn")

In this code, we create our TextSelection object and jump to the start of the document. Then, we use the FindText method to look for a match on our parameter name.

  While (sel.Text <> "")
    Dim selectedText As String

    selectedText = sel.Text

    'First we need to delete the selected text and the 
    'comma before it
    sel.Delete()
    sel.CharLeft()
    sel.Delete()
    sel.CharLeft()
    sel.Delete()

    sel.FindText("{")
    sel.LineDown()
    sel.NewLine()
    sel.LineUp()
    sel.Indent()
    sel.Text = selectedText + " = GetSqlConnection();"
    sel.FindText("SqlConnection sqlConn")

  End While
End Sub

Then we use a while loop to see if a match was found. If a match was found, we perform the refactoring and then call FindText() again to see if there is another method in the document we need to refactor.

Macros and Refactoring are two technologies that perfectly complement each other. Using macros you can automate many of the tedious tasks involved in refactoring large applications and thus make refactoring easier and less time consuming, leading to better code.

Related Links

Visual Studio Hacks

Related Reading

Visual Studio Hacks
Tips & Tools for Turbocharging the IDE
By James Avery

James Avery has been programming with Microsoft technologies for the last 7 years and has been working with .NET since the second beta release.


Return to OnDotNet.com