 Monday, January 16, 2006
If I could get istantaneous results for the following simple two-question survey
- Is Visual Studio the application that you use most often?
- Did you ever use regex searches in the VS Find dialog box?
I'd bet that 80% of you would answer YES to the first question, but 99% of you would answer NO to the second question, which would be a rather weird result. Regex searches are among the most powerful VS features, yet few developers use them or even know that they exist.
IMHO, the real problem is that VS regex's syntax is completely different from the syntax you use with the Regex class, therefore using this feature requires that you learn yet another regex dialect. This is a bit too much for most developers. Microsoft should allow the standard regex syntax in this dialog: they could implement this change very easily and in a short time, without caring about backward compabibility issues.
While waiting for Microsoft to offer this little-big innovation, you can have fun with what you have today. Here are a few examples, excerpted from my new book Programming Microsoft Visual Basic 2005: The Language:
:i = :z Search assignments of an integer constant (:z) to a variable (:i). In VB, but more rarely in C#, it can deliver false matches, when the = operator is used in an expression.
:i = :q Search assignments of a quoted string constant (:q) to a variable.
(Dim|Private|Public) :i As String Search for variable declarations of string type (VB only). You can easy adapt it to other data types.
Dim <(:Lu(:Ll)*)+> As Search for local VB variables that use a PascalCase naming convention and therefore violate Microsoft's guidelines. (Local variables should use the camelCase convention.)
^:b*'.+\n Search for comment lines in VB, that is, lines that begin with an apostrophe. (It doesn't consider the REM keyword.) You can replace the apostrophe with // to use this search pattern in C# as well.
Dim {:i} As (.|\n)#<\1> Highlights the portion of code between the declaration of a local variable and the first occurrence of that variable in the method. You can repeat this search for all the local variables in a method and check whether you should refactor your code by moving the declaration closer to where the variable is used for the fist time. (See effect in figure below.)

 Wednesday, December 14, 2005
In a previous post I introduced a tiny utility to clean up Visual Studio projects. I have introduced minor fixes to it since then, so this is the most recent version, which also deletes *.suo and *.user files and produces a report of all the files and folders that couldn't be deleted for any reason:
Imports System.IO
Module Module1 Sub Main(ByVal args() As String) Console.WriteLine("VSProjCleaner tool (C) 2005 Francesco Balena, Code Archirects") If args.Length = 0 Then Console.WriteLine("Removes BIN and OBJ folders, .suo and .user files from Visual Studio projects") Console.WriteLine() Console.WriteLine(" SYNTAX: VSProjCleaner dirname") Console.WriteLine() Return End If Console.WriteLine()
' Use current directory if no argument has been specified Dim rootDir As String = Directory.GetCurrentDirectory() If args.Length > 0 Then rootDir = args(0) ' Read all the folder names in the specified directory tree Dim dirNames() As String = Directory.GetDirectories(rootDir, "*.*", SearchOption.AllDirectories) Dim errorsList As New List(Of String)
' delete any .suo and vbproj.user file For Each dir As String In dirNames Dim files As New List(Of String) files.AddRange(Directory.GetFiles(dir, "*.suo")) files.AddRange(Directory.GetFiles(dir, "*.user")) For Each fileName As String In files Try Console.Write("Deleting {0} ...", fileName) File.Delete(fileName) Console.WriteLine("DONE") Catch ex As Exception Console.WriteLine() Console.WriteLine(" ERROR: {0}", ex.Message) errorsList.Add(fileName & ": " & ex.Message) End Try Next Next
' Delete all the BIN and OBJ subdirectories For Each dir As String In dirNames Dim dirName As String = Path.GetFileName(dir).ToLower() If dirName = "bin" OrElse dirName = "obj" Then Try Console.Write("Deleting {0} ...", dir) Directory.Delete(dir, True) Console.WriteLine("DONE") Catch ex As Exception Console.WriteLine() Console.WriteLine(" ERROR: {0}", ex.Message) errorsList.Add(dir & ": " & ex.Message) End Try End If Next Console.WriteLine() Console.WriteLine(New String("-"c, 60)) If errorsList.Count = 0 Then Console.WriteLine("All directories and files were removed successfully") Else Console.WriteLine("{0} directories or directories couldn't be removed", errorsList.Count) Console.WriteLine(New String("-"c, 60)) For Each msg As String In errorsList Console.WriteLine(msg) Next End If End Sub End Module
You can download the binary version here: VSProjCleaner.exe (24 KB) or the complete solution here: VSProjCleaner_Source.zip (15.08 KB)
Using this tool couldn't be simpler: just run it from the command window and pass a folder name as an argument, enclosing it in double quotes if it contains spaces: VSPROJCLEARNER "c:\my projects\testproj" The tool recursively visits all the folders and deletes all BIN and OBJ directories, and deletes *.suo and *.user files. At the end of the process it lists all the folders and files that couldn't be removed, if any.
DISCLAIMER: I have tested this code against my own projects and it always worked correctly, but I don't should be held responsible for any loss of important files (for example, a .user file that has nothing to do with a Visual Studio project.). For this reason, I urge you to apply this tool only to a COPY of your projects
 Monday, November 21, 2005
