George Lucas was quoted as saying that he originally envisioned Star Wars as a series of nine movies. The first movie (self-titled) came out in 1976, followed by The Empire Strikes Back and Return of the Jedi. Almost thirty years since the first movie was released, prequels 1, 2, and 3 came out, which makes the first three movies episodes 4, 5, and 6—I guess. It’s all sort of confusing.
This is the third and final installment of the Prime Programming Proficiency series—and you didn’t have to wait 30 years for it. The first installment discussed heuristics and how you can use lines of code as a rudimentary starting place for establishing efficient software development. The subject led to many more questions than answers, but questions are good beginnings. The second installment introduced the extensibility object model and macros, setting the stage for implementing a built-in, lines-of-code counter using macros and VB.NET.
This final installment uses the Visitor behavior pattern to implement this lines-of-code counter. It includes the complete code listings, making the article a bit longish, but it will help you reproduce the solution.
The Problem and Solution Summarized
The problem is trying to track the number of projects, files, and lines of code in any given solution implemented with VS.NET. The solution—stated in Part 2 as a list of imperatives—is to invoke the utility in VS.NET and implement it as a macro that other teams can share. Finally, the utility displays running sub-totals and final totals (which include project and file names, project and file counts, and lines-of-code counts for source code files) when it has examined all files. This is manageable.
Introducing the Visitor Pattern
What are design patterns? I think of them as solution blueprints. Someone else had a problem, solved it successfully (and hopefully simplified some things), factored out domain-specific information, and then published his or her ideas. A design is an instance of one or more patterns, and software is an instance of a design. It might seem strange that software preceded design, which preceded patterns, but this is what happens with most if not all things. Innovation is followed by formalization, which is a precursor to mass production, automation, and finally, obsolescence.
The Visitor pattern is called a behavior pattern because it describes a solution for how something behaves. The Visitor pattern is useful when one encounters a problem where an action needs to be performed on a group of things. The action might be as simple as modifying a property or as complex as creating new objects. Think of an after-hours security guard making rounds at a government facility. Every hour, he has to ensure that all the doors are locked, a complicated and time-consuming operation considering that the facility has several floors, hundreds of doors, and multiple buildings. Now, suppose the security guard also has to check alarm systems, safes, parking facilities, and walk Mrs. Johnson to her car. The guard is still stopping at—or visiting—each thing, but what he does when he gets there is different depending on what he’s visiting. For instance, Mrs. Johnson doesn’t want to be jiggled and the door can’t be walked to the parking lot. If the guard’s duties were a programming solution, the Visitor pattern would separate the visiting behavior—or the making of rounds—from the action that occurs during the course of the visit.
Tip: Good designs are often based on metaphorical physical processes with distinct, well-known boundaries.
In this article’s problem domain, a solution, project, and file are all elements known to Visual Studio .NET. While we want to visit each, we want to do something different with each, based on its type. Because we implement our solution as a macro, we have certain constraints that must be implemented too, like a macro entry point. Let’s start there.
Defining the Macro Entry Point
Design patterns do not solve all of our problems. We might use many patterns in an application, using some patterns more than once. Other things do not require patterns. For example, a macro entry point simply is a subroutine that VS.NET requires; it has little to do with the Visitor pattern. The starting point for our solution is shown in Listing 1.
Listing 1: The macro starting point.
Option Explicit Off Imports EnvDTE Imports System.Diagnostics Imports System.Windows Imports System.Windows.Forms Imports System Imports System.IO Imports System.Text.RegularExpressions Imports System.text Public Module MyUtilities Public Sub DumpProject() Dim i As SolutionIterator = New SolutionIterator i.Iterate() End Sub End Module
Generally, I start working with objects and move to the user interface, which means I actually wrote the code in Listing 1 last. As you can see, the DumpProject method is very straightforward: It creates something called SolutionIterator and calls Iterate. Simple code like Listing 1 should be familiar to you; it is about the same level of complexity as the Main subroutine in Windows Forms and Console applications.
Listing 2 shows the SolutionIterator. This class and its Iterate method are also quite simple. Iterator creates an instance of a class called Visitor and a SolutionElement that is provided with the current solution.
Listing 2: The SolutionIterator class.
Imports EnvDTE Imports System.Diagnostics Public Class SolutionIterator Public Sub Iterate() Dim visitor As Visitor = New Visitor Dim solutionElement As SolutionElement = _ New SolutionElement(DTE.Solution.Projects) solutionElement.Accept(visitor) Output.WriteLine("Total Line Count: " & visitor.LineCount) Output.WriteLine("Total Files: " & visitor.ItemCount) Output.WriteLine("Total Projects: " & visitor.ProjectCount) End Sub End Class
After you create the solutionElement, you invoke an Accept method. When Accept returns, you should have your totals.
The pieces you need to create, as Listing 2 suggests, are the Visitor, SolutionElement class, and the Output class. The Output class is a wrapper for the VS.NET IDE’s Output window, and Visitor and SolutionElement are dependent on the Visitor pattern. Now, you are getting into the meat of the solution.
Implementing the IVisitor Interface
The Visitor pattern can be implemented using abstract classes or interfaces. I chose interfaces. The basic idea is that you define an interface with methods named Visit. For each kind of thing you want to Visit, you define an overloaded Visit method. For example, I want to the Visit SolutionElement, ItemElement, and ProjectElement types; hence, I declare a Visit method for each of these types in IVisitor. In addition, the Visitor is visiting each of these types and grabbing information from them. It will be up to the Visitor to store that information. In our example, we store project, item, and line counts. Therefore, our visitors also will need to collect and make accessible this information.
Listing 3 shows the definition of IVisitor and the implementation of an IVisitor, the Visitor class.
Listing 3: The IVisitor interface and Visitor class.
' IVisitor.vb Imports EnvDTE Imports System.Diagnostics Public Interface IVisitor Sub Visit(ByVal element As SolutionElement) Sub Visit(ByVal element As ItemElement) Sub Visit(ByVal element As ProjectElement) Property ProjectCount() As Long Property ItemCount() As Long Property LineCount() As Long End Interface ' Visitor.vb Imports EnvDTE Imports System.Diagnostics Public Class Visitor Implements IVisitor Private FProjectCount As Long = 0 Private FItemCount As Long = 0 Private FLineCount As Long = 0 Public Property ProjectCount() As Long Implements _ IVisitor.ProjectCount Get Return FProjectCount End Get Set(ByVal Value As Long) FProjectCount = Value End Set End Property Public Property ItemCount() As Long Implements _ IVisitor.ItemCount Get Return FItemCount End Get Set(ByVal Value As Long) FItemCount = Value End Set End Property Public Property LineCount() As Long Implements _ IVisitor.LineCount Get Return FLineCount End Get Set(ByVal Value As Long) FLineCount = Value End Set End Property Public Sub VisitProject(ByVal element As ProjectElement) _ Implements IVisitor.Visit element.Iterate() End Sub Public Sub VisitProjectItem(ByVal element As ItemElement) _ Implements IVisitor.Visit element.Iterate() End Sub Public Sub VisitProjects(ByVal element As SolutionElement) _ Implements IVisitor.Visit element.Iterate() End Sub End Class
I hope you appreciate just how simple the code is. Individual pieces of code should be pretty simple. This supports the idea of divid et imperum, or divide and conquer. To divide and conquer a problem means to subdivide it into smaller, easier-to-implement pieces.
As you will see shortly, each of the prefixSolution classes implement an Iterate method that supports traversing that node’s solution and project directory-based file system. For example, a ProjectItem might be a folder that contains other folders or files that require traversing that ProjectItem’s elements.
Implementing the IHost Interface
A good technique for designing solutions is to state the object and pluck out the nouns and verbs. The nouns will tell you what is acting, what is acted upon, and what the actions are. In our implementation, we simply state that hosts are something that accept visitors. Plucking the nouns and verbs we have host, visitor, something, and accept.
Something is suitable here because all kinds of things play the role of host and visitor. The current confluence in my lungs suggests that I am hosting (albeit unwanted guests) some very small visitors. Your grandmother may play host to your family during the holidays, and when my kids forgot to do laundry for a week while I was on a recent business trip my dryer vent was host to a swallow. Because something and patterns in general are generic, the word something suggests we use an interface. Hosts accept visitors, resulting in the culmination of an interface (IHost) with a method (Accept) that requires an IVisitor (see Listing 4 for an implementation of IHost).
Note: Some implementations of the visitor pattern might use Visitor and Visitable. This has a nice symmetry, but Visitor and Host sound a little better. Precise naming is not important. Clarity is sufficient.
Listing 4: The IHost interface.
Imports EnvDTE Imports System.Diagnostics Public Interface IHost Sub Accept(ByVal visitor As IVisitor) End Interface
Every class that accepts a visitor needs to implement IHost. As mentioned previously, the Host is capable of doing or knowing something about the Visitor’s wants and needs. In this example, the visitors want to gather count heuristics from each host.
Implementing hosts
The extensibility object model was not implemented with the Visitor pattern. Thus to make ProjectItem, Project, and Projects (part of the extensibility object model) work with our pattern, we need to wrap these elements in classes that do work with the Visitor, that do implement IHost. I defined the following three classes for this purpose:
- SolutionElement represents the wrapper for a solution, containing a reference to the extensibility object model’s Projects collection.
- ProjectElement represents the wrapper for a Project.
- ItemElement wraps ProjectItem.
Listings 5, 6, and 7 show the implementation of each class, respectively. Each listing is followed by a brief summary showcasing the finer points.
Listing 5: The SolutionElement.
Imports EnvDTE Imports System.Diagnostics Public Class SolutionElement Implements IHost Private FProjects As Projects Private FVisitor As IVisitor Public Sub New(ByVal projects As Projects) FProjects = projects End Sub Public ReadOnly Property Projects() As Projects Get Return FProjects End Get End Property Public Sub Accept(ByVal visitor As IVisitor) _ Implements IHost.Accept FVisitor = visitor visitor.Visit(Me) End Sub Public Sub Iterate() Output.Clear() Output.WriteLine(DTE.Solution.FullName) Dim project As Project Dim projectElement As ProjectElement = New ProjectElement For Each project In FProjects projectElement.CurrentProject = project projectElement.Accept(FVisitor) Next End Sub End Class
Listing 6 is the SolutionElement. It implements IHost and is initialized with a Projects collection when the constructor—Sub New—is called. The Accept method implements IHost and accepts a visitor. The Iterate method does the interesting work. Iterate clears the Output device (see the Implementing the Output Window section later in this article), writes the name of the current solution, and then iterates over each Project in the Projects collection.
Notice that I create only one instance of ProjectElement and reuse it inside the For Each loop. Because it is a wrapper and its state is transient, this conservative code will reduce memory chunking. Within the loop, the ProjectElement is told which ProjectItem it currently represents and then the ProjectElement is visited.
Listing 6: The ProjectElement.
Imports EnvDTE Imports System Imports System.Diagnostics Public Class ProjectElement Implements IHost Private FProject As Project Private FVisitor As IVisitor Public Sub New() End Sub Public Sub New(ByVal Project As Project) FProject = Project End Sub Public Sub Accept(ByVal visitor As IVisitor) _ Implements IHost.Accept FVisitor = visitor visitor.Visit(Me) End Sub Public Property CurrentProject() As Project Get Return FProject End Get Set(ByVal Value As Project) FProject = Value End Set End Property Public Sub Iterate() Debug.Assert(FProject Is Nothing = False) Try Output.WriteLine("Project: " & FProject.Name) Catch Output.WriteLine("Project: <no name>") End Try If (FProject.ProjectItems Is Nothing) Then Return FVisitor.ProjectCount += 1 Dim item As ProjectItem Dim itemElement As ItemElement = New ItemElement For Each item In FProject.ProjectItems itemElement.CurrentItem = item itemElement.Accept(FVisitor) Next End Sub End Class
The ProjectElement represents an extensibility object model Project. Again, we implement IHost and call Accept to visit our host. The ProjectElement host also implements Iterate. At this level of granularity, we have more work to do. We output the project name, increment the visitor’s ProjectCount, and then examine each ProjectItem using the ItemElement wrapper.
Listing 7: The ItemElement.
Imports EnvDTE Imports System Imports System.Diagnostics Public Class ItemElement Implements IHost Private FProjectItem As ProjectItem Private FVisitor As IVisitor Public Sub New() End Sub Public Sub New(ByVal item As ProjectItem) FProjectItem = item End Sub Public Property CurrentItem() As ProjectItem Get Return FProjectItem End Get Set(ByVal Value As ProjectItem) FProjectItem = Value End Set End Property Public Sub Accept(ByVal visitor As IVisitor) _ Implements IHost.Accept FVisitor = visitor visitor.Visit(Me) End Sub Public Sub Iterate(Optional ByVal Indent As String = " ") Iterate(FProjectItem, Indent) End Sub Private Sub Iterate(ByVal item As ProjectItem, _ Optional ByVal Indent As String = " ") Try Output.Write(Indent & "Name: " & item.Name) UpdateLineCount(item) FVisitor.ItemCount += 1 If (FProjectItem.ProjectItems Is Nothing = False _ Or FProjectItem.ProjectItems.Count > 0) Then Dim child As ProjectItem For Each child In item.ProjectItems Iterate(child, New String(" ", Indent.Length + 2)) Next End If Catch e As Exception Debug.WriteLine(e.Message) End Try End Sub Private Function GetLineCount(ByVal item As ProjectItem) ' "{6BB5F8EE-4483-11D3-8BCF-00C04F8EC28C}" If (item.Kind = EnvDTE.Constants.vsProjectItemKindPhysicalFile) _ Then Return LineCounter.GetLineCount(item) Else Return 0 End If End Function Private Sub UpdateLineCount(ByVal item As ProjectItem) Try Dim count As Long = GetLineCount(item) Output.WriteLine(String.Format("({0})", count)) FVisitor.LineCount += count Catch Output.WriteLine("(0)") End Try End Sub End Class
ItemElement performs the most work. Again, it is a host with an Accept method and we want to iterate the ItemElement. Keep in mind that projects may have folders (sub-projects), so we have to look out for nested project items.
Iterate writes the ProjectItem name and updates the line count to include the ProjectItem contained in the ItemElement wrapper. Next, we update the visitor’s ItemCount. After we have performed the information-gathering steps, we need to see whether this element has sub-elements. If the ProjectItem has sub-projects, we use recursion to examine those elements.
Two more methods are important to note: GetLineCount and UpdateLineCount. UpdateLineCount calls GetLineCount to get the actual number of lines, output that result, and add the count to the visitor’s tally. GetLineCount uses the LineCounter class I implemented, which I cover in the next section.
Implementing the Line Counter
The LineCounter is a completely separate class. As a result, I can stub it out with an imperfect implementation and improve it at a later date or outsource it to someone who may specialize in the counting of source lines of code. Because the LineCounter (see Listing 8) does not know about the visitor and hosts, another developer will not need my entire implementation to do his or her job, implementing the LineCounter.
Listing 8: The LineCounter class sub-divides lines of a separate utility.
Imports EnvDTE Imports System Imports System.IO Imports System.Diagnostics Imports System.Text.RegularExpressions Public Module LineCounter Public Function GetLineCount(ByVal Item As ProjectItem) If (Not IsValidItem(Item)) Then Return 0 Return CountLines(Item) End Function Private Function IsValidItem(ByVal item As ProjectItem) As Boolean Return IsValidItem(item.Name) End Function Private Function IsValidItem(ByVal FileName As String) As Boolean Return Regex.IsMatch(FileName, "^w+.cs$") Or _ Regex.IsMatch(FileName, "^w+.ascx.cs$") _ Or Regex.IsMatch(FileName, "^w+.aspx.cs$") Or _ Regex.IsMatch(FileName, "^w+.aspx$") _ Or Regex.IsMatch(FileName, "^w+.ascx") End Function Private Function CountLines(ByVal item As ProjectItem) As Long Try Return DoCountLines(item) Catch ex As Exception Return DoManualCount(item.FileNames(1)) End Try End Function Private Function DoManualCount(ByVal FileName As String) As Long Dim reader As TextReader = New StreamReader(FileName) Dim all As String = reader.ReadToEnd() Return Regex.Matches(all, vbCrLf).Count() End Function Private Function DoCountLines(ByVal item As ProjectItem) As Long Debug.Assert(IsValidItem(item)) Dim Count As Long = 0 Open(item) Try Dim s As TextSelection = item.Document.Selection() StoreOffset(s) s.EndOfDocument() Count = s.ActivePoint.Line() RestoreOffset(s) Finally Close(item) End Try Return Count End Function Private WasOpen As Boolean = False Private Current As Long = 0 Private Sub Open(ByVal item As ProjectItem) WasOpen = item.IsOpen If (Not WasOpen) Then item.Open() End Sub Private Sub Close(ByVal item As ProjectItem) If (Not WasOpen) Then item.Document.Close() End If End Sub Private Sub StoreOffset(ByVal selection As TextSelection) Current = selection.ActivePoint.Line End Sub Private Sub RestoreOffset(ByVal selection As TextSelection) If (WasOpen) Then selection.MoveToLineAndOffset(Current, 0) End If End Sub End Module
Oddly enough, the extensibility object model does not seem to have a straightforward property for getting the number of lines in a project item. (Perhaps a means of doing this exits, but I just can’t find it.)
Two methods in this class try to count the number of lines: DoCountLines and DoManualCount. DoCountLines uses the ProjectItem passed to the constructor, opens the file represented by ProjectItem, stores the current position in the file, moves the cursor to the end of the document, and then asks what the active line is. Finally, it restores the offset and closes the ProjectItem. Both Open and Close take into account whether the ProjectItem was already opened or not and close only ProjectItems that the class opened.
If an exception occurs in DoCountLines, the caller, CountLines, catches the exception and calls DoManualCount. DoManualCount opens the ProjectItem and attempts to count carriage return and line feed pairs. DoManualCount is a lot slower then DoCountLines, but together they seem to form a resilient pair.
Note: Kevin McFarlane sent me an e-mail that mentions Oz Solomnovich’s Project Line Counter add-in. You should be able to find the source here: http://wndtabs.com/plc/. I haven’t looked at the source, but the GUI looks good.
Implementing the Output Window
The last piece is the Output class. The extensibility object model has an OutputWindowPane. Again, I wrapped the existing OutputWinowPane to add some convenience methods for my specific purposes. Listing 9 shows the code.
Listing 9: A Wrapper for the OutputWindowPane.
Imports EnvDTE Imports System.Diagnostics Public Class Output Private Shared FOutputWindow As OutputWindowPane Shared Sub New() FOutputWindow = GetOutputWindowPane("Project Utility") End Sub Public Shared ReadOnly Property Output() As OutputWindowPane Get Return FOutputWindow End Get End Property Public Shared Sub Clear() FOutputWindow.Clear() End Sub Public Shared Sub Write(ByVal Text As String) FOutputWindow.OutputString(Text) End Sub Public Shared Sub WriteLine(ByVal Text As String) FOutputWindow.OutputString(Text & vbCrLf) End Sub Shared Function GetOutputWindowPane(ByVal Name As String, _ Optional ByVal show As Boolean = True) As OutputWindowPane Dim win As Window = _ DTE.Windows.Item(EnvDTE.Constants.vsWindowKindOutput) If show Then win.Visible = True Dim ow As OutputWindow = win.Object Dim owpane As OutputWindowPane Try owpane = ow.OutputWindowPanes.Item(Name) Catch e As System.Exception owpane = ow.OutputWindowPanes.Add(Name) End Try owpane.Activate() Return owpane End Function End Class
I borrowed GetOutputWindowPane from the Samples.Utilities module that ships with VS.NET. This window is the Output window in VS.NET, and you can supply your own pane with a suitable title. In the example, we name it Project Utility. The rest of the wrapper methods orchestrate clearing or sending text to the window pane.
Build Your Skills Base
Programming requires a huge amount of knowledge. We learn about object models, grammars, libraries and third-party tools, patterns, refactoring, algorithms, threading, database design, testing tools, delegates, events, XML, stylesheets, source control tools, and much more. It is easy to forget how much the average programmer has to know to create even a “Hello World” application.
I hope this three-part series helps you see how many of these skills are tied together to create a whole. Still, all of these skills may only make one a competent programmer. The kind of talent that creates a thing of beauty and artistry is rare indeed and very difficult to attain.
(Eventually, I will get the source for this example posted on my Web site at http://www.softconcepts.com.)
Biography
Paul Kimmel is the VB Today columnist, has written several books on .NET programming, and is a software architect. You may contact him at pkimmel@softconcepts.com if you need assistance or are interested in joining the Lansing Area .NET Users Group (glugnet.org).
Copyright © 2004 by Paul Kimmel. All Rights Reserved.