Skip to content

Commit

Permalink
Use AutomaticGraphLayout.WpfGraphControl
Browse files Browse the repository at this point in the history
This replaces the use of images generated by WinGraphViz with a WPF control that allows interaction.

A few features have been removed, such as the creation of DOT files and the ability to save to PNG files.
  • Loading branch information
drewnoakes committed Oct 22, 2021
1 parent 8dd5eeb commit 2f6ec6c
Show file tree
Hide file tree
Showing 44 changed files with 330 additions and 1,459 deletions.
11 changes: 0 additions & 11 deletions Source/DependencyAnalyser.sln → DependencyAnalyser.sln
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,10 @@ VisualStudioVersion = 17.0.31412.12
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DependencyAnalyser", "DependencyAnalyser\DependencyAnalyser.csproj", "{C5BAF76A-B49F-40D3-A24B-26880A1352DD}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DependencyAnalyser.Tests", "DependencyAnalyser.Tests\DependencyAnalyser.Tests.csproj", "{962A3463-9C5E-4BA2-AA40-AF7AB120CEA8}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{6F8C6B01-A2D4-4115-8F83-D6C9F5A77AF6}"
ProjectSection(SolutionItems) = preProject
..\.gitignore = ..\.gitignore
..\LICENSE.txt = ..\LICENSE.txt
Notes.txt = Notes.txt
..\README.md = ..\README.md
EndProjectSection
EndProject
Expand All @@ -30,14 +27,6 @@ Global
{C5BAF76A-B49F-40D3-A24B-26880A1352DD}.Release|Any CPU.Build.0 = Release|Any CPU
{C5BAF76A-B49F-40D3-A24B-26880A1352DD}.Release|x86.ActiveCfg = Release|x86
{C5BAF76A-B49F-40D3-A24B-26880A1352DD}.Release|x86.Build.0 = Release|x86
{962A3463-9C5E-4BA2-AA40-AF7AB120CEA8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{962A3463-9C5E-4BA2-AA40-AF7AB120CEA8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{962A3463-9C5E-4BA2-AA40-AF7AB120CEA8}.Debug|x86.ActiveCfg = Debug|x86
{962A3463-9C5E-4BA2-AA40-AF7AB120CEA8}.Debug|x86.Build.0 = Debug|x86
{962A3463-9C5E-4BA2-AA40-AF7AB120CEA8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{962A3463-9C5E-4BA2-AA40-AF7AB120CEA8}.Release|Any CPU.Build.0 = Release|Any CPU
{962A3463-9C5E-4BA2-AA40-AF7AB120CEA8}.Release|x86.ActiveCfg = Release|x86
{962A3463-9C5E-4BA2-AA40-AF7AB120CEA8}.Release|x86.Build.0 = Release|x86
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
File renamed without changes.
5 changes: 5 additions & 0 deletions DependencyAnalyser/App.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<Application
x:Class="DependencyAnalyser.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="DependencyAnalyzerWindow.xaml" />
8 changes: 8 additions & 0 deletions DependencyAnalyser/App.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using System.Windows;

namespace DependencyAnalyser
{
public partial class App : Application
{
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public static void Analyze(Assembly assembly, DependencyGraph<string> graph, ILo

foreach (var dependantAssemblyName in dependencies)
{
logger.WriteLine("- {dependantAssemblyName.Name}");
logger.WriteLine($"- {dependantAssemblyName.Name}");

if (!graph.AddDependency(assemblyName, dependantAssemblyName.Name))
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,29 +11,17 @@
<Description>Displays dependencies between projects/assemblies as a directed graph.</Description>
<Company>https://drewnoakes.com</Company>
<Copyright>Drew Noakes 2003-2021</Copyright>
<UseWindowsForms>true</UseWindowsForms>
<UseWPF>true</UseWPF>
</PropertyGroup>

<ItemGroup>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
<_Parameter1>$(AssemblyName).Tests</_Parameter1>
</AssemblyAttribute>

<COMReference Include="WINGRAPHVIZLib">
<Guid>{052DB09C-95F7-43BD-B7F8-492373D1151E}</Guid>
<VersionMajor>1</VersionMajor>
<VersionMinor>0</VersionMinor>
<Lcid>0</Lcid>
<WrapperTool>tlbimp</WrapperTool>
<Isolated>False</Isolated>
<EmbedInteropTypes>False</EmbedInteropTypes>
</COMReference>

<PackageReference Include="AutomaticGraphLayout.WpfGraphControl" Version="1.1.11" />
<PackageReference Include="Microsoft.Build.Locator" Version="1.4.1" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.2" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.9.0" />
<PackageReference Include="Microsoft.CodeAnalysis.VisualBasic.Workspaces" Version="3.9.0" />
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.MSBuild" Version="3.9.0" />

</ItemGroup>

</Project>
61 changes: 61 additions & 0 deletions DependencyAnalyser/DependencyAnalyzerWindow.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<Window x:Class="DependencyAnalyser.DependencyAnalyzerWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:mlsagl="http://mlsagl"
mc:Ignorable="d"
Title=".NET Dependency Analyzer"
Height="450"
Width="800">

<Window.Resources>

<!-- Hide the icon area from menus (https://stackoverflow.com/a/17724242/24874) -->
<ItemsPanelTemplate x:Key="MenuItemPanelTemplate">
<StackPanel Margin="-20,0,0,0" Background="White"/>
</ItemsPanelTemplate>
<Style TargetType="{x:Type MenuItem}">
<Setter Property="ItemsPanel" Value="{StaticResource MenuItemPanelTemplate}"/>
</Style>
<Style TargetType="{x:Type ContextMenu}">
<Setter Property="ItemsPanel" Value="{StaticResource MenuItemPanelTemplate}"/>
</Style>

</Window.Resources>

<DockPanel>

<Menu DockPanel.Dock="Top">
<!-- TODO convert to commands and handle CanExecute -->
<!-- TODO bind keyboard shortcuts -->
<MenuItem Header="_File">
<MenuItem Header="_Open..." Click="OnOpenClicked"></MenuItem>
<MenuItem Header="_Merge..." Click="OnMergeClicked"></MenuItem>
<!--
<MenuItem Header="Save _PNG..."></MenuItem>
<MenuItem Header="Save _SVG..."></MenuItem>
-->
<Separator />
<MenuItem Header="E_xit" Click="OnExitClicked"></MenuItem>
</MenuItem>
<MenuItem Header="_Simplify" Click="OnSimplifyClicked"></MenuItem>
<MenuItem Header="F_ilter..." Click="OnFilterClicked"></MenuItem>
<MenuItem Header="_About..." Click="OnAboutClicked"></MenuItem>
</Menu>

<TabControl TabStripPlacement="Bottom">

<TabItem Header="Graph">
<mlsagl:AutomaticGraphLayoutControl x:Name="_graphControl" />
</TabItem>

<TabItem Header="Log">
<TextBox IsReadOnly="True" IsReadOnlyCaretVisible="True" x:Name="_log" HorizontalScrollBarVisibility="Auto" />
</TabItem>

</TabControl>

</DockPanel>

</Window>
174 changes: 174 additions & 0 deletions DependencyAnalyser/DependencyAnalyzerWindow.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
using System;
using System.Reflection;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
using Microsoft.Win32;
using Microsoft.Msagl.Drawing;

namespace DependencyAnalyser
{
public partial class DependencyAnalyzerWindow
{
private readonly OpenFileDialog _openFileDialog;
private readonly FilterPreferences _filterPreferences = new();
private readonly StringBuilderLogger _logger = new();

private DependencyGraph<string> _dependencyGraph = new();

public DependencyAnalyzerWindow()
{
_openFileDialog = new OpenFileDialog()
{
Filter = "All supported files|*.sln;*.csproj;*.vbproj;*.fsproj;*.exe;*.dll|" +
"Solution files (*.sln)|*.sln|" +
"Project files (*.csproj)|*.csproj;*.vbproj;*.fsproj|" +
"Assemblies (*.dll)|*.dll|" +
"Assemblies (*.exe)|*.exe|" +
"All files|*.*",
Title = "Open File"
};

InitializeComponent();
}

private void OnAboutClicked(object sender, RoutedEventArgs e)
{
// TODO make this a dialog with a link button
MessageBox.Show($".NET Assembly Dependency Analyser v{Assembly.GetExecutingAssembly().GetName().Version}\n\nCopyright Drew Noakes 2003-{DateTime.Now.Year}.\n\nThanks to John Maher.\n\nLatest version at http://drewnoakes.com/code/dependency-analyser/\nCharts provided using Dot & Wingraphviz.");
}

private void OnFilterClicked(object sender, RoutedEventArgs e)
{
using (WaitCursor())
{
var filterForm = new FilterForm(_filterPreferences);

if (filterForm.ShowDialog() == System.Windows.Forms.DialogResult.OK)
UpdateDiagram();
}
}

private void OnExitClicked(object sender, RoutedEventArgs e)
{
Application.Current.Shutdown();
}

#region Wait cursor

private IDisposable WaitCursor()
{
var reverter = new CursorReverter(Cursor, this);
Cursor = Cursors.Wait;
return reverter;
}

private sealed class CursorReverter : IDisposable
{
private readonly Cursor _cursor;
private readonly Window _form;

public CursorReverter(Cursor cursor, Window form)
{
_cursor = cursor;
_form = form;
}

public void Dispose()
{
_form.Cursor = _cursor;
}
}

#endregion

private async void OnOpenClicked(object sender, RoutedEventArgs e)
{
// Start a new graph
_dependencyGraph = new DependencyGraph<string>();

await MergeFileAsync();
}

private async void OnMergeClicked(object sender, RoutedEventArgs e)
{
await MergeFileAsync();
}

private async Task MergeFileAsync()
{
try
{
if (_openFileDialog.ShowDialog() != true)
{
return;
}

string fileName = _openFileDialog.FileName;

using (WaitCursor())
{
if (fileName.EndsWith(".sln") || fileName.EndsWith("proj"))
{
await SolutionAndProjectFileAnalyser.AnalyseAsync(fileName, _dependencyGraph, _logger);
}
else
{
var assembly = Assembly.LoadFrom(fileName);
AssemblyAnalyser.Analyze(assembly, _dependencyGraph, _logger);
}

_filterPreferences.SetAssemblyNames(_dependencyGraph.Nodes);
// EnableAndDisableMenuItems();
UpdateDiagram();
}
}
catch (Exception e)
{
MessageBox.Show(e.ToString());
}
finally
{
_log.Text = _logger.ToString();
}
}

private void UpdateDiagram()
{
var graph = new Graph();

// TODO style projects and assemblies differently
// TODO style nodes with no dependents differently
// TODO style nodes with no dependencies differently

foreach (var depending in _dependencyGraph.Nodes)
{
if (!_filterPreferences.IncludeInPlot(depending))
continue;

foreach (var depended in _dependencyGraph.GetDependenciesForNode(depending))
{
if (!_filterPreferences.IncludeInPlot(depended))
continue;

graph.AddEdge(depending, depended);
}
}

graph.Attr.LayerDirection = LayerDirection.TB;
graph.Attr.AspectRatio = _graphControl.ActualWidth / _graphControl.ActualHeight;

_graphControl.Graph = null;
_graphControl.Graph = graph;
// _graphControl.InvalidateMeasure();
// _graphControl.InvalidateVisual();
}

private void OnSimplifyClicked(object sender, RoutedEventArgs e)
{
_dependencyGraph.TransitiveReduce();

UpdateDiagram();
}
}
}
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
67 changes: 67 additions & 0 deletions DependencyAnalyser/SolutionAndProjectFileAnalyser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

using Microsoft.Build.Locator;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.MSBuild;

namespace DependencyAnalyser
{
/// <summary>
/// Builds a DependencyGraph from a given Visual Studio solution (.sln) or project (e.g. .csproj) file.
/// </summary>
public static class SolutionAndProjectFileAnalyser
{
private static bool _isRegistered;

public static async Task AnalyseAsync(string filePath, DependencyGraph<string> graph, ILogger logger)
{
if (!_isRegistered)
{
var instance = MSBuildLocator.QueryVisualStudioInstances().OrderByDescending(i => i.Version).First();

logger.WriteLine($"Located MSBuild at: {instance.MSBuildPath}");

MSBuildLocator.RegisterInstance(instance);

_isRegistered = true;
}

using var workspace = MSBuildWorkspace.Create();

workspace.WorkspaceFailed += (o, e) => logger.WriteLine(e.Diagnostic.Message);

if (filePath.EndsWith(".sln"))
{
logger.WriteLine($"Loading solution: {filePath}");
await workspace.OpenSolutionAsync(filePath);
logger.WriteLine($"Finished loading solution: {filePath}");
}
else
{
logger.WriteLine($"Loading project: {filePath}");
await workspace.OpenProjectAsync(filePath);
logger.WriteLine($"Finished loading project: {filePath}");
}

var projectById = new Dictionary<Guid, Project>();

foreach (var project in workspace.CurrentSolution.Projects)
{
projectById.Add(project.Id.Id, project);
}

foreach (var project in workspace.CurrentSolution.Projects)
{
foreach (var projectReference in project.ProjectReferences)
{
var referencedProject = projectById[projectReference.ProjectId.Id];

graph.AddDependency(project.Name, referencedProject.Name);
}
}
}
}
}
Binary file removed Documentation/filter-window.png
Binary file not shown.
Binary file removed Documentation/four-node-graph.png
Binary file not shown.
1 change: 0 additions & 1 deletion Documentation/four-node-graph.svg

This file was deleted.

Binary file removed Documentation/many-node-graph.png
Binary file not shown.
Binary file removed Documentation/ui-filtered.png
Binary file not shown.
Binary file removed Documentation/ui-unfiltered.png
Binary file not shown.
Binary file removed Libraries/WinGraphviz_v1.02.17.msi
Binary file not shown.
Binary file removed Libraries/WinGraphviz_v1.02.24.msi
Binary file not shown.

0 comments on commit 2f6ec6c

Please sign in to comment.