I am back from the Windows Professional Conference (Milan, Italy), for which I also server as the chairman for two of the four tracks. The conference touched virtually all the new features of Visual Studio and SQL Server 2005, plus tons of other topics. But that's another story.
When preparing the material for the conference I had to pack all my code samples in a zip file. Even after zipping them, I came up with a 7-8M file, which is a bit too much. So I was about to manually delete all the files that could be recreated by recompiling the projects. It's the same old story, that repeat itself with all conferences, books, articles for magazines and blog posts.
Instead of using the manual approach, this time I decided to write a tiny utility that does the work for me. It took (literally) two minutes to write a console utility that takes the path as an argument and recurses over that directory tree to remove all the folders named "bin" and "obj". If a delete operation fails, a message error is displayed. (This happens when an executable is running and is therefore locked by the operating system.) The code is especially concise thanks to an overload of the Directory.GetDirectories method (added in .NET 2.0) that returns all the directory in a tree.
Imports System.IO
Module Module1 Sub Main(ByVal args() As String) ' Use current directory if no argument has been specified Dim rootDir As String = Directory.GetCurrentDirectory() If args.Length > 0 Then rootDir = args(0) ' Read all the folder names in the specified directory tree Dim dirNames() As String = Directory.GetDirectories(rootDir, "*.*", SearchOption.AllDirectories) Dim errors As Integer = 0
' Delete all the BIN and OBJ subdirectories For Each dir As String In dirNames Dim dirName As String = Path.GetFileName(dir).ToLower() If dirName = "bin" OrElse dirName = "obj" Then Try Console.Write("Deleting {0} ...", dir) Directory.Delete(dir, True) Console.WriteLine("DONE") Catch ex As Exception Console.WriteLine() Console.WriteLine(" ERROR: {0}", ex.Message) errors += 1 End Try End If Next
Console.WriteLine() If errors = 0 Then Console.WriteLine("All directories were removed successfully") Else Console.WriteLine("{0} directories couldn't be removed") End If End Sub End Module
In addition to using this tool from the command line, you can add it to the Tools menu, so that you can quickly delete all the files produced by compiling the current solution, by using this command:
DELETEBINPATH $(SolutionDir)
where of course DeleteBinPath is the name you used when compiling the utility.
UPDATE: I have posted a new version of this utility in a more recent post.
 Wednesday, November 16, 2005
