You can track the number of projects, files, and lines of code in any given solution implemented with VS.NET. Just invoke a built-in, lines-of-code counter in VS.NET and implement it as a macro that other teams can share.
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.