Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

.NET

Reducing Dependencies in .NET Development


Jan02: Reducing Dependencies in .NET Development

A utility that graphically displays project dependencies

Scott is a software developer in Hagerstown, Maryland, and can be contacted at [email protected].


Have you ever worked on an application where changing one line of code results in a massive rebuild of the system? To avoid situations such as this, you clearly need to understand the project dependencies before you can begin improvements. However, development environments rarely offer tools to view all of the project relationships at once. Faced with this lack of tools, I wrote (in C#) a utility called "GDEPENDS" that graphically displays project dependencies parsed from Microsoft Visual Studio files. In this article, I show how GDEPENDS leverages .NET Framework features, including COM interoperability, form-based GUI design, extensible metadata, and the .NET class library.

There are several benefits to understanding project dependencies of large applications. Reducing dependencies can shrink build time and improve reusability. In addition, unit testing and regression testing are easier to manage. (For more information, see Large-Scale C++ Software Design, by John Lakos, Addison-Wesley, 1996.)

Visual Studio lets you group a collection of projects into a workspace (Visual Studio 6) or solution (Visual Studio.NET). Both let you create project dependencies when one project needs the build artifacts of a second project to compile or link. In the not too distant past, these dependencies arose when one project needed a DLL import library from another project. With COM, these dependencies involved MIDL-generated header files and type libraries. In .NET, a project relies on other "assemblies." The rich amount of metadata stored in an assembly replaces header files and other techniques previously used to export information.

I had a few simple design goals when starting GDEPENDS. While I mainly needed to parse and analyze Visual Studio 6 and Visual Studio.NET files, I also wanted an extensible design to add new file types with a minimum of effort. Furthermore, I wanted to graph the results of a parse, perhaps using third-party tools or libraries, instead of writing layout code myself.

The Base Class

DependencyAnalyzer (from DAnaylzer.cs) is an abstract class with two public methods: Parse and Display. Parse is an abstract method, and in C# the abstract keyword makes a method virtual. It is the responsibility of a derived class to implement Parse and build a directed graph of projects and their dependencies.

There are two popular techniques to choose from when building a directed graph. Adjacency lists use a collection of linked lists, while adjacency matrices are built on a two-dimensional array. Since most of the parsing algorithms in GDEPENDS need to find a project by name, I chose to use an adjacency list implementation built on a hash table.

DependencyAnalyzer contains a protected member variable of type Hashtable from the System.Collections namespace. Each entry in a Hashtable contains a key-value pair. In this case, the key is a project identifier (a name or a GUID), and the value is an instance of DependencyNode. A DependencyNode maintains some basic properties and display attributes for a dependency and uses an ArrayList to hold references to the other DependencyNodes in the Hashtable.

COM Interoperability

Originally, I planned to use only .NET technologies for GDEPENDS, perhaps porting some directed graph-drawing algorithms to C#. However, I couldn't stop thinking about all of the shapes and page layout capabilities available for COM automation clients from Microsoft Visio. Since COM is so deeply entrenched in current Microsoft tools, it was no surprise to find robust support for integrating COM components into managed .NET applications.

Using COM components from .NET begins by generating an interop assembly from a COM type library. The .NET IDE automatically generates an interop assembly for COM libraries when users add a project reference to a COM type library. Alternatively, the type library import utility (tlbimp.exe) offers more control (via command-line parameters) to set filenames, namespaces, and other options. Finally, the TypeLibConverter class from System.Runtime.InteropServices offers a programmatic means to create the interop assembly.

The metadata inside the interop assembly lets .NET create a run-time callable wrapper class (RCW) for .NET objects to use. The RCW marshals calls from the .NET-managed environment into the COM component, and manages the COM component lifetime via AddRef and Release. COM interop does come with a price, as one TechEd session reported an overhead of 50-70 instructions for each call into a COM component.

GDEPENDS uses Visio 2002 and the embedded Visio type library in vislib.dll. Although Visio 2002 now supports XML as a native format, I found the automation approach more intuitive. With the interop assembly in place, Visio interfaces appear as native .NET components to C#.

Visio Automation

Listing One is source code for the DependencyAnalyzer class. The algorithm for Display is to iterate through the Hashtable of DependencyNodes to place a shape into Visio for each node, then reiterate the Hashtable, drawing a connector from each node's shape to any of the node's dependency shapes. The flexibility of the algorithm is in the protected virtual functions used to do the automation. For instance, a derived class can override the DropShape function to use an entirely different shape. A derived class could also forward the DropShape call to the base class implementation, then use the returned IVShape object to perform additional formatting or add extra text.

LoadVisio is the first virtual method called. LoadVisio starts the Visio application and loads the Auto-size box and Dynamic connector masters. Master objects are instantiated as shapes when users drag them into a drawing. The Auto-size box can adjust its width/height to accommodate the text it contains, while the Dynamic connector can route itself around other shapes on the drawing, as well as provide line jumps when it crosses other connector lines. These characteristics help simplify the drawing code.

DropShape places a box in the drawing and sets the text to the name of the DependencyNode. The second and third parameters to the Drop method of IVPage are the x and y positions. DropShape simply drops all of the shapes into the middle of the page for Visio to rearrange later.

DropConnector works in a similar fashion. However, DropShape has the additional task of formatting the connector with a thick end arrow. Almost every object in Visio has an attached "ShapeSheet" spreadsheet and almost every property adjustment or formatting task requires manipulation of a ShapeSheet. Although intimidating when you first see the large number of cells associated with each object, ShapeSheets are nonetheless powerful. A dynamic connector, for example, has around 130 cells to manipulate. A program can access a cell via a label (Height and Width, for example), and a cell may contain a formula. DropConnector sets the thick end arrow by putting the value 4 into the EndArrow cell. The easiest way for me to find the cells and values to use is to open the ShapeSheet window and experiment directly with the Visio application.

GlueShape uses the GlueTo method of a cell to attach a connector from one shape to the next. GlueShape glues the EndX cell of the connector (x-coordinate of the end point) to the PinX cell of the shape (x-coordinate of the center of rotation). However, the connector does not draw directly into the center of the shape. Instead, using the PinX cell in GlueTo creates "dynamic glue," which lets the connector attach to the shape along the edge, and lets the connector "walk" along the edge as shapes are moved around.

Finally, LayoutPage lets Visio rearrange shapes and reroute connectors to produce an aesthetic drawing. The resulting layout is generally good (see Figure 1), although a few tweaks by hand after the automation is finished often improve the diagram.

Metadata

Recall that one of my design goals was for GDEPENDS to easily support new file types. I envisioned a plug-in type architecture where the program discovers supported file types during execution. Fortunately, I found an easy solution in the extensible metadata of .NET.

Metadata is the lifeblood of managed execution in .NET. Metadata facilitates garbage collection, security, versioning, serialization, language interoperability, JIT compiling, and many other run-time services and features. COM also used metadata, but the metadata was not easily extensible and often spread throughout the system. For example, a single COM component might store information in a type library, the Windows registry, and the COM+ catalog. In .NET, all of the metadata for a component lives in the component's assembly.

Custom attributes let you extend the metadata when needed. For GDEPENDS, I needed such a mechanism to describe the file types a class could parse. The PluginAttribute class, derived from System.Attribute (available electronically; see "Resource Center," page 5), is the custom attribute I wrote for this purpose. PluginAttribute lets a class publicize a file extension and a descriptive string without using a configuration file or registry entry.

You apply an attribute to a target using a pair of square brackets. For example, the PluginAttribute class has an AttributeUsage attribute defined before the class declaration. The AttributeUsage attribute, defined by .NET, describes the allowable usage for a custom attribute. In this case, the AttributeUsage attribute restricts a PluginAttribute to target only a class declaration. If the compiler finds a PluginAttribute attached to another element (a method or parameter, for example), the compiler generates an error. I also allow multiple PluginAttributes on a single class, for classes that support more than one file extension.

The SLN Plug-In

The SLNAnalyzer class derives from DependencyAnalyzer. SLNAnalyzer contains an implementation to parse Visual Studio.NET solution files. In Listing Two, an attribute specification applies a PluginAttribute to the SLNAnalyzer class. There are a couple of interesting items to note about the attribute specification. First, the Attribute suffix is optional in C#. Second, the attribute specification uses a combination of positional and named parameters. Because the PluginAttribute class has a single constructor with a string parameter, the positional parameter (SLN) is required to satisfy the constructor. The attribute specification can optionally initialize other public properties and fields (such as Description) using a named parameter.

Parsing a solution file is straightforward. The first section of the file lists all of the projects in the following format: Project(SLN_GUID)=PRJ_NAME,PRJ_CONFIG,PRJ_GUID. From this section, ParseProject extracts PRJ_NAME (the project name) and PRJ_GUID (the project's globally unique identifier), and populates the hash table to map GUIDs to DependencyNodes. The parser makes use of the string class's Split and Trim methods. Split turns a single string into an array of strings based on the string delimiter parameter. Trim can remove whitespace, or any characters specified in an array, from the beginning and end of a string.

Later in the solution file, a ProjectDependencies section contains the dependencies in the format: P_PRJ_GUID .n=C_PRJ_GUID. The ParseDependencies method extracts the P_PRJ_GUID (the parent's project GUID) and the C_PRJ_GUID (the GUID of the project the parent depends on). The method uses the GUIDs to retrieve the corresponding DependencyNodes from the hash table, then adds the child DependencyNode to the parent project's ArrayList of dependencies. When the method finishes, a complete dependency graph is in memory.

The final piece of plumbing is a class factory to abstract away the details of creating the correct DependencyAnalyzer during run time. The AnalyzerFactory (available electronically) uses classes from System.Reflection to discover the available plug-ins and create the correct object for a given file extension.

The application needs to forward a plug-in assembly to the RegisterAssembly method. RegisterAssembly retrieves an array of all exported types in the assembly, and queries each Type object for custom attributes. Since a type may contain multiple custom attributes, the method tests the cast to a PluginAttribute using the C# is keyword. When the method finds a PluginAttribute, it stores away the file extension and the Type object into a hash table. It also adds the custom attribute to an ArrayList. An application can maintain this ArrayList to know the file extensions and descriptions available for selection in a GUI. When the application is ready to create an analyzer, it uses the CreateAnalyzer method. CreateAnalyzer looks up the correct Type for a given file extension and uses the Activator class to create an instance of the Type. The Activator class has the ability to create local objects, remote objects, objects with constructor parameters, and more.

The User Interface

The WinForms Designer in the .NET IDE is similar to many of the current RAD tools. Figure 2 shows GDEPENDS in design mode. Source code for the main form (less designer-generated code) is available electronically. The first few methods are event handlers invoked by the UI elements. These methods simply forward the calls to other member functions for processing.

When the form loads, the program calls LoadPlugins, which reads the XML file in Listing Three using a typed DataSet. I only briefly describe the DataSet class because a full discussion of data access techniques in .NET is beyond the scope of this article. The IDE can create a typed DataSet given an XSD schema file. Instead of using XPath queries or XML DOM navigation techniques, the typed DataSet lets you view an XML file as a collection of tables, each table with a collection of rows. The XML attributes are columns in each table, and hierarchical XML generates parent/child relationships between tables. With a DataSet, you can easily abstract away the source of your data (DataSets work equally well with any OleDB provider), and a typed DataSet forces compile errors instead of run-time errors when the underlying schema changes. For GDEPENDS, I asked the IDE to generate an XSD schema for my XML file in Listing Three, and then generated a typed DataSet with the name Plugins.

LoadPlugins iterates through the assembly names, fetching them into the application with the static LoadFrom method on the Assembly class. The method also registers each assembly with the AnalyzerFactory, and keeps an ArrayList of all the PluginAttributes found. The method uses the ArrayList to build a file filter for the OpenFileDialog to use when prompting users. The code uses a StringBuilder object for efficient string concatenation.

When users click on the Open button, control arrives at the OpenFile method. If users select a file, the method retrieves the file extension using the FileInfo class, and asks the AnalyzerFactory to create the correct analyzer object.

My first iteration of the UI was a form with Open, Graph, and Exit buttons. I quickly realized how many applications have all projects dependent on one or two projects of common code or resources, and I wanted a way to remove the clutter of lines generated in Visio. To help, I added a tree view to the form and allow users to select nodes to mark as hidden; see Figure 3.

To display the tree view the application calls PopulateTreeView, which simply iterates the hash table and calls AddNode, passing the top-level TreeNodeCollection. AddNode places the node into the TreeNodeCollection and calls itself recursively to add dependencies of the current node. When users select the Hide button, the HideSelectedNode method searches the dependency collection for a project matching the name of the tree node. The node is then marked as hidden and the method refreshes the tree view. Any rendering function should check the Visible property of a DependencyNode before adding the node to a display.

Conclusion

.NET introduces a number of new features and paradigms. I believe metadata and custom attributes will drive some unique and interesting applications in the future. For GDEPENDS, the garbage collection and RAD environment certainly reduced the development time and lines of code needed to get GDEPENDS to the current stage. I did miss using type-safe collections with the STL, but overall, the expressiveness of C# should make C++ programmers feel comfortable. The complete source code (available electronically) includes a class for parsing Visual Studio 6 workspace files, and I plan to keep adding to the number of plug-in assemblies for the utility.

DDJ

Listing One

/// <summary>
/// A DependencyNode keeps state for a dependency: name, 
/// visibility, visio shape, and child dependencies
/// </summary>
public class DependencyNode
{
    public DependencyNode(string name)
    {
        if(name == null)
        {
            throw new ArgumentNullException();
        }
        this.name = name;
        visible = true;
        dependencies = new ArrayList();
    }
    public string Name
    {
        get { return name;  }
        set { name = value; }
    }
    public bool Visible
    {
        get { return visible; }
        set { visible = value; }
    }
    public Visio.IVShape Shape
    {
        get { return shape; }
        set { shape = value; }
    }
    public ArrayList Dependencies
    {
        get { return dependencies; }
    }
    protected string name;
    protected bool visible;
    protected Visio.IVShape shape;
    protected ArrayList dependencies;
};

Back to Article

Listing Two

/// <summary>
/// This custom attribute is applied to classes implementing 
//  DependencyAnalyzer. It allows class to announce supported file extension.
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public class PluginAttribute : System.Attribute
{
    public PluginAttribute(string fileExtension)
    {
        this.fileExtension = fileExtension;
    }
    public string FileExtension
    {
        get { return fileExtension; }
    }
public string Description
    {
        get { return description; }
        set { description = value; }
    }
    private string fileExtension;
    private string description;
};

Back to Article

Listing Three

<?xml version="1.0" encoding="utf-8" ?>
<Plugins xmlns="http://tempuri.org/Plugins.xsd">
	<Assembly Name="DSWAnalyzer.dll" />
	<Assembly Name="SLNAnalyzer.dll" />
</Plugins>

Back to Article


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.