I am reviewing the chapter devoted to the My Namespace and I am adding a few details that I overlooked in the first draft, such as the creation of custom setting providers (to save settings on a medium other than the configuration file, e.g. a database) and the ability to bind a control's property to a user sertting. Custom setting providers are relatively complex and are of interest for a relatively small number of users, whereas setting binding is a simpler topic that will surely draw the attention of any developer working with Windows Forms applications.
I found many articles and posts on the My.Settings object (VB) and Settings object (C#), but most of them omit to emphasize the ability to bind a user setting to a form property, such as the Size and Location properties. This feature enables you - among the many things - to restore the size and position of a form from a previous session. The .NET infrastructure automatically assigns these properties when the form is loaded and save them when the form is resized or moved.

Figure 1. Visual Studio 2005 enables you to define user-level and application-level settings
|

Figure 2. How to bind a property to a user-level setting. |
The great thing of this technique is that you don't need to write a single line of code. In fact, you just need to define one or more settings in the Settings page of the My Project designer (Visual Basic) or Properties designer (C#), for example the MainFormLocation and MainFormSize settings (see Figure 1). It is crucial that these settings are defined as user settings, because application-level settings are read-only. Next, you can select the form, switch to the Properties window, open the (Application Settings) section, click on the arrow near the ClientSize and Location properties, and select the user setting you want to bind the property to. If you haven't created the user setting yet, you can do it now by clicking on the New element. (See Figure 2.)
As I already noted, the noteworthy detail is that these settings are automatically updated when the end user moves or resizes the form. You can bind other properties, for example Text, BackColor, etc. If you perform this action for all the forms in the application, you can implement a simple yet powerful persistance mechanism for all user's preferences, again without writing code!
Obviously, you can extend this mechanism to properties of individual controls. Not all properties can notify to the world that they have been modified, though. More precisely, the control that exposes the property must implement the IBindableComponent interface and must expose an event named XxxxChanged for each property, or it must implement the INotifyPropertyChanged interface (new in .NET 2.0). Most Windows Forms controls, but not all of them, implement these interfaces. For example, the ToolStripItem control doesn't implement it. In this case, the property is assigned correctly when the form loads, but you must update the corresponding user setting via code.
|
 Sunday, November 13, 2005
Visual Studio 2005 comes with dozens of ready-to-use code snippets. You might argue on the usefulness of some of them, but for sure many of them are really well-conceived. For example, the prop expansion that creates public C# properties is a real time saver.
The Code Snippet Manager dialog box (in the Tools menu) enables you to inspect all the installed snippets, one by one, but doesn't offer the option to export a list of all the snippets, therefore you have to browse them one by one to take notice of their name, purpose, and keyboard shortcut. While I was working on chapter 4 of Programming Microsoft Visual Basic 5, I wrote this little throw-away program which decodes the snippet index and list them on a console window. Of course you can redirect the output to a file to have a document that you can use as a reference.
The program takes a parameter equal to the path of the SnippetIndex.xml (VB) or SnippetsIndex.xml (C#) file that contains the snippet index. (Oddly, this file has a slightly different name in the two languages.) If you run it without passing any argument, it uses the path of the VB snippet index in a default Visual Studio installation. A comment in the listing explains how you can use the default index for C# instead.
The output of this code is quite terse - just snippet names and shortcuts, grouped in categories - but you can easily modify the source code to extract and display more attributes.
Imports System.IO Imports System.Xml Imports System.Text.RegularExpressions
Module Module1 Dim snippetsPath As String Dim catNames As New Dictionary(Of String, String)
Sub Main(ByVal args() As String) ' If no argument has been provided, use default path for snippets. If args.Length = 0 Then args = New String() {"C:\Program Files\Microsoft Visual Studio 8\Vb\Snippets\1033\SnippetIndex.xml"} ' Uncomment next line to list C# snippets ' args = New String() {"C:\Program Files\Microsoft Visual Studio 8\VC#\Snippets\1033\SnippetsIndex.xml"} End If
Dim snippetsFile As String = args(0) snippetsPath = Path.GetDirectoryName(snippetsFile) ' Load the snippet index file. Dim xmlIndex As New XmlDocument() xmlIndex.Load(snippetsFile) ' We need two passes, because dirs and subdirs use a different XML element. ParseSnippetIndex(xmlIndex, "//SnippetDir") ParseSnippetIndex(xmlIndex, "//SnippetSubDir") ' Iterate over all the directories in the main snippet directory. For Each dir As String In Directory.GetDirectories(snippetsPath) ParseSnippetFolder(dir, "") Next End Sub
Sub ParseSnippetIndex(ByVal xmlIndex As XmlDocument, ByVal searchKey As String) ' Create the correspondence between relative paths and localized categories For Each xmlEl As XmlElement In xmlIndex.SelectNodes(searchKey) Dim elPath As XmlElement = DirectCast(xmlEl.SelectSingleNode("DirPath"), XmlElement) Dim elName As XmlElement = DirectCast(xmlEl.SelectSingleNode("LocalizedName"), XmlElement) catNames.Add(elPath.InnerText, elName.InnerText) Next End Sub
Sub ParseSnippetFolder(ByVal dir As String, ByVal parentCategory As String) ' Retrieve the relative name of this subdirectory. Dim relPath As String = dir.Substring(snippetsPath.Length) ' The default name for this category Dim categoryName As String = parentCategory & Path.GetFileNameWithoutExtension(dir) ' Search this relative path in the snippet index. Dim searchPath As String = "%InstallRoot%\Vb\Snippets\%LCID%" + relPath + "\" If catNames.ContainsKey(searchPath) Then ' If found, use the localized category as appears in the index file categoryName = parentCategory & catNames(searchPath) End If Console.WriteLine(categoryName.ToUpper()) ' Parse individual snippets in this directory. For Each file As String In Directory.GetFiles(dir, "*.snippet") ParseSnippetFile(file) Next ' Parse all sub-categories For Each subdir As String In Directory.GetDirectories(dir) ParseSnippetFolder(subdir, categoryName & " / ") Next End Sub
Dim reTitle As New Regex("<Title>(.+?)</Title>") Dim reShortcut As New Regex("<Shortcut>(.+?)</Shortcut>")
Sub ParseSnippetFile(ByVal snippetFile As String) Dim text As String = File.ReadAllText(snippetFile) ' We use regexes to extract information for individual snippet files. Dim maTitle As Match = reTitle.Match(text) Dim maShortcut As Match = reShortcut.Match(text) Dim title As String = maTitle.Groups(1).Value Dim shortcut As String = maShortcut.Groups(1).Value Console.WriteLine(" {0} [{1}]", title, shortcut) End Sub End Module
 Friday, November 11, 2005
I find it quite ironical that many developers spend hours to debate which language is the most efficient or productive, and yet forget to learn how to use the tool with which they spend most of their time: the Visual Studio IDE.
The best way to increase productivity with Visual Studio is to write macros that automate repetitive tasks. There are many commercial and freeware add-ins on the market, but I rarely find one that does exactly what I need. In cases like this I just write a macro, either from scratch or starting with a recorded macro that captures the actions that I want to repeat.
For example, I found that I typically prototype my classes with Public fields, but then I convert them to properties when I convert the prototype to the "real" code. The conversion process takes me a lot of time. To see what I mean, I typically start with a simple variable such as
' The name of the element
Public Name As String = "Francesco"
and I convert it into something like this:
' The name of the element Private m_Name As String = "Francesco"
Public Property Name() As String Get Return m_Name End Get Set(ByVal Value As String) m_Name = Value End Set End Property
At last, some months ago I decided to write a macro that automates this conversion. It took me about 30 minutes, but in these months it saved me hours. Here it is:
Imports EnvDTE Imports System.Text.RegularExpressions
Public Module CodeArchitectsMacros Dim repPattern As String Dim repPatternReadOnly As String
Sub ConvertVariables() ' Determine current language by looking at the extension of the current document. Dim doc As Document = DTE.ActiveDocument If doc Is Nothing Then Exit Sub Dim docName As String = doc.Name.ToLower()
' Read all the text lines touched by the selection. Dim sel As TextSelection = CType(DTE.ActiveDocument.Selection, TextSelection) Dim ed1 As EditPoint = sel.AnchorPoint.CreateEditPoint() ed1.EndOfLine() : ed1.StartOfLine() : ed1.StartOfLine() Dim ed2 As EditPoint = sel.BottomPoint.CreateEditPoint() ed2.EndOfLine() Dim text As String = ed1.GetText(ed2)
' The find and replacement pattern depend on the current language. Dim findPattern As String If docName.EndsWith(".vb") Then findPattern = "(?<indent>[\t ]+)Public\s+(?<static>Shared\s+)?(?<readonly>ReadOnly\s+)?" _ & "(?<name>\w+)\s+As\s+(?<type>\S+)(?<init>.*?)\n" ' {0}=property name, {1}=property type, {2}=static keyword, {3} initvalue, ' {4}=CR-LF, {5}=Tab, {6}=indent repPattern = "{6}Private {2}m_{0} As {1}{3}{4}" _ & "{6}Public {2}Property {0}() As {1}{4}" _ & "{6}{5}Get{4}" _ & "{6}{5}{5}Return m_{0}{4}" _ & "{6}{5}End Get{4}" _ & "{6}{5}Set(ByVal Value As {1}){4}" _ & "{6}{5}{5}m_{0} = Value{4}" _ & "{6}{5}End Set{4}" _ & "{6}End Property{4}{4}" repPatternReadOnly = "{6}Private {2}ReadOnly m_{0} As {1}{3}{4}" _ & "{6}Public ReadOnly {2}Property {0}() As {1}{4}" _ & "{6}{5}Get{4}" _ & "{6}{5}{5}Return m_{0}{4}" _ & "{6}{5}End Get{4}" _ & "{6}End Property{4}{4}" ElseIf docName.EndsWith(".cs") Then ' Notice the (?.*;) element is needed to ensure that public fields are matched, ' but public properties aren't findPattern = "(?<indent>[\t ]+)public\s+(?<static>static\s+)?(?<readonly>readonly\s+)?" _ "(?<type>\S+)\s+(?<name>\w+)(?=.*;)(?<init>.*?)\n" ' {0}=property name, {1}=property type, {2}=static keyword, {3} initvalue, ' {4}=CR-LF, {5}=Tab, {6}=indent repPattern = "{6}private {2}{1} m_{0}{3}{4}" _ & "{6}public {2}{1} {0}{4}" _ & "{6}{{{4}" _ & "{6}{5}get {{ return m_{0}; }}{4}" _ & "{6}{5}set {{ m_{0} = value; }}{4}" _ & "{6}}}{4}{4}" repPatternReadOnly = "{6}private {2}readonly {1} m_{0}{3}{4}" _ & "{6}public {2}{1} {0}{4}" _ & "{6}{{{4}" _ & "{6}{5}get {{ return m_{0}; }}{4}" _ & "{6}}}{4}{4}" End If
' Replace the text. Add a trailing CR-LF but remove it later. Dim replaceText As String = Regex.Replace(text + ControlChars.CrLf, findPattern, _ AddressOf ReplaceWithProperty) ed1.ReplaceText(ed2, replaceText.Substring(0, replaceText.Length - 2), 0) End Sub
' Private callback function for the Replace method Private Function ReplaceWithProperty(ByVal m As Match) As String Dim pattern As String = repPattern If m.Groups("readonly").Length > 0 Then pattern = repPatternReadOnly Return String.Format(pattern, m.Groups("name").Value, m.Groups("type").Value, _ m.Groups("static").Value, m.Groups("init").Value, ControlChars.CrLf, _ ControlChars.Tab, m.Groups("indent").Value) End Function
End Module
Thanks to regular expressions, and in spite of the low amount of code it contains, this macro works both in VB and C#, it enables you to convert multiple fields in one shot, it preserves the field's initial value and even its static/Shared and Readonly attributes, and it also preserves any statement between variable declarations. In practice, therefore, you can just select the source code of an entire class and convert all its public fields into properties, with just a mouse click! 
For each property, the macro creates a variable named m_PropertyName; obviously you can use your favorite naming convention by editing the statement that assigns regPattern. C# developers can edit the code to generate multi-lined get/set blocks. (I prefer to have more compact blocks.)
 Sunday, November 06, 2005
C# 2.0 has Surround With command that enables you to wrap the selected code inside a if, for, foreach, while, #if (and a few mode) blocks. Some of the options are virtually useless - for example, I'd never wrap a piece of code in a class, interface, or enum block - but all in all it's a very handy command. Actually, it is so convenient that I decided to create a set of Visual Studio macros that add the same functionality to Visual Basic (both 2003 and 2005 editions) and C# 2003.
To install and use the macros listed below, add the following module to the Macro IDE, go back to Visual Studio, select a piece of code, open the WrappingMacros element in the Macro Explorer window, and double-click the macro you want to apply. In some cases, after applying the macro you'll also need to edit the generated code, for example to insert a condition in the If statement or the name of the #region you've created. Also, if you are working with C# you should manually reformat the selected code (by typing Ctrl+K, Ctrl+F), because for some reason the Edit.FormatSelection command works only in Visual Basic.
Even better, you should associate the macros you like most with a keyboard shortcut, so that you can apply the macro without opening the Macro Explorer window. If you need to list which shortcuts are available, have a look at yesterday's post.
Imports System Imports EnvDTE
Public Module WrappinglMacros
' -------------------------------------------------------------------- ' Wrap the selected code inside IF, TRY, etc. ' --------------------------------------------------------------------
Public Sub WrapIf() WrapCode("WrapIf", "If True Then\n$sel$End If\n", "if ( true )\n{\n\t$sel$}\n") End Sub
Public Sub WrapIfElse() WrapCode("WrapIfElse", "If True Then\n$sel$Else\n\nEnd If\n", "if ( true )\n{\n\t$sel$}\nelse\n{\n}\n") End Sub
Public Sub WrapTryCatch() WrapCode("WrapTryCatch", "Try\n$sel$Catch ex As Exception\n\nEnd Try", _ "try\n{\n$sel$}\ncatch (Exception ex)\n{\n}\n") End Sub
Public Sub WrapTryFinally() WrapCode("WrapTryFinally", "Try\n$sel$Finally\n\nEnd Try", "try\n{\n$sel$}\nfinally\n{\n}\n") End Sub
Public Sub WrapTryCatchFinally() WrapCode("WrapTryCatchFinally", "Try\n$sel$Catch ex As Exception\n\nFinally\n\nEnd Try", _ "try\n{\n$sel$}\ncatch (Exception ex)\n{\n}\nfinally\n{\n}\n") End Sub
Public Sub WrapRegion() WrapCode("WrapRegion", "#Region ""RegionName""\n\n$sel$\n#End Region", _ "#region RegionaName\n\n$sel$\n#endregion") End Sub
Public Sub WrapSharpIf() WrapCode("WrapSharpIf", "#IF True Then\n\n$sel$\n#End If", "#if true\n\n$sel$\n#endif") End Sub
Public Sub WrapFor() WrapCode("WrapFor", "For index As Integer = startIndex To endIndex\n$sel$Next", _ "for (int index = startIndex; i <= endIndex; index++)\n{\n$sel$}\n") End Sub
Public Sub WrapForEach() WrapCode("WrapForEach", "For Each obj As Object In collection\n$sel$Next", _ "foreach (object obj in collection)\n{\n$sel$}\n") End Sub
Public Sub WrapWhile() WrapCode("WrapWhile", "Do While True\n$sel$Loop", "while (true)\n{\n$sel$}\n") End Sub
Public Sub WrapDoWhile() WrapCode("WrapDoWhile", "Do\n$sel$Loop While True", "do\n{\n$sel$} while ( true );\n") End Sub
Public Sub WrapNamespace() WrapCode("WrapNamespace", "Namespace NamespaceName\n$sel$End Namespace", _ "namespace NamespaceName\n{\n$sel$} // end of namespace") End Sub
Public Sub WrapSelect() WrapCode("WrapSelect", "Select Case expression\nCase 0\n$sel$Case 1\nCase Else\nEnd Select\n", _ "switch ( expression )\n{\n\tcase 0:\n\t\tbreak;\n\tcase 1:\n\t\tbreak;\n\tdefault:\n\t\tbreak;\n}\n") End Sub
Public Sub WrapSyncLock() WrapCode("WrapIf", "SyncLock lockObject\n$sel$End SyncLock\n", "lock ( lockObject )\n{\n\t$sel$}\n") End Sub
' Helper method that replaces the selection with the specified templated text. ' The template can include $sel$ (the selected code) and escape sequences such as \r\n, \t Private Sub WrapCode(ByVal cmdName As String, ByVal vbTemplate As String, ByVal csTemplate As String) ' Determine the current language by looking at the extension of the current document. Dim doc As Document = DTE.ActiveDocument If doc Is Nothing Then Exit Sub Dim docName As String = doc.Name.ToLower() Dim sel As TextSelection = DirectCast(DTE.ActiveDocument.Selection, TextSelection) If sel Is Nothing Then Exit Sub
' Open an undo context. DTE.UndoContext.Open(cmdName) ' Retrieve the selected text, append a newline if necessary. Dim selText As String = sel.Text If Not selText.EndsWith(ControlChars.NewLine) Then selText &= ControlChars.NewLine
' Wrap the selected text, using either the VB or the C# command Dim template As String If docName.EndsWith(".vb") Then template = vbTemplate ElseIf docName.EndsWith(".cs") Then template = csTemplate End If
' Replace CR-LF, tabs, and the selected text Dim newText As String = Regex.Unescape(template).Replace("$sel$", selText) ' Reselect the text just added and format it. (Doesn't work in C#.) Dim ep As EditPoint = sel.TopPoint.CreateEditPoint() sel.Text = newText sel.MoveToPoint(ep, True) DTE.ExecuteCommand("Edit.FormatSelection") ' Close the undo context. DTE.UndoContext.Close() End Sub
End Module
 Saturday, November 05, 2005
When you create a macro and want to associate it with a shortcut key you face the problem of determining which shortcuts are available. At times this can become a time-consuming job, because Visual Studio has taken so many shortcuts for itself. More in general, how do you get the list of all the Visual Studio commands and their shortcuts, as defined in a given keyboard configuration?
The most obvious answer is "read the documentation, dude!", however the docs can't list the shortcuts that have been modified or added after installing Visual Studio. Microsoft provides the Keybindings Table Add-in, an add-in written in C++ that lists all key bindings (that is, the shortcuts associated with each command). But why should you install an add-in if you can achieve the same result with a macro that contains just a few lines of code? Here's a macro that creates a text file in the C:\ folder, with all the information you need:
Public Sub ShowKeyboardBindings() Dim sw As New System.IO.StreamWriter("c:\keybindings.txt") For Each cmd As Command In DTE.Commands For Each o As Object In cmd.Bindings sw.WriteLine(cmd.Name & " - " & o.ToString()) Next Next sw.Close() End Sub
The advantage of using a macro instead of the shrik-wrapped add-in is that you can modify its source code to display information in any format you like. For example, you might sort the result by the shortcut, so that you can immediately see which shortcuts are used and which are available. Or you can list only the commands that are available inside a given window, such as the code editor or the form designer.
If you aren't familiar with macros, here's how to proceed. Type Alt+F11 to bring up the Macro IDE, open a module under MyMacros (e.g. Module1) by clicking in the Project Explorer, then paste the previous code and type Ctrl+S to save. Close the Macro IDE and go back to Visual Studio, type Alt+F8 to display the Macro Explorer window, expand the MyMacros-Module1 node and double-click on ShowKeyboardBinding to run the macro. Done!
 Wednesday, November 02, 2005
Every now and then I discover a Visual Studio shortcut that doesn't correspond to any menu command. Not all these under-documented shortcuts are truly useful, but a few of them can help you accellerate common editing operations and save a fraction of a second. Adding one fraction of a second today and tomorrow, at the end of your programming career you will save two or three entire days. Maybe in those days it will be raining (quoting Woody Allen), but that's a different story Here are some of my discoveries:
Ctrl+L cuts the current line into the Clipboard CStrl+Shift+L deletes the current line without copying it in the Clipboard. Ctrl+C, if no text is currently selected, it copies the current line in the Clipboard. Ctrl+Enter creates an empty line above the current line. Ctrl+Shift+Enter creates an empty line below the current line.
Ctrl+Shift+T swaps the current word with the word on its right (word transpose). Ctrl+Alt+T swaps the current line with the next line (line transpose) thus you can easily move a line elsewhere in the listing
Ctrl+F3 finds the next occurrence of the searched text. Ctrl+Shift+F3 finds the previous occurrence of the searched text.
Ctrl+F10 invokes the Run to Cursor command in debug mode. Ctrl+Shift+F10 invokes the Set Next Statement command in debug mode. Ctrl+* (on the numeric keypad) invokes the Show Next Statement command.
Ctrl+PgUp e Ctrl+PgDn move to the previous and next toolwindow among those that are usually hosted near the right border of the IDE, enabling you to cycle among the Solution Explorer, Properties window, Class View window, etc. Alt+Shift+F6 e Alt+F6 move to the previous and next window among those hosted in the bottommost panel, enabling you to cycle among the Immediate, Command, Task List, and Threads window. Shift+Esc closes a toolwindow.
All these shortcuts are active if youselect the Visual Studio's default keyboard configuration. If you enabled a different keyboard configuration (e.g. Visual Basic), some of these shortcuts won't work. You can modify the keyboard configuration in the Keyboard page of the Tools-Options dialog box.
 Sunday, October 30, 2005
I often need to paste a text fragment as a comment in my source code. Unfortunately I can't simply paste the text and then use the Edit-Comment Selectio command. because the Visual Studio editor - at least when working with VB.NET - tries to interpret the pasted text as code and it ruins its formatting, adds or deletes characters, and so forth. In addition to this problem, when I am preparing samples for my books I need to revise all CR-LFs in the text, to wrap longer lines, because Microsoft Press standards mandate that lines aren't longer than 92 characters. All in all, it's a real nuisance.
A few weeks ago I decided to avoid this waste of time and wrote a macro that would do the pasting and the formatting for me. It's a simple and tiny way to increase productivity, that allows me to focus on the things that really matter. If you like tidy code listings, I am sure you'll find this macro useful.
The first problem I had to solve is a limitation of the Clipboard.GetObjectData method. When invoked from a macro, this method always returns Nothing, thus I needed a different way to read the text in the Clipboard. I can surely do this with an API call or by calling a method in a separate DLL, but I thought that reading the Clipboard from a macro shouldn't be that difficult. My next attempt was based on the Paste method of the TextBox control:
' Retrieve the text in the clipboard
Dim tb As New TextBox
tb.Multiline = True
tb.WordWrap = False
tb.ScrollBars = ScrollBars.Both
tb.Paste()
Dim text As String = tb.Text
This approach works nearly always. Every now and then, in fact, the Paste method fails with a cryptic message: "Class already exists". I noticed that this error occurs only if the macro editor is open, therefore during the normal use it doesn't cause much trouble. However, once you get this error, the only way to get rid of it is by restarting Visual Studio. When I posted this first solution on my Italian blog, reader Andrea Ferendeles suggested a different approach, based on the Paste method of the TextSelection object:
' Read the text in the clipbard, through the Selection.Paste method. Dim sel As TextSelection = DirectCast(DTE.ActiveDocument.Selection, TextSelection) Dim sp As EditPoint = sel.ActivePoint.CreateEditPoint() sel.Paste() ' Select and read the text just pasted, then delete it
sel.MoveToPoint(sp, True) Dim text As |