 Thursday, December 08, 2005
 |
Last spring I co-authored this book, Practical Guidelines and Best Practices for Microsoft Visual Basic .NET and Visual C# Developers, arguably the longest title in Microsoft Press's history. The book is a reasoned list of guidelines that all .NET developers should follow, actually is by far the largest collection of its kind you can find anywhere. It covers language syntax, memory usage, Windows Forms and ASP.NET applications, security, and more.
Unlike most other similar collections, though, we clearly divide the "rules" in guidelines (naming guidelines, comment usage, etc.) and best practices. The difference is subtle but important: most guidelines are primarily a style matter, whereas best practices impact the scalability, the speed, or the robustness of your application. This means that our guidelines are arbitrary and in fact we often offer alternate rules and clearly explain the pros and cons of each style.
You can learn more about the principles we used in the book's Introduction and in John Robbins's Foreword. (Unlike most foreword writers, John actually read each and every page in the manuscript and gave us some great advice about improving it.) Or click the figure to jump to the book's home page, where you can read three sample chapters and download the book's source code.
Today I have uploaded a 30-page Word document that contains a summary of all the rules covered in the book, orderly grouped by topic and with a reference where in the book each rule is explained. You can edit this document as you see fit, delete or edit the guidelines you aren't interested in, and so forth. We routinely use this document in internal code reviews or when we consult at customers' places, so we hope it will be useful to you as well.
P.S. You must register to access this material. We swear we'll never send you anything that vaguely resemble spamming, just 100% technical contents!
|
 Saturday, December 03, 2005
Consider the following code, that converts all the elements of an Int32 array into the corresponding hex value:
' VB Dim intArray() As Integer = {4, 6, 9, 10, 99, 233, 34, 88, 189} Dim hexArray(intArray.Length - 1) As String For i As Integer = 0 To intArray.Length - 1 hexArray(i) = intArray(i).ToString("X") Next
// C# int[] intArray = {4, 6, 9, 10, 99, 233, 34, 88, 189}; string[] hexArray = new string[intArray.Length]; for ( int i = 0; i < intArray.Length; i++) { hexArray[i] = intArray[i].ToString("X"); }
The question is: how can you make this code more concise in .NET 2.0? The first answer that might come up is to use the Array.ConvertAll method together with a C#'s anonymous method:
string[] hexArray = Array.ConvertAll<int,string>(intArray, new Converter<int,string>( delegate(int n) { return n.ToString("X");}));
Actually, you can write even more concise code if you remember than the Microsoft.VisualBasic library already contains the Hex method, which matches the signature of the Converter<int,string> delegate. Using this method and delegate inference, you can shrink the code to:
' VB Dim hexArray() As String = Array.ConvertAll(Of Integer, String)(intArray, AddressOf Hex) // C# string[] hexArray = Array.ConvertAll<int,string>(intArray, Microsoft.VisualBasic.Conversion.Hex );
I am certain that few C# developers will use this trick, but I thought it was worth mentioning. (Of course, you must add a reference to the Microsoft.VisualBasic.dll assembly if you work with C#.) The key idea, however, is that in some cases you don't need to write an anonymous method to accomplish a given task, because often you can find what you're looking for in the .NET Framework. For example, you can display all the elements of an array in the Console window with just one statement:
' VB Array.ForEach(hexArray, AddressOf Console.WriteLine) // C# Array.ForEach(hexArray, Console.WriteLine);
There are many other methods in the VB library that you can use to convert all the elements of an array or a generic List, including UCase, LCase, LTrim, RTrim, and Trim.
 Thursday, December 01, 2005
