Visual Studio 2008 的项目依赖图实用程序





5.00/5 (35投票s)
查看您的项目依赖关系。


引言
我最近想查看解决方案中一组相当大的项目的依赖关系(不是截图中的那个),发现虽然有应用程序/工具可以做到这一点,但它们要么是针对早期版本的 Visual Studio 的 Code Project 文章,要么创建了一堆难以阅读的框和线条,因为它们从未设计用于处理包含 50 个或更多项目的解决方案。所以,我决定创建一个基于文本、基于树视图的依赖关系浏览器。
它非常简单,提供了项目依赖关系的层次视图或扁平列表,还有一个树视图显示作为其他项目依赖关系的项目(如上图的右侧)。
限制
- 肯定会随着 VS2010 的出现而过时
- 仅适用于 C# 项目(不解析任何其他项目类型)
- 仅适用于 VS2008 解决方案文件
项目依赖关系的层次视图

请注意,该视图是分层的,允许您深入了解每个项目的依赖关系。
项目依赖关系的扁平视图
在此视图中,应用程序会深入了解项目的依赖关系,并以扁平列表的形式呈现所有唯一的依赖关系

项目对其他项目的依赖关系
您还可以选择一个项目,并找出哪些项目引用了所选项目

代码
代码真的很简单。很少进行错误检查,并且基本上是蛮力实现。我发现的令人烦恼的一件事是解决方案文件不是 XML 文档,而项目文件 (csproj) 是。 让人疑惑。
读取解决方案文件
基本上,这涉及到大量的字符串检查和处理,其中创建了一个项目名称和项目路径的字典。
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();
  }
}
读取项目文件
啊,一个 XML 文件! 万岁! 我花了一段时间才意识到我需要指定 XML 命名空间以及元素名称。 就像,几个小时的忙乱,拔头发,最后偶然发现了 MSDN 中的一些文档,其中给出了使用带有命名空间的Elements的例子。 叹息。
与Solution类类似,此类构建了一个引用的项目字典,其中键是引用的项目名称,值是引用的项目路径。
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);
    }
  }
}
解析解决方案文件
使用一个单独的方法将解决方案和项目组合成另一个字典,这次键是项目名称,值是Project实例。 此方法还填充了Project类中的Dependencies集合(是的,我知道这确实是不好的做法)。
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);
    }
  }
}
填充依赖关系图
有两种填充依赖关系图的方法:层次结构或扁平结构。 两者的代码类似——最大的区别是没有创建子节点。 并且扁平实现中的另一个丑陋的笨拙之处在于我如何搜索节点集合中已经存在的程序集。 恶心!
/// <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);
  }
}
填充“是...的依赖”图
也很简单,也有一个笨拙之处来删除重复的项目名称。
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);
          }
        }
      }
    }
  }
}
结论
希望有人会发现这很有用,甚至可以在此基础上构建! 这是一个短暂而有趣的应用程序。 我想添加的一件事是对项目名称进行排序。
历史
2009 年 6 月 17 日 - 初始版本
6/25/2009
- 现在项目按字母顺序排序
- 添加了一个同步选项,该选项在相反的树中查找项目,选择它,并打开树。 这对于同时查看项目依赖关系和项目的依赖关系非常有用。
2009 年 7 月 5 日 - 使用 graphviz 添加了图表的呈现。 感谢 Dmitri Nesteruk 对此应用程序进行了最初的更改并公开了他的呈现代码。


