Click here to Skip to main content
Email Password   helpLost your password?

ProjectDependencyGraph

ProjectDependencyGraph

Introduction

I recently wanted to look at the dependencies of a fairly large set of projects in a solution (not the one in the screenshot), and discovered that while there are apps/tools that do that, they are either Code Project articles for previous versions of Visual Studio or they create an unreadable smear of boxes and lines, because they were never designed to handle a solution with 50 or more projects. So, I decided to create a textual, treeview based browser of dependencies.

It's very simple, offering both a hierarchical view of project dependencies or a flattened list of dependencies, and also a tree-view showing projects that are dependencies of other projects (the right-hand side of the image above).

Limitations

Hierarchical View of Project Dependencies

Note how the view is hierarchical, allowing you to drill into each project's dependencies.

Flattened View of Project Dependencies

In this view, the application drills into project dependencies for you and presents all unique dependencies in a flattened list:

Project's Dependency on Other Projects

You can also pick a project and find out what projects reference the selected project:

The Code

The code is really simple. Very little error checking and pretty much brute force implementation. The one annoying thing I discovered is that the solution file is not an XML document, whereas the project files (csproj) are. Makes one wonder.

Reading the Solution File

Basically, this involves a lot of string checking and processing, in which a dictionary of project names and project paths is created.

public class Solution
{
  protected Dictionary<string, string> projectPaths;

  public Dictionary<string, string> ProjectPaths 
  {
    get { return projectPaths; }
  }

  public Solution()
  {
    projectPaths = new Dictionary<string, string>();
  }

  // Example:
  // Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NumericKeypadComponent", 
  //   "NumericKeypadComponent\NumericKeypadComponent.csproj", 
  //   "{05D03020-4604-4CE9-8F99-E1D93ADEDF15}"
  // EndProject

  public void Read(string filename)
  {
    StreamReader sr = new StreamReader(filename);
    string solPath = Path.GetDirectoryName(filename);

    while (!sr.EndOfStream)
    {
      string line = sr.ReadLine();

      if (!String.IsNullOrEmpty(line))
      {
        if (line.StartsWith("Project"))
        {
          string projName = StringHelpers.Between(line, '=', ',');
          projName = projName.Replace('"', ' ').Trim();
          string projPath = StringHelpers.RightOf(line, ',');
          projPath = StringHelpers.Between(projPath, '"', '"');
          projPath = projPath.Replace('"', ' ').Trim();

          // virtual solution folders appear as projects but don't end with .csproj
          // gawd, imagine what happens if someone creates a foo.csproj 
          // virt. solution folder!
          if (projPath.EndsWith(".csproj"))
          {
            // assume relative paths. Probably not a good assumption
            projPath = Path.Combine(solPath, projPath);

            // we don't allow projects with the same name, even if different paths.
            projectPaths.Add(projName, projPath);
          }
        }
      }
    }

    sr.Close();
  }
}

Reading a Project File

Ah, an XML file! Woohoo! It took a while for me to realize that I needed to specify the XML namespace along with the element name. As in, several hours of fussing, pulling hair out, and finally stumbling across some documentation in MSDN that gave an example of using Elements with a namespace. Sigh.

Similar to the Solution class, this class builds a dictionary of referenced projects, where the key is the referenced project name and the value is the referenced project path.

public class Project
{
  protected Dictionary<string, string> referencedProjects;
  protected List<Project> dependencies;

  public string Name { get; set; }

  public Dictionary<string, string> ReferencedProjects
  {
    get { return referencedProjects; }
  }

  public List<Project> Dependencies
  {
    get { return dependencies; }
  }

  public Project()
  {
    referencedProjects = new Dictionary<string, string>();
    dependencies = new List<Project>();
  }

  // Example:
  // <Project ToolsVersion="3.5" DefaultTargets="Build" 
  // xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  // <ItemGroup>
  // <ProjectReference Include="..\Cx.Attributes\Cx.Attributes.csproj">
  // <Project>{EFDBD81C-64BE-47F3-905E-7618B61BD224}</Project>
  // <Name>Cx.Attributes</Name>
  // </ProjectReference>