One of the .NET Framework features that fascinate me most is regular expressions, which I often use to simplify and speed up my applications. Well, at least this is what I believed until some time ago, when I was busy writing the forthcoming Programming Microsoft Visual Basic 2005: The Language (due in mid-January). This book is a core reference on the VB language and includes a section on the LIKE operator, which in recent years a overlooked in favor of regexes. I (mistakenly) assumed that the Like operator internally used the Regex classes, therefore surely it would have been slower. After all these years, I should have learned that I should never jump to conclusions without testing and benchmarking my code accurately. .
Let's say that you must check that a string has 9 characters, the first of which must be an uppercase "A" and the last four chars must be digits. This is how you'd perform this test with a regex:
Dim re As New Regex("^A....\d\d\d\d$")
and here's the version that uses the Like operator:
If teststring Like "A????####" Then Match = True
Surprise! Putting this code in a loop (but leaving the creation of the regex out of the loop) and using a string that makes the test succeed (e.g. "ABCDE1234"), the Like operator is about 4 times faster than the regular expression. Not bad, uh? But the biggest surprise came when I benchmarked the same test based on methods of the System.Char class exclusively:
If teststring.Length = 9 AndAlso teststring.Char(0) = "A"c AndAlso Char.IsDigit(teststring.Char(5)) Then AndAlso Char.IsDigit(teststring.Char(6)) AndAlso Char.IsDigit(teststring.Char(7)) AndAlso Char.IsDigit(teststring.Char(8)) Then match = True
Despite of its length, this last test is about five times faster than the Lik operator, and therefore about 20 times faster than the regexes! The gap gets closer if using compiled regexes, but the System.Chars approach is by far the fastest of the lot.
The bottom line: (1) if you write VB code, use the Like operator instead of regexes if the condition isn't too complex, and (2) regardless of the language you work with, if you really want the highest performance, use the methods of the String and Char types, if the search operation isn't too complex.
 Friday, November 25, 2005