  public void Read(string filename)
  {
    XDocument xdoc = XDocument.Load(filename);
    XNamespace ns = "http://schemas.microsoft.com/developer/msbuild/2003";

    foreach (var projRef in from el in 
       xdoc.Root.Elements(ns + "ItemGroup").Elements(ns + "ProjectReference")
    select new
    {
      Path = el.Attribute("Include").Value,
      Name = el.Element(ns + "Name").Value
    })
    {
      string projPath = Path.GetDirectoryName(filename);
      projPath = Path.Combine(projPath, projRef.Path);
      referencedProjects.Add(projRef.Name, projPath);
    }
  }
}

Parsing the Solution File

A separate method is used to put the solution and projects together into yet another dictionary, this time the key being the project name and the value being a Project instance. This method also populates the Dependencies collection in the Project class (yeah, that's really bad practice, I know).

protected Dictionary<string, Project> projects;

protected void ParseSolution(string filename)
{
  projects = new Dictionary<string, Project>();
  Solution sol = new Solution();
  sol.Read(filename);

  foreach (KeyValuePair<string, string> kvp in sol.ProjectPaths)
  {
    Project proj = new Project();
    proj.Name = kvp.Key;
    proj.Read(kvp.Value);
    projects.Add(proj.Name, proj);
  }

  foreach (KeyValuePair<string, Project> kvp in projects)
  {
    foreach (string refProjName in kvp.Value.ReferencedProjects.Keys)
    {
      Project refProject = projects[refProjName];
      kvp.Value.Dependencies.Add(refProject);
    }
  }
}

Populating the Dependency Graph

There are two ways of populating the dependency graph: hierarchical or flat. The code for both is similar--the big difference is that child nodes aren't being created. And another ugly kludge in the flattened implementation is how I search for assemblies already in the node collection. Yuck!

/// <summary>
/// Sets up initial project name and first level of dependencies.
/// From there, child dependencies are either added hierarchically or flattened.
/// </summary>
protected void PopulateNewLevel(TreeNode node, ICollection<Project> projects)
{
  List<string> nodeNames = new List<string>();

  foreach (Project p in projects)
  {
    TreeNode tn = new TreeNode(p.Name);
    node.Nodes.Add(tn);

    if (asTree)
    {
      PopulateNewLevel(tn, p.Dependencies);
    }
    else
    {
      // flatten the dependency hierarchy, removing duplicates
      PopulateSameLevel(tn, p.Dependencies);
    }
  }
}

protected void PopulateSameLevel(TreeNode node, ICollection<Project> projects)
{
  foreach (Project p in projects)
  {
    bool found = false;

    foreach (TreeNode child in node.Nodes)
    {
      if (child.Text == p.Name)
      {
        found = true;
        break;
      }
    }

    if (!found)
    {
      TreeNode tn = new TreeNode(p.Name);
      node.Nodes.Add(tn);
    }

    PopulateSameLevel(node, p.Dependencies);
  }
}

Populating the "Is Dependency Of" Graph

Also straightforward, also has a kludge to remove duplicate project names.

protected void PopulateDependencyOfProjects(TreeNode node, ICollection<Project> projects)
{
  foreach (Project p in projects)
  {
    TreeNode tn = new TreeNode(p.Name);
    node.Nodes.Add(tn);

    foreach (Project pdep in projects)
    {
      foreach (Project dep in pdep.Dependencies)
      {
        if (p.Name == dep.Name)
        {
          bool found = false;

          // the project pdep has a dependency on the project p.
          // p is a dependency of pdep
          foreach (TreeNode tnDep in tn.Nodes)
          {
            if (tnDep.Text == pdep.Name)
            {
              found = true;
              break;
            }
          }
  
          if (!found)
          {
            TreeNode tn2 = new TreeNode(pdep.Name);
            tn.Nodes.Add(tn2);
          }
        }
      }
    }
  }
}

Conclusion

Hopefully someone will find this useful and maybe even build upon it! It was a short and fun to write application. One thing I'd like to add is sorting the project names.

History

6/17/2009 - Initial Version

6/25/2009

7/5/2009 - Added rendering of graph using graphviz.  Thanks to Dmitri Nesteruk for making the original changes to this application and for making his rendering code public.

   

You must Sign In to use this message board.
 
 
Per page   
 FirstPrevNext
GeneralDownload not Working ??
Bill Elsey
4:20 13 Oct '09  
I do not seem to be able to download the ZIP for this article. Is anyone else having problems?
GeneralRe: Download not Working ??
Marc Clifton
5:18 13 Oct '09  
Still working for me. Email me directly, and I'll email you the zip file (it'll have to be wrapped in a rar though for me to send it through gmail)

Marc

Will work for food. Interacx

I'm not overthinking the problem, I just felt like I needed a small, unimportant, uninteresting rant! - Martin Hart Turner


GeneralBinding XDocument content to treeview
mukhtharsal
1:38 31 Jul '09  
Hello,

I have XDoucument object which contains the XMLFile data,
I wanted to bind this XDocument data to a treeview in vb.net 3.5?

Regards,
Mukhthar Saleem
GeneralThanks
Johan Fourie
18:44 12 Jul '09  
Nice, I like it.
Just a small bug, I think.

Line 174 of Form1.cs...

TreeNode tn2 = new TreeNode(pdep.Name + " (" + p.TargetVersion + ")");

needs to be

TreeNode tn2 = new TreeNode(pdep.Name + " (" + pdep.TargetVersion + ")");

Coding silicon daily.
Johan Fourie
Coded Silicon

GeneralRe: Thanks
Marc Clifton
5:04 13 Jul '09  
Johan Fourie wrote:
Nice, I like it.
Just a small bug, I think.

Oops! Thanks for spotting that!

Marc

Will work for food. Interacx

I'm not overthinking the problem, I just felt like I needed a small, unimportant, uninteresting rant! - Martin Hart Turner


QuestionWhere is the graph?
Dmitri Nesteruk
21:43 27 Jun '09  
Umm, I don't see an actual graph in your screenshots - are you just showing TreeView controls? It would be better to actually show a proper graph, I think.
AnswerRe: Where is the graph?
Marc Clifton
10:22 28 Jun '09  
Dmitri Nesteruk wrote:
Umm, I don't see an actual graph in your screenshots - are you just showing TreeView controls? It would be better to actually show a proper graph, I think.

The term "graph" refers to anything that shows the relationship between entities. For example, the term "object graph" is often used to describe the object hierarchy expressed in XAML.

A TreeView meets the definition of a graph:

a diagram representing a system of connections or interrelations among two or more things by a number of distinctive dots, lines, bars, etc.
because it is a diagram of interrelations using a number of distinctive lines.

Marc

Will work for food. Interacx

I'm not overthinking the problem, I just felt like I needed a small, unimportant, uninteresting rant! - Martin Hart Turner


AnswerRe: Where is the graph?
Dmitri Nesteruk
10:34 28 Jun '09  
I actually meant something like this[^]. By the way, to generate this image, I used your source code and Graphviz. So I guess I'm answering my own question, in a way.

Thanks for the article!
GeneralRe: Where is the graph?
Marc Clifton
10:38 28 Jun '09  
Dmitri Nesteruk wrote:
I actually meant something like this[^]. By the way, to generate this image, I used your source code and Graphviz.

Sweet. Want to share your code?

Marc

Will work for food. Interacx

I'm not overthinking the problem, I just felt like I needed a small, unimportant, uninteresting rant! - Martin Hart Turner


AnswerRe: Where is the graph?
Dmitri Nesteruk
10:50 28 Jun '09  
Sure, just get GraphViz[^] and the source code is here[^].
GeneralRe: Where is the graph?
Dmitri Nesteruk
10:53 28 Jun '09  
Btw, feel free to add this stuff to your article - I think people might actually find this useful, if a bit scary. And, speaking of which, I actually got the idea for using this tool from someone who uses it to visualize IoC containers. Which is another very useful option if you're into IoC/DI.
GeneralRe: Where is the graph?
Marc Clifton
11:01 28 Jun '09  
Dmitri Nesteruk wrote:
Btw, feel free to add this stuff to your article - I think people might actually find this useful, if a bit scary.

Thanks! Yes, scary indeed, seeing all the intertwined dependencies.

Dmitri Nesteruk wrote:
And, speaking of which, I actually got the idea for using this tool from someone who uses it to visualize IoC containers. Which is another very useful option if you're into IoC/DI.