Every now and then I get an email from a reader or a customer, who asks for clarifications on object finalization and disposing. As far as I know, the best article on this topic is this essay by Joe Duffy. It's over 25-page long, covers both .NET 1.1 and 2.0, and includes comments from gurus such as Jeffrey Richter e Chris Brumme. This is easily the definitive article on this topic and I urge you to read it if you haven't already.
The Dispose-Finalize pattern is objectively a complex matter. However, in most cases it can be simplified significantly if you use the following approach: (1) the class with the Dispose/Finalize method should wrap only one single unmanaged resource, and (2) this finalizable class should be private and nested inside another disposable (but not finalizable) type. The outer class is the only class that can use the finalizable class.
This simple trick enables the GC to immediately release all the memory used by the wrapper (disposable) class even in the worst case - that is, if the client code omits to invoke the Dispose method - and simplifies the structure of the type that uses the unmanaged resource. A listing is worth one thousand words, thus here is the C# version of what I mean:
// the class that clients use to work with the unmanaged resource class WinResource : IDisposable { // private field that creates a wrapper for the unmanaged resource private UnmanagedResourceWrapper wrapper = null; // this is true if the object has been disposed of bool disposed = false;
public WinResource(string someData) { // allocate the unmanaged resource here wrapper = new UnmanagedResourceWrapper(someData); }
// a public method that clients call to work with the unmanaged resource public void DoSomething() { // throw if the object has been already disposed of if ( disposed ) throw new ObjectDisposedException("");
// this code can pass the wrapper.Handle value to API calls. // ... }
public void Dispose() { // avoid issues when multiple threads call Dispose at the same time. lock ( this ) { // do nothing if already disposed of if ( disposed ) return; // dispose of all the disposable objects used by this instance // including the one that wraps the unmanaged resource // ... wrapper.Dispose(); // remember this object has been disposed of disposed = true; } } // the nested private class that allocates and release the unmanaged resource private sealed class UnmanagedResourceWrapper : IDisposable { // an invalid handle value, that the wrapper class can use to check // whether the handle is valid public static readonly IntPtr InvalidHandle = new IntPtr(-1);
// a public field, but accessible only from inside the WinResource class public IntPtr Handle = InvalidHandle;
// the constructor takes some data and allocates the unmanaged resource (eg a file) public UnmanagedResourceWrapper(string someData) { // this is just a demo... this.Handle = new IntPtr(12345); }
// the Dispose method can be invoked only by WinResource class public void Dispose() { Dispose(true); GC.SuppressFinalize(this); }
// the finalizer ~UnmanagedResourceWrapper() { Dispose(false); }
// This is where the unmanaged resource is actually disposed of. // Notice that it takes an argument only for compliance with .NET coding standards // but the disposing argument is never used, because in all cases this class // can access and release only the single unmanaged resource it wraps. private void Dispose(bool disposing) { // exit now if this object didn't completed its constructor correctly if ( this.Handle == InvalidHandle ) return; // release the unmanaged resource // eg. CloseHandle(Handle); // finally, invalidate the handle this.Handle = InvalidHandle; } } }
Notice that, if the unmanaged resource must interact with other fields, this interaction should be taken care of inside the WinResource class, not in the nested class. The UnmanagedResourceWrapper works only as a wrapper for the handle and shouldn't contain other fields or methods, besides those shown in the above listing. The code in the WinResource class must coordinate all the resources being used, both managed and unmanaged ones, and must release all of them in its Dispose method. But if the client code omits to call the Dispose method, the destructor in the nested class will orderly release the unmanaged resource during the next garbage collection.
Let's see all the advantages of this simplified approach.
- The requirement that you shouldn't access reference fields from inside the Finalize method is automatically satisfied, because the only field of the UnmanagedResourceWrapper type is a handle (a value type).
- If the client code omits to invoke the WinResource.Dispose method before the WinResource object goes out of scope, the WinResource object is removed from the heap anyway at the first GC; only the few bytes used by the UnmanagedResourceWrapper object survive in the heap and will be promoted to generation 1 or 2. Therefore this technique is more efficient than writing a single finalizable object that allocates both managed and unmanaged resource.
- The UnmanagedResourceWrapper class is private and you can't inherit from it, therefore you can mark it as sealed. This means that you never have to worry about the Dispose/Finalize pattern in derived classes - a topic on which tons of digital ink has been spilled. It is possible to inherit from WinResource as you'd do with disposable class, therefore there are no limitations in this respect. (It's exactly like when you inherit from other disposable classes such as FileStream.)
- The UnmanagedResourceWrapper is private and nested in another type and it isn't possible to achieve a refernence to one of its instances; therefore, a client can't "resurrect" a UnmanagedResourceWrapper object during the finalization step, a technique that is rarely useful and often dangerous. (Even though I show in my Programming Visual Basic .NET book how you can use it to implement an object pool.)
- The UnmanagedResourceWrapper constructor performs a single "atomic" action; if this action fails, the value of the handle is still qual to InvalidHandle, therefore the code in the Finalize method can detect this special value and do nothing in that case. There are only two cases: either the unmanaged resource has been correctly allocated or an exception prevented it from being created, and you don't have to worry about an object that has been built only partially because of an exception in its constructor.
- Many other recommendations related to the Dispose/Finalize pattern become void, such as the one that dictates that you should neither write finalizers inside structures nor calling virtual methods from inside the finalizer. In fact, the UnmanagedResourceWrapper class is sealed and has no virtual methods. Nor do you have to worry about versioning issues.
- Another advantage: the UnmanagedResourceWrapper class is so simple and generic that you can ofter reuse it as-is (or with minor edits) inside other classes, by means of a plain copy-and-paste action. Being a nested class, you don't even need to change its name to avoid name collisions.
I am sure that in some cases this simplified pattern can't be used, though it always worked well in my applications. I believe that it's quite odd that this simplified approach is rarely mentioned in articles and books on this topic.
Technical matters aside, I think that another kind of consideration about the Dispose/Finalize pattern is in order.
In my opinion, it is essential to put a lot of emphasis on the fact that the Dispose/Finalize pattern should be used only when your type invokes unmanaged code that allocates unmanaged resources (including unmanaged memory) and returns an handle that you must use eventually to release the resource. If the unmanaged resource is already wrapped by a .NET object (e.g. a FileStream or a SqlConnection) or a COM object, the .NET class that uses the resource must implement IDisposable, not the finalizer. And you must implement the Finalize method only if your code assigns the handle to a class-level field. If the handle is assigned to a local variable and the unmanaged resource is released before exiting the method - possibly in the Finally section of a Try block - you don't even have to implement the IDisposable interface. One of the few exceptions to this rule is when your managed code allocates unmanaged memory directly, by means of the System.Runtime.InteropServices.Marshal type.
I talked to many developers who believe that, in doubt, they should always implement the Finalize method, just in case. This is a common mistake. Defining a finalizable class without a real reason to do so can hurt performance, because the CLR takes slightly longer to allocate finalizable objects, because it has to register them in the f-reachable queue. And in the worst case - that is if the caller omits to call to the Dispose method - a finalizable object can have even more impact on performance, because it will be promoted to a higher generation without any real reason for such an overhead.
To recap: when do you really need to implement the Finalize method? Thinking of all the commercial apps I worked on in these years, I'd say that I used this pattern no more than 4 or 5 times. For sure, I used it more frequently in books and articles than in the real world. 
 Tuesday, November 22, 2005
I work with Microsoft Press since 1998 and I wrote as many as 5 books for them (plus 3 more books I am working on right now). Every three months I get a check with my royalties from my US books and the translation rights for versions published elsewhere in the world, but without specifying which languages the books have been translated into.
For some reason I always forgot to ask for a list of the languages my books had been translated into, until a couple of months ago, when by acquisition editor made a search and returned this the following list. Every now and then, readers ask whether the book has been translated to their language, therefore I decided to post the information here.
Programming Microsoft Visual Basic 6 : English, Italian, Japanese, Korean, Spanish, Chinese (simplified, China), Chinese (traditional, Taiwan) + local English-language version in India.
Programming Microsoft Visual Basic .NET : English, Italian, French, Arabic, Japanese, Korean, Spanish, Chinese (simplified), Chinese (traditional) + local English-language versions in China and India.
Applied Microsoft .NET Framework Programming in Microsoft Visual Basic .NET (with Jeffrey Richter): English, Italian, Korean, Chinese (simplified), Chinese (traditional) + local English-language in China.
Programming Microsoft Visual Basic .NET Version 2003 : English, Italian.
Practical Guidelines and Best Practices: English, Italiano, Russian + local English-language in China.
I can't help admitting that being translated into as many as ten languages is truly thrilling. 
 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.
 Friday, November 18, 2005
Let's consider the following code, which represents a typical situation: you are inside a nested loop and you want to exit both loops when a condition is true:
For i As Integer = 1 To 10 Dim exiting As Boolean = False For j As Integer = 1 To 20 ' If the Evaluate function returns zero you want to exit both loops If Evaluate(i, j) = 0 Then exiting = True Exit For End If ' Do something here Next If exiting Then Exit For Next
It isn't important to understand what the Evaluate function does, just consider that when this function returns zero you must exit both loops. The above code isn't optimized, because it repeatedly tests the exiting variable. You might optimize the loop by using a Goto statement that points to a label following the second Next keyword, but educated programmers don't use Gotos, right? So, the question is simple: how can you simplify this code and optimize it at the same time by dropping the exiting variable?
The solution is simple, and is based on the fact that Visual Basic supports as many as three different kinds of loops: For, Do, and While. Each kind of loop supports a corresponding Exit keyword (Exit For, Exit Do, and Exit While), thus you can rewrite the code as follows:
Dim i As Integer = 1 Do While i <= 10 For j As Integer = 1 To 20 If Evaluate(i, j) = 0 Then Exit Do ' Do something here Next i += 1 Loop
You can use the same technique when you have up to three nested loops.
Incidentally, you can't adopt this technique in C#, because its break statement doesn't have the same "semantics power" of the Exit keyword in VB.
 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}" &nb |