That's a great idea! I've been working with both CAB and Spring.NET, and the dependencies can get quite difficult to follow.

Marc

Will work for food. Interacx

I'm not overthinking the problem, I just felt like I needed a small, unimportant, uninteresting rant! - Martin Hart Turner


GeneralGood job, but can be slow for large solutions
ZTransform
7:06 22 Jun '09  
We have only 177 projects, but with many dependencies.

Your utility works, but is very slow to the point of not being useful.

After examining it with a performance profiler, I found that virtually all the time is being spent in populating nodes for the treeview: Form1.PopulateNewLevel() (recursively).

Maybe it could be improved by building the initial tree in memory, and only showing nodes that the user expands "on demand", rather than constructing the entire tree view in one hit. Is this not how Windows Explorer works? Unsure
GeneralRe: Good job, but can be slow for large solutions
Marc Clifton
11:31 22 Jun '09  
ZTransform wrote:
Maybe it could be improved by building the initial tree in memory, and only showing nodes that the user expands "on demand", rather than constructing the entire tree view in one hit.

I was thinking about that as a future refinement. Look for it soon (give me a week or so.)

Thanks for investigating where the bottleneck is--I rather figured that might be a problem, given my experience recently with some XSD recursion issues on an unrelated project. Smile

Marc

Will work for food. Interacx

I'm not overthinking the problem, I just felt like I needed a small, unimportant, uninteresting rant! - Martin Hart Turner


GeneralComment and a problem ...
sdahlbac
23:53 21 Jun '09  
As the author of one of the tools (dependencyvisualizer.codeplex.com) that "create an unreadable smear of boxes and lines, because they were never designed to handle a solution with 50 or more projects." I do see the need for an alternative view for large solutions. (I just never have gotten around to provide it..)

While you can easily parse the .csproj file manually (and other msbuild compatible files for that matter), there is an object model available in Microsoft.Build.* assemblies.

I tried the project and got the following exception:
System.ArgumentException was unhandled
Message="An item with the same key has already been added."
Source="mscorlib"
StackTrace:
at System.ThrowHelper.ThrowArgumentException(ExceptionResource resource)
at System.Collections.Generic.Dictionary`2.Insert(TKey key, TValue value, Boolean add)
at System.Collections.Generic.Dictionary`2.Add(TKey key, TValue value)
at ProjectDependencyGraph.Project.Read(String filename) in C:\Download\ProjectDependencyGraph\ProjectDependencyGraph\ProjectDependencyGraph\Project.cs:line 57
at ProjectDependencyGraph.Form1.ParseSolution(String filename) in C:\Download\ProjectDependencyGraph\ProjectDependencyGraph\ProjectDependencyGraph\Form1.cs:line 32
at ProjectDependencyGraph.Form1.btnLoadSolution_Click(Object sender, EventArgs e) in C:\Download\ProjectDependencyGraph\ProjectDependencyGraph\ProjectDependencyGraph\Form1.cs:line 183
at System.Windows.Forms.Control.OnClick(EventArgs e)
at System.Windows.Forms.Button.OnClick(EventArgs e)
at System.Windows.Forms.Button.OnMouseUp(MouseEventArgs mevent)
at System.Windows.Forms.Control.WmMouseUp(Message& m, MouseButtons button, Int32 clicks)
at System.Windows.Forms.Control.WndProc(Message& m)
at System.Windows.Forms.ButtonBase.WndProc(Message& m)
at System.Windows.Forms.Button.WndProc(Message& m)
at System.Windows.Forms.Control.ControlNativeWindow.OnMessage(Message& m)
at System.Windows.Forms.Control.ControlNativeWindow.WndProc(Message& m)
at System.Windows.Forms.NativeWindow.DebuggableCallback(IntPtr hWnd, Int32 msg, IntPtr wparam, IntPtr lparam)
at System.Windows.Forms.UnsafeNativeMethods.DispatchMessageW(MSG& msg)
at System.Windows.Forms.Application.ComponentManager.System.Windows.Forms.UnsafeNativeMethods.IMsoComponentManager.FPushMessageLoop(Int32 dwComponentID, Int32 reason, Int32 pvLoopData)
at System.Windows.Forms.Application.ThreadContext.RunMessageLoopInner(Int32 reason, ApplicationContext context)
at System.Windows.Forms.Application.ThreadContext.RunMessageLoop(Int32 reason, ApplicationContext context)
at System.Windows.Forms.Application.Run(Form mainForm)
at ProjectDependencyGraph.Program.Main() in C:\Download\ProjectDependencyGraph\ProjectDependencyGraph\ProjectDependencyGraph\Program.cs:line 19
at System.AppDomain._nExecuteAssembly(Assembly assembly, String[] args)
at System.AppDomain.ExecuteAssembly(String assemblyFile, Evidence assemblySecurity, String[] args)
at Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly()
at System.Threading.ThreadHelper.ThreadStart_Context(Object state)
at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
at System.Threading.ThreadHelper.ThreadStart()
InnerException:

Edit: and if try to fix that with an if (!referencedProjects.ContainsKey(projRef.Name))
I get the following:

System.Collections.Generic.KeyNotFoundException was unhandled
Message="The given key was not present in the dictionary."
Source="mscorlib"
StackTrace:
at System.ThrowHelper.ThrowKeyNotFoundException()
at System.Collections.Generic.Dictionary`2.get_Item(TKey key)
at ProjectDependencyGraph.Form1.ParseSolution(String filename) in C:\Download\ProjectDependencyGraph\ProjectDependencyGraph\ProjectDependencyGraph\Form1.cs:line 40
at ProjectDependencyGraph.Form1.btnLoadSolution_Click(Object sender, EventArgs e) in C:\Download\ProjectDependencyGraph\ProjectDependencyGraph\ProjectDependencyGraph\Form1.cs:line 183
at System.Windows.Forms.Control.OnClick(EventArgs e)
at System.Windows.Forms.Button.OnClick(EventArgs e)
at System.Windows.Forms.Button.OnMouseUp(MouseEventArgs mevent)
at System.Windows.Forms.Control.WmMouseUp(Message& m, MouseButtons button, Int32 clicks)
at System.Windows.Forms.Control.WndProc(Message& m)
at System.Windows.Forms.ButtonBase.WndProc(Message& m)
at System.Windows.Forms.Button.WndProc(Message& m)
at System.Windows.Forms.Control.ControlNativeWindow.OnMessage(Message& m)
at System.Windows.Forms.Control.ControlNativeWindow.WndProc(Message& m)
at System.Windows.Forms.NativeWindow.DebuggableCallback(IntPtr hWnd, Int32 msg, IntPtr wparam, IntPtr lparam)
at System.Windows.Forms.UnsafeNativeMethods.DispatchMessageW(MSG& msg)
at System.Windows.Forms.Application.ComponentManager.System.Windows.Forms.UnsafeNativeMethods.IMsoComponentManager.FPushMessageLoop(Int32 dwComponentID, Int32 reason, Int32 pvLoopData)
at System.Windows.Forms.Application.ThreadContext.RunMessageLoopInner(Int32 reason, ApplicationContext context)
at System.Windows.Forms.Application.ThreadContext.RunMessageLoop(Int32 reason, ApplicationContext context)
at System.Windows.Forms.Application.Run(Form mainForm)
at ProjectDependencyGraph.Program.Main() in C:\Download\ProjectDependencyGraph\ProjectDependencyGraph\ProjectDependencyGraph\Program.cs:line 19
at System.AppDomain._nExecuteAssembly(Assembly assembly, String[] args)
at System.AppDomain.ExecuteAssembly(String assemblyFile, Evidence assemblySecurity, String[] args)
at Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly()
at System.Threading.ThreadHelper.ThreadStart_Context(Object state)
at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
at System.Threading.ThreadHelper.ThreadStart()
InnerException:
GeneralRe: Comment and a problem ...
Marc Clifton
6:58 22 Jun '09  
sdahlbac wrote:
As the author of one of the tools (dependencyvisualizer.codeplex.com) that "create an unreadable smear of boxes and lines

Busted! Blush

There's no error handling at all, and yes, two projects with the same name is a problem (if that wasn't in the limitations, it needs to be--I knew I forgot something!)

I'm putting together another release which sorts the projects and adds a synchronize feature. I'll add some error checking, see what's involved in supporting multiple projects, and also look at Microsoft.Build.

Thanks for the great feedback--watch for updates!

Marc

Will work for food. Interacx

I'm not overthinking the problem, I just felt like I needed a small, unimportant, uninteresting rant! - Martin Hart Turner


GeneralVery timely indeed Mr Clifton
Sacha Barber
7:00 20 Jun '09  
We just got a huge solution back at work from an external consultant that provided 1 part of our project, and it is just huge, and we think it is badly written, and way too many dependencies. We have like 100 projects, a lot with 1 class in them and 4 line of code. Shocking really. I think it could have been done in like 4 projects.

This may actually help us to prove that.

Anyway even if it doesnt its still very useful.

Sacha Barber
  • Microsoft Visual C# MVP 2008/2009
  • Codeproject MVP 2008/2009
Your best friend is you.
I'm my best friend too. We share the same views, and hardly ever argue

My Blog : sachabarber.net

GeneralRe: Very timely indeed Mr Clifton
Marc Clifton
10:23 28 Jun '09  
Sacha Barber wrote:
This may actually help us to prove that.

Thanks Sacha. BTW, I posted an update that alphabetizes the projects and synchronizes the selection between the two tree views.

Marc

Will work for food. Interacx

I'm not overthinking the problem, I just felt like I needed a small, unimportant, uninteresting rant! - Martin Hart Turner


GeneralMy Vote of 5
programmersmind
0:33 20 Jun '09  
I tried it. Its awesomeeeeeee

Great Job Marc,

Best Regards

T.S Chowdhury

GeneralMy vote of 5
Dr.Luiji
12:37 19 Jun '09  
It's friday evening and I do not have time to try it now, but just yesterday I had a dependency problem ... (a second level), this tool would have saved me a lot of work during the installation.
Thank you for the tool will try next monday morning...
You can immagine my vote
Good job!

Dr.Luiji
Trust and you'll be trusted.
Try iPhone UI [^] a new fresh face for your Windows Mobile, here on Code Project.

GeneralRe: My vote of 5
Dr.Luiji
22:29 21 Jun '09  
I just tried it and it works really well.
It will save me a lot of time during deploy.

Only a note:
I work with VS2005 and I try to convert your project to it. But unfortunately You use the System.Xml.Linq and obviously the program is only compatible with .NET Framework 3.5 or above.
Probably you need to change some article attributes.

Anyway I'll download the express version to see the code in action Wink

For the moment I use directly the binary you provide into the zip.

Dr.Luiji
Trust and you'll be trusted.
Try iPhone UI [^] a new fresh face for your Windows Mobile, here on Code Project.

GeneralVery nice Mr C.
Pete O'Hanlon
13:59 18 Jun '09  
I voted for this earlier today and forgot to leave a message (doh!) I like this article, I like it a lot - hence the 5.

"WPF has many lovers. It's a veritable porn star!" - Josh Smith

As Braveheart once said, "You can take our freedom but you'll never take our Hobnobs!" - Martin Hughes.

My blog | My articles | MoXAML PowerToys | Onyx


GeneralRe: Very nice Mr C.
Marc Clifton
8:20 19 Jun '09  
Pete O'Hanlon wrote:
I like this article, I like it a lot - hence the 5.

Thanks!

Say, what about it do you like? I'm curious, so I can refine my style. Smile

Marc

Will work for food. Interacx

I'm not overthinking the problem, I just felt like I needed a small, unimportant, uninteresting rant! - Martin Hart Turner


GeneralRe: Very nice Mr C.
Pete O'Hanlon
9:41 19 Jun '09  
I like the fact that you haven't tried to shoehorn things into XML, using string manipulation without complex regexes and the fact you provide multiple views of the same information. Oh - and as always, I like your writing style.

"WPF has many lovers. It's a veritable porn star!" - Josh Smith

As Braveheart once said, "You can take our freedom but you'll never take our Hobnobs!" - Martin Hughes.

My blog | My articles | MoXAML PowerToys | Onyx


General???
Paw Jershauge
1:25 18 Jun '09  
I am curious, why dont you just use reflection for this Confused am i missing something here WTF

With great code, comes great complexity, so keep it simple stupid...Shucks Shucks


Last Updated 17 Jun 2009 | Advertise | Privacy | Terms of Use | Copyright © CodeProject, 1999-2010