diff --git a/Source/DependencyAnalyser.sln b/DependencyAnalyser.sln similarity index 67% rename from Source/DependencyAnalyser.sln rename to DependencyAnalyser.sln index 613f937..430e750 100644 --- a/Source/DependencyAnalyser.sln +++ b/DependencyAnalyser.sln @@ -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 @@ -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 diff --git a/Source/DependencyAnalyser/App.ico b/DependencyAnalyser/App.ico similarity index 100% rename from Source/DependencyAnalyser/App.ico rename to DependencyAnalyser/App.ico diff --git a/DependencyAnalyser/App.xaml b/DependencyAnalyser/App.xaml new file mode 100644 index 0000000..993d907 --- /dev/null +++ b/DependencyAnalyser/App.xaml @@ -0,0 +1,5 @@ + diff --git a/DependencyAnalyser/App.xaml.cs b/DependencyAnalyser/App.xaml.cs new file mode 100644 index 0000000..57d3206 --- /dev/null +++ b/DependencyAnalyser/App.xaml.cs @@ -0,0 +1,8 @@ +using System.Windows; + +namespace DependencyAnalyser +{ + public partial class App : Application + { + } +} diff --git a/Source/DependencyAnalyser/AssemblyAnalyser.cs b/DependencyAnalyser/AssemblyAnalyser.cs similarity index 97% rename from Source/DependencyAnalyser/AssemblyAnalyser.cs rename to DependencyAnalyser/AssemblyAnalyser.cs index 5850247..f7af2b8 100644 --- a/Source/DependencyAnalyser/AssemblyAnalyser.cs +++ b/DependencyAnalyser/AssemblyAnalyser.cs @@ -23,7 +23,7 @@ public static void Analyze(Assembly assembly, DependencyGraph graph, ILo foreach (var dependantAssemblyName in dependencies) { - logger.WriteLine("- {dependantAssemblyName.Name}"); + logger.WriteLine($"- {dependantAssemblyName.Name}"); if (!graph.AddDependency(assemblyName, dependantAssemblyName.Name)) { diff --git a/Source/DependencyAnalyser/Controls/TriStateTreeView.cs b/DependencyAnalyser/Controls/TriStateTreeView.cs similarity index 100% rename from Source/DependencyAnalyser/Controls/TriStateTreeView.cs rename to DependencyAnalyser/Controls/TriStateTreeView.cs diff --git a/Source/DependencyAnalyser/Controls/TriStateTreeView.resx b/DependencyAnalyser/Controls/TriStateTreeView.resx similarity index 100% rename from Source/DependencyAnalyser/Controls/TriStateTreeView.resx rename to DependencyAnalyser/Controls/TriStateTreeView.resx diff --git a/Source/DependencyAnalyser/DependencyAnalyser.csproj b/DependencyAnalyser/DependencyAnalyser.csproj similarity index 66% rename from Source/DependencyAnalyser/DependencyAnalyser.csproj rename to DependencyAnalyser/DependencyAnalyser.csproj index d13d37f..f163265 100644 --- a/Source/DependencyAnalyser/DependencyAnalyser.csproj +++ b/DependencyAnalyser/DependencyAnalyser.csproj @@ -11,29 +11,17 @@ Displays dependencies between projects/assemblies as a directed graph. https://drewnoakes.com Drew Noakes 2003-2021 + true + true - - <_Parameter1>$(AssemblyName).Tests - - - - {052DB09C-95F7-43BD-B7F8-492373D1151E} - 1 - 0 - 0 - tlbimp - False - False - - + - \ No newline at end of file diff --git a/DependencyAnalyser/DependencyAnalyzerWindow.xaml b/DependencyAnalyser/DependencyAnalyzerWindow.xaml new file mode 100644 index 0000000..be602f1 --- /dev/null +++ b/DependencyAnalyser/DependencyAnalyzerWindow.xaml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/DependencyAnalyser/DependencyAnalyzerWindow.xaml.cs b/DependencyAnalyser/DependencyAnalyzerWindow.xaml.cs new file mode 100644 index 0000000..ace8ce5 --- /dev/null +++ b/DependencyAnalyser/DependencyAnalyzerWindow.xaml.cs @@ -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 _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(); + + 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(); + } + } +} diff --git a/Source/DependencyAnalyser/DependencyGraph.cs b/DependencyAnalyser/DependencyGraph.cs similarity index 100% rename from Source/DependencyAnalyser/DependencyGraph.cs rename to DependencyAnalyser/DependencyGraph.cs diff --git a/Source/DependencyAnalyser/FilterForm.Designer.cs b/DependencyAnalyser/FilterForm.Designer.cs similarity index 100% rename from Source/DependencyAnalyser/FilterForm.Designer.cs rename to DependencyAnalyser/FilterForm.Designer.cs diff --git a/Source/DependencyAnalyser/FilterForm.cs b/DependencyAnalyser/FilterForm.cs similarity index 100% rename from Source/DependencyAnalyser/FilterForm.cs rename to DependencyAnalyser/FilterForm.cs diff --git a/Source/DependencyAnalyser/FilterForm.resx b/DependencyAnalyser/FilterForm.resx similarity index 100% rename from Source/DependencyAnalyser/FilterForm.resx rename to DependencyAnalyser/FilterForm.resx diff --git a/Source/DependencyAnalyser/FilterPreferences.cs b/DependencyAnalyser/FilterPreferences.cs similarity index 100% rename from Source/DependencyAnalyser/FilterPreferences.cs rename to DependencyAnalyser/FilterPreferences.cs diff --git a/Source/DependencyAnalyser/Graphics/Filter.ico b/DependencyAnalyser/Graphics/Filter.ico similarity index 100% rename from Source/DependencyAnalyser/Graphics/Filter.ico rename to DependencyAnalyser/Graphics/Filter.ico diff --git a/Source/DependencyAnalyser/ILogger.cs b/DependencyAnalyser/ILogger.cs similarity index 100% rename from Source/DependencyAnalyser/ILogger.cs rename to DependencyAnalyser/ILogger.cs diff --git a/DependencyAnalyser/SolutionAndProjectFileAnalyser.cs b/DependencyAnalyser/SolutionAndProjectFileAnalyser.cs new file mode 100644 index 0000000..e35041c --- /dev/null +++ b/DependencyAnalyser/SolutionAndProjectFileAnalyser.cs @@ -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 +{ + /// + /// Builds a DependencyGraph from a given Visual Studio solution (.sln) or project (e.g. .csproj) file. + /// + public static class SolutionAndProjectFileAnalyser + { + private static bool _isRegistered; + + public static async Task AnalyseAsync(string filePath, DependencyGraph 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(); + + 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); + } + } + } + } +} \ No newline at end of file diff --git a/Documentation/filter-window.png b/Documentation/filter-window.png deleted file mode 100644 index 72ed650..0000000 Binary files a/Documentation/filter-window.png and /dev/null differ diff --git a/Documentation/four-node-graph.png b/Documentation/four-node-graph.png deleted file mode 100644 index 29b4ad1..0000000 Binary files a/Documentation/four-node-graph.png and /dev/null differ diff --git a/Documentation/four-node-graph.svg b/Documentation/four-node-graph.svg deleted file mode 100644 index 222d6fa..0000000 --- a/Documentation/four-node-graph.svg +++ /dev/null @@ -1 +0,0 @@ - G 1 Drew.DependencyAnalyser.Tests 3 Drew.DependencyAnalyser 1->3 17 nunit.framework 1->17 15 Interop.WINGRAPHVIZLib 3->15 \ No newline at end of file diff --git a/Documentation/many-node-graph.png b/Documentation/many-node-graph.png deleted file mode 100644 index 9084c05..0000000 Binary files a/Documentation/many-node-graph.png and /dev/null differ diff --git a/Documentation/ui-filtered.png b/Documentation/ui-filtered.png deleted file mode 100644 index 6074dad..0000000 Binary files a/Documentation/ui-filtered.png and /dev/null differ diff --git a/Documentation/ui-unfiltered.png b/Documentation/ui-unfiltered.png deleted file mode 100644 index 87ff2b3..0000000 Binary files a/Documentation/ui-unfiltered.png and /dev/null differ diff --git a/Libraries/WinGraphviz_v1.02.17.msi b/Libraries/WinGraphviz_v1.02.17.msi deleted file mode 100644 index 6d44b30..0000000 Binary files a/Libraries/WinGraphviz_v1.02.17.msi and /dev/null differ diff --git a/Libraries/WinGraphviz_v1.02.24.msi b/Libraries/WinGraphviz_v1.02.24.msi deleted file mode 100644 index f6c3ccd..0000000 Binary files a/Libraries/WinGraphviz_v1.02.24.msi and /dev/null differ diff --git a/README.md b/README.md index ac05bb3..bc935d1 100644 --- a/README.md +++ b/README.md @@ -4,64 +4,23 @@ After manually drawing an assembly dependency graph for a twenty-something-proje Run the application, and open a .NET assembly or solution from the _File|Open..._ menu (or just press Ctrl+O). -The analyser will immediately generate a diagram such as the one shown: +Let's see how it looks if we run the application on itself: -![Example screenshot from .NET Assembly Dependency Analyser graph](https://raw.githubusercontent.com/drewnoakes/dependency-analyser/master/Documentation/ui-unfiltered.png) +![Example screenshot from .NET Assembly Dependency Analyser graph](img/ui-unfiltered.png) -Most assemblies will reference large numbers of system assemblies, either directly or indirectly. -In this graph, both the `System` and `mscorlib` assemblies are referenced by almost all other assemblies. -The diagram is clearer without these explicit references. +As you can see there is a lot of information here and the graph is impossible to read as-is. +It's possible to zoom and pan, and you can drag nodes around to make things easier to see, +but most of the time you'll want to hide assemblies you're not interested in. -See this graph, exported as a [PNG file](https://raw.githubusercontent.com/drewnoakes/dependency-analyser/master/Documentation/many-node-graph.png). +Using the _Filter..._ command lets us exclude items we don't want to see. Removing several +`System.*`and other framework assemblies gives a clearer picture: -![Example of the exclude menu showing how to omit selected assemblies from the graph](https://raw.githubusercontent.com/drewnoakes/dependency-analyser/master/Documentation/filter-window.png) +![A graph showing dependencies when most of the behind-the-scenes assemblies have been removed](img/ui-filtered.png) -Select which assemblies you want to include in the plot and press OK. - -![A graph showing dependencies when most of the behind-the-scenes assemblies have been removed](https://raw.githubusercontent.com/drewnoakes/dependency-analyser/master/Documentation/ui-filtered.png) - -This plot tells us a great deal about the analysed assembly (in this case, `DependencyAnalyser.Tests.dll`). -It requires three non-system assemblies: `DependencyAnalyser`, `DependencyAnalyser.Tests` and `nunit.framework`, -even though `nunit.framework` is not referenced directly. We can also tell, unsurprisingly, that `DependencyAnalyser` -uses WinForms assemblies. - -Note the [circular dependency between `System` and `System.Xml`](https://stackoverflow.com/q/1316518/24874)!!! - -Excluding all `System` assemblies shows all dependent assemblies that must be deployed with the -selected assembly for it to operate properly. - -![A graph showing uncluttered core dependencies when all framework and other supporting assemblies have been removed](https://raw.githubusercontent.com/drewnoakes/dependency-analyser/master/Documentation/four-node-graph.png) - -Here's an example of the [SVG output](https://raw.githubusercontent.com/drewnoakes/dependency-analyser/master/Documentation/four-node-graph.svg). - -## The graph - -The graph is produced via a Dot script. WinGraphViz is a COM component that generates Dot -images, and is used by this application. It must be installed for the .NET dependency analyser -to work. - -A Dot script may look something like this: - - digraph G { - size="100,50" - center="" - ratio=All - node[width=.25,hight=.375,fontsize=12,color=lightblue2,style=filled] - 1 -> 3; - 1 -> 17; - 3 -> 15; - 1 [label="DependencyAnalyser.Tests"]; - 3 [label="DependencyAnalyser"]; - 15 [label="Interop.WINGRAPHVIZLib"]; - 17 [label="nunit.framework"]; - } - -This markup can be seen in the 'Dot Output' tab. - -More information on Dot can be found at http://www.graphviz.org/ +You can open either assemblies or MSBuild project/solution files. The latter produces richer data, +including target frameworks which can be useful for multi-targeting projects. ## Installation 1. Download zipped binaries on the [releases page](https://github.com/drewnoakes/dependency-analyser/releases) and extract to a folder on your PC. -2. Run the WinGraphviz installer included in the archive (if you do not already have it installed). -3. Run `DependencyAnalyser.exe` to start the program. \ No newline at end of file +2. Run `DependencyAnalyser.exe` to start the program. \ No newline at end of file diff --git a/Source/DependencyAnalyser.Tests/DependencyAnalyser.Tests.csproj b/Source/DependencyAnalyser.Tests/DependencyAnalyser.Tests.csproj deleted file mode 100644 index ba89e81..0000000 --- a/Source/DependencyAnalyser.Tests/DependencyAnalyser.Tests.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - - net48 - - - - - - - - - - - \ No newline at end of file diff --git a/Source/DependencyAnalyser.Tests/DependencyGraphTest.cs b/Source/DependencyAnalyser.Tests/DependencyGraphTest.cs deleted file mode 100644 index 475d28d..0000000 --- a/Source/DependencyAnalyser.Tests/DependencyGraphTest.cs +++ /dev/null @@ -1,66 +0,0 @@ -using Xunit; - -namespace DependencyAnalyser.Tests -{ - public sealed class DependencyGraphTest - { - [Fact] - public void AddDependencyTest() - { - var graph = new DependencyGraph(); - graph.AddDependency("A", "B"); - - Assert.Equal(new[] { "A", "B" }, graph.Nodes); - Assert.Equal(new[] { "B" }, graph.GetDependenciesForNode("A")); - } - - [Fact] - public void TransitiveReduceThreeNodes() - { - var graph = new DependencyGraph(); - graph.AddDependency('a', 'b'); - graph.AddDependency('b', 'c'); - graph.AddDependency('a', 'c'); - - graph.TransitiveReduce(); - - Assert.Equal(new[] { 'a', 'b', 'c' }, graph.Nodes); - Assert.Equal(new[] { 'b' }, graph.GetDependenciesForNode('a')); - Assert.Equal(new[] { 'c' }, graph.GetDependenciesForNode('b')); - } - - [Fact] - public void TransitiveReduceCascade() - { - var graph = new DependencyGraph(); - - for (var c1 = 'a'; c1 < 'f'; c1++) - for (var c2 = (char)(c1 + 1); c2 < 'f'; c2++) - graph.AddDependency(c1, c2); - - graph.TransitiveReduce(); - - for (var c = 'a'; c < 'f' - 1; c++) - Assert.Equal(new[] { (char)(c + 1) }, graph.GetDependenciesForNode(c)); - } - - [Fact] - public void TransitiveReduceCycles() - { - var graph = new DependencyGraph(); - - graph.AddDependency('a', 'b'); // a <--> b - graph.AddDependency('b', 'a'); - - graph.AddDependency('a', 'c'); // a --> c - graph.AddDependency('b', 'c'); // b --> c - - graph.TransitiveReduce(); - - // NOTE result here depends upon order of enumeration from HashSet, making this test potentially fragile - - Assert.Equal(new[] { 'b' }, graph.GetDependenciesForNode('a')); - Assert.Equal(new[] { 'a', 'c' }, graph.GetDependenciesForNode('b')); - } - } -} diff --git a/Source/DependencyAnalyser.Tests/DotCommandBuilderTest.cs b/Source/DependencyAnalyser.Tests/DotCommandBuilderTest.cs deleted file mode 100644 index 402b98b..0000000 --- a/Source/DependencyAnalyser.Tests/DotCommandBuilderTest.cs +++ /dev/null @@ -1,49 +0,0 @@ -using Xunit; - -namespace DependencyAnalyser.Tests -{ - public sealed class DotCommandBuilderTest - { - [Fact] - public void GenerateDotCommand() - { - var graph = new DependencyGraph(); - graph.AddDependency("A", "B"); - - var filterPreferences = new FilterPreferences(); - filterPreferences.SetAssemblyNames(graph.Nodes); - var command = DotCommandBuilder.Generate(graph, filterPreferences); - - const string expected = @"digraph G { - 1 -> 2; - 1 [label=""A""]; - 2 [label=""B""]; -}"; - - Assert.Equal(expected, command); - } - - [Fact] - public void GenerateDotCommand_ExclusionList() - { - var graph = new DependencyGraph(); - graph.AddDependency("A", "B"); - graph.AddDependency("B", "C"); - graph.AddDependency("C", "A"); - - var filterPreferences = new FilterPreferences(); - filterPreferences.SetAssemblyNames(graph.Nodes); - filterPreferences.Exclude("C"); - - var command = DotCommandBuilder.Generate(graph, filterPreferences); - - const string expected = @"digraph G { - 1 -> 2; - 1 [label=""A""]; - 2 [label=""B""]; -}"; - - Assert.Equal(expected, command); - } - } -} diff --git a/Source/DependencyAnalyser.Tests/TemporaryFileManagerTest.cs b/Source/DependencyAnalyser.Tests/TemporaryFileManagerTest.cs deleted file mode 100644 index b5e6d02..0000000 --- a/Source/DependencyAnalyser.Tests/TemporaryFileManagerTest.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System.IO; - -using Xunit; - -namespace DependencyAnalyser.Tests -{ - public sealed class TemporaryFileManagerTest - { - [Fact] - public void CreateTemporaryFile_Format() - { - var filename = TemporaryFileManager.CreateTemporaryFile(); - Assert.StartsWith("DependencyAnaylser.", Path.GetFileName(filename)); - Assert.EndsWith(".tmp", Path.GetFileName(filename)); - Assert.Equal(Path.GetTempPath(), Path.GetDirectoryName(filename) + "\\"); - } - - [Fact] - public void CreateTemporaryFile_CreatesZeroByteFile() - { - var filename = TemporaryFileManager.CreateTemporaryFile(); - Assert.True(File.Exists(filename)); - Assert.Equal(0L, new FileInfo(filename).Length); - } - - [Fact] - public void GetExistingTemporaryFilenames() - { - TemporaryFileManager.DeleteAllTemporaryFiles(); - var filename1 = TemporaryFileManager.CreateTemporaryFile(); - var filename2 = TemporaryFileManager.CreateTemporaryFile(); - var filenames = TemporaryFileManager.GetExistingTemporaryFilenames(); - Assert.Equal(2, filenames.Length); - Assert.True(filename1!=filename2); - Assert.True(filename1==filenames[0] || filename1==filenames[1]); - Assert.True(filename2==filenames[0] || filename2==filenames[1]); - } - - [Fact] - public void DeleteAllTemporaryFiles() - { - TemporaryFileManager.DeleteAllTemporaryFiles(); - TemporaryFileManager.CreateTemporaryFile(); - TemporaryFileManager.CreateTemporaryFile(); - Assert.Equal(2, TemporaryFileManager.GetExistingTemporaryFilenames().Length); - TemporaryFileManager.DeleteAllTemporaryFiles(); - Assert.Empty(TemporaryFileManager.GetExistingTemporaryFilenames()); - } - } -} diff --git a/Source/DependencyAnalyser/DependencyAnalyserForm.Designer.cs b/Source/DependencyAnalyser/DependencyAnalyserForm.Designer.cs deleted file mode 100644 index 9dd3a76..0000000 --- a/Source/DependencyAnalyser/DependencyAnalyserForm.Designer.cs +++ /dev/null @@ -1,299 +0,0 @@ -using System.Windows.Forms; - -namespace DependencyAnalyser -{ - partial class DependencyAnalyserForm - { - /// - /// Required designer variable. - /// - private System.ComponentModel.IContainer components = null; - - /// - /// Clean up any resources being used. - /// - /// true if managed resources should be disposed; otherwise, false. - protected override void Dispose(bool disposing) - { - if (disposing && (components != null)) - { - components.Dispose(); - } - - // delete up our temporary files - TemporaryFileManager.DeleteAllTemporaryFiles(); - - base.Dispose(disposing); - } - - #region Windows Form Designer generated code - - /// - /// Required method for Designer support - do not modify - /// the contents of this method with the code editor. - /// - private void InitializeComponent() - { - this.components = new System.ComponentModel.Container(); - System.Windows.Forms.MenuItem _mnuFile; - System.Windows.Forms.MenuItem _mnuFileOpen; - System.Windows.Forms.MenuItem _mnuFileSeparator; - System.Windows.Forms.MenuItem _mnuFileExit; - System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(DependencyAnalyserForm)); - this._mnuFileMerge = new System.Windows.Forms.MenuItem(); - this._mnuFileSavePng = new System.Windows.Forms.MenuItem(); - this._mnuFileSaveSvg = new System.Windows.Forms.MenuItem(); - this._mnuFilePrint = new System.Windows.Forms.MenuItem(); - this._mnuFilePrintPreview = new System.Windows.Forms.MenuItem(); - this._mnuAbout = new System.Windows.Forms.MenuItem(); - this._openFileDialog = new System.Windows.Forms.OpenFileDialog(); - this._menu = new System.Windows.Forms.MainMenu(this.components); - this._mnuFilter = new System.Windows.Forms.MenuItem(); - this._savePngFileDialog = new System.Windows.Forms.SaveFileDialog(); - this._saveSvgFileDialog = new System.Windows.Forms.SaveFileDialog(); - this._printDialog = new System.Windows.Forms.PrintDialog(); - this._printPreviewDialog = new System.Windows.Forms.PrintPreviewDialog(); - this._tabPageMessages = new System.Windows.Forms.TabPage(); - this._txtMessage = new System.Windows.Forms.TextBox(); - this._tabPageDotOutput = new System.Windows.Forms.TabPage(); - this._txtDotScriptOutput = new System.Windows.Forms.TextBox(); - this._tabPageImage = new System.Windows.Forms.TabPage(); - this._imgDotDiagram = new System.Windows.Forms.PictureBox(); - this._tabControl = new System.Windows.Forms.TabControl(); - _mnuFile = new System.Windows.Forms.MenuItem(); - _mnuFileOpen = new System.Windows.Forms.MenuItem(); - _mnuFileSeparator = new System.Windows.Forms.MenuItem(); - _mnuFileExit = new System.Windows.Forms.MenuItem(); - this._tabPageMessages.SuspendLayout(); - this._tabPageDotOutput.SuspendLayout(); - this._tabPageImage.SuspendLayout(); - ((System.ComponentModel.ISupportInitialize)(this._imgDotDiagram)).BeginInit(); - this._tabControl.SuspendLayout(); - this.SuspendLayout(); - // - // _mnuFile - // - _mnuFile.Index = 0; - _mnuFile.MenuItems.AddRange(new System.Windows.Forms.MenuItem[] { - _mnuFileOpen, - this._mnuFileMerge, - this._mnuFileSavePng, - this._mnuFileSaveSvg, - this._mnuFilePrint, - this._mnuFilePrintPreview, - _mnuFileSeparator, - _mnuFileExit}); - _mnuFile.Text = "&File"; - // - // _mnuFileOpen - // - _mnuFileOpen.Index = 0; - _mnuFileOpen.Shortcut = System.Windows.Forms.Shortcut.CtrlO; - _mnuFileOpen.Text = "&Open..."; - _mnuFileOpen.Click += new System.EventHandler(this.menuFileOpen_Click); - // - // _mnuFileMerge - // - this._mnuFileMerge.Index = 1; - this._mnuFileMerge.Shortcut = System.Windows.Forms.Shortcut.CtrlM; - this._mnuFileMerge.Text = "Merge..."; - this._mnuFileMerge.Click += new System.EventHandler(this.menuFileMerge_Click); - // - // _mnuFileSavePng - // - this._mnuFileSavePng.Index = 2; - this._mnuFileSavePng.Shortcut = System.Windows.Forms.Shortcut.CtrlS; - this._mnuFileSavePng.Text = "Save &PNG..."; - this._mnuFileSavePng.Click += new System.EventHandler(this.menuFileSavePng_Click); - // - // _mnuFileSaveSvg - // - this._mnuFileSaveSvg.Index = 3; - this._mnuFileSaveSvg.Text = "Save &SVG..."; - this._mnuFileSaveSvg.Click += new System.EventHandler(this.menuFileSaveSvg_Click); - // - // _mnuFilePrint - // - this._mnuFilePrint.Index = 4; - this._mnuFilePrint.Text = "Print..."; - this._mnuFilePrint.Click += new System.EventHandler(this.menuFilePrint_Click); - // - // _mnuFilePrintPreview - // - this._mnuFilePrintPreview.Index = 5; - this._mnuFilePrintPreview.Text = "Print preview..."; - this._mnuFilePrintPreview.Click += new System.EventHandler(this.menuFilePrintPreview_Click); - // - // _mnuFileSeparator - // - _mnuFileSeparator.Index = 6; - _mnuFileSeparator.Text = "-"; - // - // _mnuFileExit - // - _mnuFileExit.Index = 7; - _mnuFileExit.Text = "E&xit"; - _mnuFileExit.Click += new System.EventHandler(this.menuFileExit_Click); - // - // _mnuAbout - // - this._mnuAbout.Index = 2; - this._mnuAbout.Text = "About..."; - this._mnuAbout.Click += new System.EventHandler(this.mnuAbout_Click); - // - // _openFileDialog - // - this._openFileDialog.Filter = "All supported files|*.sln;*.exe;*.dll|Solution files (*.sln)|*.sln|Assemblies (*." + - "dll)|*.dll|Assemblies (*.exe)|*.exe|All files|*.exe"; - this._openFileDialog.Title = "Open File"; - // - // _menu - // - this._menu.MenuItems.AddRange(new System.Windows.Forms.MenuItem[] { - _mnuFile, - this._mnuFilter, - this._mnuAbout}); - // - // _mnuFilter - // - this._mnuFilter.Index = 1; - this._mnuFilter.Text = "F&ilter..."; - this._mnuFilter.Click += new System.EventHandler(this.menuFilter_Click); - // - // _savePngFileDialog - // - this._savePngFileDialog.DefaultExt = "png"; - this._savePngFileDialog.FileName = "DependencyGraph.png"; - this._savePngFileDialog.Filter = "PNG Images|*.png|All files|*.*"; - // - // _saveSvgFileDialog - // - this._saveSvgFileDialog.FileName = "DependencyGraph.svg"; - this._saveSvgFileDialog.Filter = "SVG files (*.svg)|*.svg|All files|*.*"; - // - // _printPreviewDialog - // - this._printPreviewDialog.AutoScrollMargin = new System.Drawing.Size(0, 0); - this._printPreviewDialog.AutoScrollMinSize = new System.Drawing.Size(0, 0); - this._printPreviewDialog.ClientSize = new System.Drawing.Size(400, 300); - this._printPreviewDialog.Enabled = true; - this._printPreviewDialog.Icon = ((System.Drawing.Icon)(resources.GetObject("_printPreviewDialog.Icon"))); - this._printPreviewDialog.Name = "_printPreviewDialog"; - this._printPreviewDialog.Visible = false; - // - // _tabPageMessages - // - this._tabPageMessages.Controls.Add(this._txtMessage); - this._tabPageMessages.Location = new System.Drawing.Point(4, 4); - this._tabPageMessages.Name = "_tabPageMessages"; - this._tabPageMessages.Size = new System.Drawing.Size(680, 343); - this._tabPageMessages.TabIndex = 2; - this._tabPageMessages.Text = "Messages"; - // - // _txtMessage - // - this._txtMessage.Dock = System.Windows.Forms.DockStyle.Fill; - this._txtMessage.Location = new System.Drawing.Point(0, 0); - this._txtMessage.Multiline = true; - this._txtMessage.Name = "_txtMessage"; - this._txtMessage.ScrollBars = System.Windows.Forms.ScrollBars.Both; - this._txtMessage.Size = new System.Drawing.Size(680, 343); - this._txtMessage.TabIndex = 0; - // - // _tabPageDotOutput - // - this._tabPageDotOutput.Controls.Add(this._txtDotScriptOutput); - this._tabPageDotOutput.Location = new System.Drawing.Point(4, 4); - this._tabPageDotOutput.Name = "_tabPageDotOutput"; - this._tabPageDotOutput.Size = new System.Drawing.Size(680, 343); - this._tabPageDotOutput.TabIndex = 0; - this._tabPageDotOutput.Text = "Dot Output"; - // - // _txtDotScriptOutput - // - this._txtDotScriptOutput.Dock = System.Windows.Forms.DockStyle.Fill; - this._txtDotScriptOutput.Location = new System.Drawing.Point(0, 0); - this._txtDotScriptOutput.Multiline = true; - this._txtDotScriptOutput.Name = "_txtDotScriptOutput"; - this._txtDotScriptOutput.ScrollBars = System.Windows.Forms.ScrollBars.Both; - this._txtDotScriptOutput.Size = new System.Drawing.Size(680, 343); - this._txtDotScriptOutput.TabIndex = 0; - // - // _tabPageImage - // - this._tabPageImage.Controls.Add(this._imgDotDiagram); - this._tabPageImage.Location = new System.Drawing.Point(4, 4); - this._tabPageImage.Name = "_tabPageImage"; - this._tabPageImage.Size = new System.Drawing.Size(680, 343); - this._tabPageImage.TabIndex = 1; - this._tabPageImage.Text = "Image"; - // - // _imgDotDiagram - // - this._imgDotDiagram.Dock = System.Windows.Forms.DockStyle.Fill; - this._imgDotDiagram.Location = new System.Drawing.Point(0, 0); - this._imgDotDiagram.Name = "_imgDotDiagram"; - this._imgDotDiagram.Size = new System.Drawing.Size(680, 343); - this._imgDotDiagram.SizeMode = System.Windows.Forms.PictureBoxSizeMode.StretchImage; - this._imgDotDiagram.TabIndex = 0; - this._imgDotDiagram.TabStop = false; - // - // _tabControl - // - this._tabControl.Alignment = System.Windows.Forms.TabAlignment.Bottom; - this._tabControl.Controls.Add(this._tabPageImage); - this._tabControl.Controls.Add(this._tabPageDotOutput); - this._tabControl.Controls.Add(this._tabPageMessages); - this._tabControl.Dock = System.Windows.Forms.DockStyle.Fill; - this._tabControl.Location = new System.Drawing.Point(0, 0); - this._tabControl.Name = "_tabControl"; - this._tabControl.SelectedIndex = 0; - this._tabControl.Size = new System.Drawing.Size(688, 369); - this._tabControl.TabIndex = 0; - // - // DependencyAnalyserForm - // - this.AutoScaleBaseSize = new System.Drawing.Size(5, 13); - this.ClientSize = new System.Drawing.Size(688, 369); - this.Controls.Add(this._tabControl); - this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon"))); - this.Menu = this._menu; - this.Name = "DependencyAnalyserForm"; - this.Text = ".NET Assembly Dependency Analyser"; - this._tabPageMessages.ResumeLayout(false); - this._tabPageMessages.PerformLayout(); - this._tabPageDotOutput.ResumeLayout(false); - this._tabPageDotOutput.PerformLayout(); - this._tabPageImage.ResumeLayout(false); - ((System.ComponentModel.ISupportInitialize)(this._imgDotDiagram)).EndInit(); - this._tabControl.ResumeLayout(false); - this.ResumeLayout(false); - - } - - private OpenFileDialog _openFileDialog; - private SaveFileDialog _savePngFileDialog; - private SaveFileDialog _saveSvgFileDialog; - private PrintDialog _printDialog; - private PrintPreviewDialog _printPreviewDialog; - - private MainMenu _menu; - private MenuItem _mnuAbout; - private MenuItem _mnuFileSavePng; - private MenuItem _mnuFileSaveSvg; - private MenuItem _mnuFilePrint; - private MenuItem _mnuFilePrintPreview; - private MenuItem _mnuFileMerge; - private MenuItem _mnuFilter; - private TabPage _tabPageMessages; - private TextBox _txtMessage; - private TabPage _tabPageDotOutput; - private TextBox _txtDotScriptOutput; - private TabPage _tabPageImage; - private PictureBox _imgDotDiagram; - private TabControl _tabControl; - - #endregion - - } -} \ No newline at end of file diff --git a/Source/DependencyAnalyser/DependencyAnalyserForm.cs b/Source/DependencyAnalyser/DependencyAnalyserForm.cs deleted file mode 100644 index e54af37..0000000 --- a/Source/DependencyAnalyser/DependencyAnalyserForm.cs +++ /dev/null @@ -1,232 +0,0 @@ -using System; -using System.Drawing.Imaging; -using System.Drawing.Printing; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Threading.Tasks; -using System.Windows.Forms; - -namespace DependencyAnalyser -{ - public partial class DependencyAnalyserForm : Form - { - private readonly FilterPreferences _filterPreferences = new(); - private readonly StringBuilderLogger _logger = new(); - - private DependencyGraph _graph = new(); - private PlotResult? _plotResult; - - public DependencyAnalyserForm() - { - InitializeComponent(); - EnableAndDisableMenuItems(); - } - - private void EnableAndDisableMenuItems() - { - var hasGraph = _graph.Nodes.Any(); - - _mnuFileSavePng.Enabled = hasGraph; - _mnuFileSaveSvg.Enabled = hasGraph; - _mnuFilter.Enabled = hasGraph; - _mnuFilePrint.Enabled = hasGraph; - _mnuFilePrintPreview.Enabled = hasGraph; - _mnuFileMerge.Enabled = hasGraph; - } - - #region Event handlers - - private async void menuFileOpen_Click(object sender, EventArgs e) - { - // Start a new graph - _graph = new DependencyGraph(); - - await MergeFileAsync(); - } - - private async void menuFileMerge_Click(object sender, EventArgs e) - { - await MergeFileAsync(); - } - - private async Task MergeFileAsync() - { - try - { - if (_openFileDialog.ShowDialog() != DialogResult.OK) - { - return; - } - - string fileName = _openFileDialog.FileName; - - using (WaitCursor()) - { - if (fileName.EndsWith(".sln")) - { - await SolutionFileAnalyser.AnalyseAsync(fileName, _graph, _logger); - } - else - { - var assembly = Assembly.LoadFrom(fileName); - AssemblyAnalyser.Analyze(assembly, _graph, _logger); - } - - _filterPreferences.SetAssemblyNames(_graph.Nodes); - EnableAndDisableMenuItems(); - UpdateImage(); - } - } - catch (Exception e) - { - MessageBox.Show(e.ToString()); - } - } - - private void menuFileSavePng_Click(object sender, EventArgs e) - { - if (_savePngFileDialog.ShowDialog() != DialogResult.OK) - return; - - using (WaitCursor()) - using (var fileStream = (FileStream)_savePngFileDialog.OpenFile()) - SavePngImage(fileStream); - } - - private void menuFileExit_Click(object sender, EventArgs e) - { - Application.Exit(); - } - - private void menuFileSaveSvg_Click(object sender, EventArgs e) - { - if (_saveSvgFileDialog.ShowDialog() != DialogResult.OK) - return; - - using (var fileStream = (FileStream)_saveSvgFileDialog.OpenFile()) - SaveSvgImage(fileStream); - } - - private void mnuAbout_Click(object sender, EventArgs 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 menuFilter_Click(object sender, EventArgs e) - { - using (WaitCursor()) - { - var filterForm = new FilterForm(_filterPreferences); - - if (filterForm.ShowDialog() == DialogResult.OK) - UpdateImage(); - } - } - - #endregion - - protected override void OnFormClosed(FormClosedEventArgs e) - { - base.OnFormClosed(e); - - TemporaryFileManager.DeleteAllTemporaryFiles(); - } - - #region Image handling - - private void SavePngImage(Stream fileStream) - { - _imgDotDiagram.Image.Save(fileStream, ImageFormat.Png); - MessageBox.Show("PNG file saved."); - } - - private void SaveSvgImage(Stream fileStream) - { - if (_plotResult?.SvgXml == null) - throw new Exception("Unable to save SVG image."); - - using (var writer = new StreamWriter(fileStream)) - writer.Write(_plotResult.SvgXml); - MessageBox.Show("SVG file saved."); - } - - private void UpdateImage() - { - var aspectRatio = _imgDotDiagram.Width / (double)_imgDotDiagram.Height; - - _plotResult = DependencyPlotter.CalculatePlot(aspectRatio, _graph, _filterPreferences); - - _txtMessage.Text = _logger.ToString(); - _txtDotScriptOutput.Text = _plotResult.DotCommand; - _imgDotDiagram.Image = _plotResult.Image; - - _imgDotDiagram.Invalidate(); - } - - #endregion - - #region GUI support and feedback to user - - private IDisposable WaitCursor() - { - var reverter = new CursorReverter(Cursor, this); - Cursor = Cursors.WaitCursor; - return reverter; - } - - private sealed class CursorReverter : IDisposable - { - private readonly Cursor _cursor; - private readonly Form _form; - - public CursorReverter(Cursor cursor, Form form) - { - _cursor = cursor; - _form = form; - } - - public void Dispose() - { - _form.Cursor = _cursor; - } - } - - #endregion - - #region Printing - - private void menuFilePrint_Click(object sender, EventArgs e) - { - var printDocument = CreatePrintDocument(); - - _printDialog.Document = printDocument; - - if (_printDialog.ShowDialog() != DialogResult.OK) - return; - - using (WaitCursor()) - printDocument.Print(); - } - - private void menuFilePrintPreview_Click(object sender, EventArgs e) - { - using (WaitCursor()) - { - _printPreviewDialog.Document = CreatePrintDocument(); - _printPreviewDialog.ShowDialog(); - } - } - - private PrintDocument CreatePrintDocument() - { - if (_plotResult?.Image == null) - throw new Exception("unable to create print document when dot image is null"); - - return new DependencyGraphPrintDocument(_plotResult.Image); - } - - #endregion - } -} \ No newline at end of file diff --git a/Source/DependencyAnalyser/DependencyAnalyserForm.resx b/Source/DependencyAnalyser/DependencyAnalyserForm.resx deleted file mode 100644 index 0e8b70e..0000000 --- a/Source/DependencyAnalyser/DependencyAnalyserForm.resx +++ /dev/null @@ -1,348 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - False - - - False - - - False - - - False - - - 17, 17 - - - 786, 17 - - - 895, 17 - - - 17, 54 - - - 157, 54 - - - 642, 17 - - - - - AAABAAYAICAQAAAAAADoAgAAZgAAABAQEAAAAAAAKAEAAE4DAAAgIAAAAQAIAKgIAAB2BAAAEBAAAAEA - CABoBQAAHg0AACAgAAABACAAqBAAAIYSAAAQEAAAAQAgAGgEAAAuIwAAKAAAACAAAABAAAAAAQAEAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAACAgACAAAAAgACAAICAAACAgIAAwMDAAAAA - /wAA/wAAAP//AP8AAAD/AP8A//8AAP///wAiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIoiI - iIiIiIiIiIiIiIiIiIiCIigiIiIozMzMzMzMyCIogiIoIiIiKM7m5ubm5sgiKIIiKCIiIijObm5ubm7I - IiiCIigiIiIozubm5ubmyCIogiIoIiIiKM5ubm5ubsgiKIIiKCIiIijO5ubm5ubIIiiIiIiIiIiIzm5u - bm5uyCIogRERERERGM7u7u7u7sgiKIHZWVlZWRjMzMzMzMzIIiiB1ZWVlZUYiIiIiIiIiIiIgdlZWVlZ - GDMzMzMzMzMzOIHVlZWVlRg/uLi4uLi4uDiB2VlZWVkYP7uLi4uLi4s4gdWVlZWVGD+4uLi4uLi4OIHZ - WVlZWRg/u4uLi4uLiziB1ZWVlZUYP7i4uLi4uLg4gdlZWVlZGD+7i4uLi4uLOIHVlZWVlRg/uLi4uLi4 - uDiB3d3d3d0YP7uLi4uLi4s4gRERERERGD+4uLi4uLi4OIiIiIiIiIg/u4uLi4uLiziCIiIiIiIoP7i4 - uLi4uLg4giIiIiIiKD+7i4uLi4uLOIIiIiIiIig/uLi4uLi4uDiCIiIiIiIoP7u7u7u7u7s4giIiIiIi - KD//////////OIIiIiIiIigzMzMzMzMzMziIiIiIiIiIiIiIiIiIiIiIIiIiIiIiIiIiIiIiIiIiIv// - ////////AAAAAHv4AA57+AAOe/gADnv4AA57+AAOe/gADgAAAA4AAAAOAAAADgAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/4AAB/+AAAf/gAAH/4AAB/+AAAf/gAAAAA - AAD/////KAAAABAAAAAgAAAAAQAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAACA - gACAAAAAgACAAICAAACAgIAAwMDAAAAA/wAA/wAAAP//AP8AAAD/AP8A//8AAP///wAiIiIiIiIiIoiI - iIiIiIiIgigijMzMyCiCKCKM5mbIKIiIiIzu7sgogRERjMzMyCiB2ZGIiIiIiIHZkYMzMzM4gdmRg/u7 - uziB3dGD+7u7OIEREYP7u7s4iIiIg/u7uziCIiKD+7u7OIIiIoP///84giIigzMzMziIiIiIiIiIiP// - KCIAACjObALm5mwCIigAAoiIAAKIzgAAbm4AACIoAAAREQAAGM4AAO7uAAAiKHwAWVl8ABjMfADMzAAA - IigoAAAAIAAAAEAAAAABAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAgAAAAICAAIAA - AACAAIAAgIAAAICAgADA3MAA8MqmAKo/KgD/PyoAAF8qAFVfKgCqXyoA/18qAAB/KgBVfyoAqn8qAP9/ - KgAAnyoAVZ8qAKqfKgD/nyoAAL8qAFW/KgCqvyoA/78qAADfKgBV3yoAqt8qAP/fKgAA/yoAVf8qAKr/ - KgD//yoAAABVAFUAVQCqAFUA/wBVAAAfVQBVH1UAqh9VAP8fVQAAP1UAVT9VAKo/VQD/P1UAAF9VAFVf - VQCqX1UA/19VAAB/VQBVf1UAqn9VAP9/VQAAn1UAVZ9VAKqfVQD/n1UAAL9VAFW/VQCqv1UA/79VAADf - VQBV31UAqt9VAP/fVQAA/1UAVf9VAKr/VQD//1UAAAB/AFUAfwCqAH8A/wB/AAAffwBVH38Aqh9/AP8f - fwAAP38AVT9/AKo/fwD/P38AAF9/AFVffwCqX38A/19/AAB/fwBVf38Aqn9/AP9/fwAAn38AVZ9/AKqf - fwD/n38AAL9/AFW/fwCqv38A/79/AADffwBV338Aqt9/AP/ffwAA/38AVf9/AKr/fwD//38AAACqAFUA - qgCqAKoA/wCqAAAfqgBVH6oAqh+qAP8fqgAAP6oAVT+qAKo/qgD/P6oAAF+qAFVfqgCqX6oA/1+qAAB/ - qgBVf6oAqn+qAP9/qgAAn6oAVZ+qAKqfqgD/n6oAAL+qAFW/qgCqv6oA/7+qAADfqgBV36oAqt+qAP/f - qgAA/6oAVf+qAKr/qgD//6oAAADUAFUA1ACqANQA/wDUAAAf1ABVH9QAqh/UAP8f1AAAP9QAVT/UAKo/ - 1AD/P9QAAF/UAFVf1ACqX9QA/1/UAAB/1ABVf9QAqn/UAP9/1AAAn9QAVZ/UAKqf1AD/n9QAAL/UAFW/ - 1ACqv9QA/7/UAADf1ABV39QAqt/UAP/f1AAA/9QAVf/UAKr/1AD//9QAVQD/AKoA/wAAH/8AVR//AKof - /wD/H/8AAD//AFU//wCqP/8A/z//AABf/wBVX/8Aql//AP9f/wAAf/8AVX//AKp//wD/f/8AAJ//AFWf - /wCqn/8A/5//AAC//wBVv/8Aqr//AP+//wAA3/8AVd//AKrf/wD/3/8AVf//AKr//wD/zMwA/8z/AP// - MwD//2YA//+ZAP//zAAAfwAAVX8AAKp/AAD/fwAAAJ8AAFWfAACqnwAA/58AAAC/AABVvwAAqr8AAP+/ - AAAA3wAAVd8AAKrfAAD/3wAAVf8AAKr/AAAAACoAVQAqAKoAKgD/ACoAAB8qAFUfKgCqHyoA/x8qAAA/ - KgBVPyoA8Pv/AKSgoACAgIAAAAD/AAD/AAAA//8A/wAAAAAAAAD//wAA////AP39/f39/f39/f39/f39 - /f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39 - /f39/f39/f39/f39/f39/f39/f39/f39qoYIqoYIhqoIqgiqCaoIqgiqhqqGhoYIhoYIqv39/f0I/f39 - /ar9/f39/YY2Ng4yDg4ODgoOCgoKCgqG/f39/Yb9/f39CP39/f39qjY7Ozs3Nzc3NjMSMjIOCqr9/f39 - qv39/f2G/f39/f0IN19fOzs3Nzc3NjcODg4KCP39/f0I/f39/ar9/f39/ao6X19fXzs7Ozc3NzY3NgqG - /f39/Yb9/f39CP39/f39hl9jY19jX187Ozs7Nzc3Dqr9/f39qv39/f2G/f39/f0IOodjh19jX19fXztf - OzcOCP39/f0ICAmqCAiqCKoICapfCYdjh2ODY19fXzs7Ow6q/f39/QhITEwoSCUoKSQoqmMJCYcJCWNj - Y2NfY19fNgj9/f39qkyZmZmYmJRwlCmqX19fXl9fX186WzY3Njc2gv39/f0JcJ2dmZmZlJmUJAmqCaoJ - hggIqggICKoIqggI/f39/YZwnp2dnZmZmJVMqnx8fHx8fFR8VHhUVFRUVKr9/f39CHChoZ2dnZ2ZmUwJ - fKSkxqSkxqSkpKSkpKBUCP39/f2qcKLDoqGdnZ2ZTKp8ysakxqSkxqSkxqSkpFSq/f39/QiUpqbDoqHE - nZ1Mq3ykqMakyqSkxqSkpKSkVAj9/f39hpTIyKbHoqGhoXAIfMrLpMqkxqSkxqTGpKRUqv39/f0IlMym - yKbIpcShcAh8y6jKpMqkxsqkpKSkxlQI/f39/aqUzMzMyKbIpqJwqnzLy8qpxsqkpMakxqSkeAj9/f39 - CJSUlJSUlJSUlJQJgMupy8qpysqkyqSkxqRUqv39/f2GCKoIqgiqCKoIhgigrcvPqcuoy8qkxsqkxnyG - /f39/ar9/f39/f39/f39qnzPz6nLy8uoyqnKpKTKVAj9/f39CP39/f39/f39/f0IfNDPz8+py8upyqjG - yqR8hv39/f2G/f39/f39/f39/Qik0K7P0M+ty8vLy6jKpXyq/f39/ar9/f39/f39/f39CHzQ09Ctz8/P - qcupy6jKeAj9/f39CP39/f39/f39/f2qoNPQ0NPQ0M/Qz8vLy6l8CP39/f2G/f39/f39/f39/QmkfKR8 - oHx8fHx8fHx8fHyG/f39/aoIqgiqCKoIqgiqCKoIqgiqCKoIqgiqCKoIqgj9/f39/f39/f39/f39/f39 - /f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f3///////////// - ///AAAAD3vgAA974AAPe+AAD3vgAA974AAPe+AADwAAAA8AAAAPAAAADwAAAA8AAAAPAAAADwAAAA8AA - AAPAAAADwAAAA8AAAAPAAAADwAAAA9/4AAPf+AAD3/gAA9/4AAPf+AAD3/gAA8AAAAP//////////ygA - AAAQAAAAIAAAAAEACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAACAAAAAgIAAgAAAAIAA - gACAgAAAgICAAMDcwADwyqYAqj8qAP8/KgAAXyoAVV8qAKpfKgD/XyoAAH8qAFV/KgCqfyoA/38qAACf - KgBVnyoAqp8qAP+fKgAAvyoAVb8qAKq/KgD/vyoAAN8qAFXfKgCq3yoA/98qAAD/KgBV/yoAqv8qAP// - KgAAAFUAVQBVAKoAVQD/AFUAAB9VAFUfVQCqH1UA/x9VAAA/VQBVP1UAqj9VAP8/VQAAX1UAVV9VAKpf - VQD/X1UAAH9VAFV/VQCqf1UA/39VAACfVQBVn1UAqp9VAP+fVQAAv1UAVb9VAKq/VQD/v1UAAN9VAFXf - VQCq31UA/99VAAD/VQBV/1UAqv9VAP//VQAAAH8AVQB/AKoAfwD/AH8AAB9/AFUffwCqH38A/x9/AAA/ - fwBVP38Aqj9/AP8/fwAAX38AVV9/AKpffwD/X38AAH9/AFV/fwCqf38A/39/AACffwBVn38Aqp9/AP+f - fwAAv38AVb9/AKq/fwD/v38AAN9/AFXffwCq338A/99/AAD/fwBV/38Aqv9/AP//fwAAAKoAVQCqAKoA - qgD/AKoAAB+qAFUfqgCqH6oA/x+qAAA/qgBVP6oAqj+qAP8/qgAAX6oAVV+qAKpfqgD/X6oAAH+qAFV/ - qgCqf6oA/3+qAACfqgBVn6oAqp+qAP+fqgAAv6oAVb+qAKq/qgD/v6oAAN+qAFXfqgCq36oA/9+qAAD/ - qgBV/6oAqv+qAP//qgAAANQAVQDUAKoA1AD/ANQAAB/UAFUf1ACqH9QA/x/UAAA/1ABVP9QAqj/UAP8/ - 1AAAX9QAVV/UAKpf1AD/X9QAAH/UAFV/1ACqf9QA/3/UAACf1ABVn9QAqp/UAP+f1AAAv9QAVb/UAKq/ - 1AD/v9QAAN/UAFXf1ACq39QA/9/UAAD/1ABV/9QAqv/UAP//1ABVAP8AqgD/AAAf/wBVH/8Aqh//AP8f - /wAAP/8AVT//AKo//wD/P/8AAF//AFVf/wCqX/8A/1//AAB//wBVf/8Aqn//AP9//wAAn/8AVZ//AKqf - /wD/n/8AAL//AFW//wCqv/8A/7//AADf/wBV3/8Aqt//AP/f/wBV//8Aqv//AP/MzAD/zP8A//8zAP// - ZgD//5kA///MAAB/AABVfwAAqn8AAP9/AAAAnwAAVZ8AAKqfAAD/nwAAAL8AAFW/AACqvwAA/78AAADf - AABV3wAAqt8AAP/fAABV/wAAqv8AAAAAKgBVACoAqgAqAP8AKgAAHyoAVR8qAKofKgD/HyoAAD8qAFU/ - KgDw+/8ApKCgAICAgAAAAP8AAP8AAAD//wD/AAAAAAAAAP//AAD///8A/f39/f39/f39/f39/f39/f0I - hgiqCKoICKoICKaGCP39qv39hv2GNg4ODjII/ar9/Yb9/ar9qjdjXzsOCP2G/f0IhquGCAleCWNfNob9 - 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 - bv+3d0n/wr/B/wAAAAAAAAAAAAAAAAAAAADCv8H/FRqE/0dN1v8/RNL/Nz3Q/y40zv8nLcz/ISfK/xwh - yf8WHMf/GxJh/8K/wf/gr3r/4K96/+Cvev/gr3r/3614/9yqdf/apnL/16Nw/9Sea//Rmmj/zZZk/8qR - X//GjFz/w4dW/7+CUv/Cv8H/AAAAAAAAAAAAAAAAAAAAAMK/wf8SHZD/WF3a/05U1/9FS9X/PUPS/zU7 - 0P8uM83/JyzL/yAmyf8aFGn/wr/B/8K/wf/Cv8H/wr/B/8K/wf/Cv8H/wr/B/8K/wf/Cv8H/wr/B/8K/ - wf/Cv8H/wr/B/8K/wf/Cv8H/wr/B/8K/wf8AAAAAAAAAAAAAAAAAAAAAwr/B/xAfnP9obt7/YGTc/1Zb - 2f9NU9f/RUrU/ztB0v80OdD/LDHO/xgWcv/Cv8H/Dn+n/w18pP8MeqH/DHie/wt1m/8Kc5j/CXGV/wlv - k/8JbJD/CGqN/wdpi/8HZ4j/BmWH/wZkhf8GYoP/wr/B/wAAAAAAAAAAAAAAAAAAAADCv8H/DiKp/3l+ - 4/9vdeH/Zmze/11i2/9UWtn/S1HW/0NI1P86P9H/Fhh9/8K/wf8Ogar/Barp/wGo6P8Apef/AKPm/wCi - 5P8An+L/AJ7h/wCd3/8AnN7/AJnc/wCY2/8AmNn/AJbX/wZjhP/Cv8H/AAAAAAAAAAAAAAAAAAAAAMK/ - wf8MJbX/iI7n/4CF5v93fOP/bnPg/2Vr3f9bYdv/UljY/0lP1v8UGoj/wr/B/w+Erf8Lrur/Bqvq/wOo - 6f8Apuf/AKTm/wCi5f8AoOT/AJ/i/wCd4f8AnN//AJrd/wCZ2/8AmNr/BmWH/8K/wf8AAAAAAAAAAAAA - AAAAAAAAwr/B/wkowP+WnOz/jpTq/4aL6P9+hOX/dXri/2xx4P9jaN3/WV/b/xEek//Cv8H/EIaw/xay - 7P8Or+z/Cavr/wWq6v8Bp+j/AKbn/wCj5f8AoeT/AJ/j/wCe4f8AnOD/AJve/wCa3f8HZ4n/wr/B/wAA - AAAAAAAAAAAAAAAAAADCv8H/CCrK/6Ko7/+coe7/lZrr/42T6f+Fiub/fIHl/3N54v9rcN//ECGg/8K/ - wf8QiLP/I7nu/xq07f8Ssez/C63r/war6v8Cqen/AKbo/wCk5v8AouX/AKHk/wCf4f8AneH/AJzf/who - i//Cv8H/AAAAAAAAAAAAAAAAAAAAAMK/wf8GLNP/q7Hy/6as8P+hpu//mp/u/5OY6/+LkOj/g4nm/3qA - 5P8NI6z/wr/B/xCKtv8xvvD/J7rv/x627f8Vsuz/Dq/s/wmr6/8Equn/Aafo/wCl5/8Ao+X/AKHk/wCf - 4v8AnuH/CGqO/8K/wf8AAAAAAAAAAAAAAAAAAAAAwr/B/wUu2/+vtPP/r7Tz/6qv8v+mq/D/oKXv/5me - 7f+Sl+v/io/p/wsmt//Cv8H/Eo24/0HF8f82wfD/LLzv/yK47v8atO3/EbHs/wut6/8Gq+r/A6np/wCm - 6P8Apeb/AKLl/wCh5P8IbJD/wr/B/wAAAAAAAAAAAAAAAAAAAADCv8H/BC/h/wQv3/8FL9z/BS3Z/wYt - 1v8GLNL/ByvP/wgqy/8IKcb/CSnC/8K/wf8Sjrv/Uszy/0fH8f87w/H/Mb7v/ye67/8et+7/FbPt/w6v - 6/8IrOv/BKnp/wGo6P8Apef/AKPl/wluk//Cv8H/AAAAAAAAAAAAAAAAAAAAAMK/wf/Cv8H/wr/B/8K/ - wf/Cv8H/wr/B/8K/wf/Cv8H/wr/B/8K/wf/Cv8H/wr/B/xKRvf9j0/P/WM/z/0zK8f9BxfH/N8Hw/yy8 - 7/8iuO7/GbTt/xGx7P8Lruv/Bqrq/wOo6f8Apuf/CnGV/8K/wf8AAAAAAAAAAAAAAAAAAAAAwr/B/wAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADCv8H/E5LA/3Ta8/9q1fP/XtHz/1LM - 8v9Hx/H/O8Pw/zG+7/8nu+//Hrbt/xay7f8Or+v/CKzq/wSq6f8Kc5j/wr/B/wAAAAAAAAAAAAAAAAAA - AADCv8H/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMK/wf8UlMH/hOD1/3rc - 9f9v2PP/ZNTy/1jO8v9NyvH/Qsbx/zbB8P8svO//I7ju/xm07f8SsOz/C67r/wt2m//Cv8H/AAAAAAAA - AAAAAAAAAAAAAMK/wf8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwr/B/xSW - w/+T5vb/iuL1/3/e9P912vT/adbz/13R8/9SzPL/R8jx/zzD8P8xvvD/J7rv/x627v8Vsuz/C3ie/8K/ - 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 - AADAwMD/GqTV/xqk1f8apNX/GaHR/xecy/8WmMb/FJK+/8DAwP8AAAAAAAAAAMDAwP/AwMD/wMDA/8DA - wP/AwMD/wMDA/8DAwP/AwMD/wMDA/8DAwP/AwMD/wMDA/8DAwP/AwMD/AAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//wAAgAEAALQF - wf+0BQAAgAUAAIAFAACAAQAAgAHB/4ABAACAAQAAgAEAALwBAAC8AQAAvAHB/4ABbP///5H/ - - - - - AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFBQUUBAQEYgQE - BHgEBARiBQUFFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEhISYJmZ - mb/k5OT/mZmZvxISEmAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYGBk0FBQVmBQUFZiUl - JXPy8vL/8vLy//Ly8v8lJSVzAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPDw9mAAAAAAAA - AAA7Oztcurq6vP39/f+6urq8Ozs7XAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGhoaZgAA - AAAAAAAASEhIEklJSVpJSUluSUlJWkhISBIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACgo - KGYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAA1NTVmAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADWWTcp2ls50NpbOf/aWznQ1lk3KQAA - AAAAAAAAQUFBZgAAAAAAAAAADg4OFAsLC2ELCwt3CwsLYQ4ODhQAAAAAuEkn0NSJcf/ov7L/1Ilx/7hJ - J9AAAAAAAAAAAExMTGYAAAAAAAAAACUlJV6fn5+95OTk/5+fn70lJSVeAAAAAK1IJv/wx7r/8Me6//DH - uv+tSCb/VFRUZlRUVGZUVFRmVFRUZlRUVGZFRUVv8vLy//Ly8v/y8vL/RUVFbwAAAADIZELQ456H//fO - wf/jnof/yGRC0AAAAAAAAAAAAAAAAAAAAAAAAAAAZWVlV8rKyrj9/f3/ysrKuGVlZVcAAAAA5oBeKemD - YdDpg2H/6YNh0OaAXikAAAAAAAAAAAAAAAAAAAAAAAAAAHp6ehF8fHxUfHx8Z3x8fFR6enoRAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - AAAAAAAA//8AAP//AAD//wAA/+MAAP/jAAD/4wAA//8AAP//AAD//wAAj/8AAAfjAAAH4wAAB+MAAI// - AAD//wAA//8AAA== - - - \ No newline at end of file diff --git a/Source/DependencyAnalyser/DependencyGraphPrintDocument.cs b/Source/DependencyAnalyser/DependencyGraphPrintDocument.cs deleted file mode 100644 index ab37b92..0000000 --- a/Source/DependencyAnalyser/DependencyGraphPrintDocument.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System.Drawing; -using System.Drawing.Printing; - -namespace DependencyAnalyser -{ - /// - /// Logic for printing dependency graphs. - /// - public class DependencyGraphPrintDocument : PrintDocument - { - private Image _dotImage; - - public DependencyGraphPrintDocument(Image dotImage) - { - _dotImage = dotImage; - } - -// protected override void OnBeginPrint(PrintEventArgs ev) -// { -// base.OnBeginPrint(ev) ; -// } - - protected override void OnPrintPage(PrintPageEventArgs args) - { - base.OnPrintPage(args); - - float leftMargin = args.MarginBounds.Left; - float topMargin = args.MarginBounds.Top; - float bottomMargin = args.MarginBounds.Bottom; - - float availableHeight = args.MarginBounds.Height; - float availableWidth = args.MarginBounds.Width; - - using (var font = new Font("Arial", 8)) - { - args.Graphics.DrawString("Created using .NET Dependency Analyser - http://drewnoakes.com/code/dependency-analyser/", font, Brushes.Black, leftMargin, bottomMargin - font.Height, new StringFormat()); - - // maximise image on page without distorting dimensions - args.Graphics.DrawImage(_dotImage, leftMargin, topMargin, availableWidth, availableHeight - font.Height); - } - - args.HasMorePages = false; - } - } -} diff --git a/Source/DependencyAnalyser/DependencyPlotter.cs b/Source/DependencyAnalyser/DependencyPlotter.cs deleted file mode 100644 index 4d848ee..0000000 --- a/Source/DependencyAnalyser/DependencyPlotter.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System.Drawing; -using WINGRAPHVIZLib; - -namespace DependencyAnalyser -{ - public static class DependencyPlotter - { - public static PlotResult CalculatePlot(double aspectRatio, DependencyGraph graph, FilterPreferences filterPreferences) - { - const double widthInches = 100; - var heightInches = (double)(int)(widthInches / aspectRatio); - - // node [color=lightblue2, style=filled]; - // page=""8.5,11"" - // size=""7.5, 10"" - // ratio=All - // widthInches = 75; - // heightInches = 100; - - var extraCommands = $"size=\"{widthInches},{heightInches}\"\r\n center=\"\"\r\n ratio=All\r\n node[width=.25,hight=.375,fontsize=12,color=lightblue2,style=filled]"; - var dotCommand = DotCommandBuilder.Generate(graph, filterPreferences, extraCommands); - - // a temp file to store image - var tempFile = TemporaryFileManager.CreateTemporaryFile(); - - // generate dot image - var dot = new DOTClass(); - dot.ToPNG(dotCommand).Save(tempFile); - var dotImage = Image.FromFile(tempFile); - - // generate SVG - var svgXml = dot.ToSvg(dotCommand); - - return new PlotResult(dotCommand, dotImage, svgXml); - } - } - - public class PlotResult - { - public string DotCommand { get; } - public Image Image { get; } - public string SvgXml { get; } - - public PlotResult(string dotCommand, Image image, string svgXml) - { - DotCommand = dotCommand; - Image = image; - SvgXml = svgXml; - } - } -} \ No newline at end of file diff --git a/Source/DependencyAnalyser/DotCommandBuilder.cs b/Source/DependencyAnalyser/DotCommandBuilder.cs deleted file mode 100644 index e22aec2..0000000 --- a/Source/DependencyAnalyser/DotCommandBuilder.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System.Collections.Generic; -using System.Text; - -namespace DependencyAnalyser -{ - /// - /// Builds a Dot command for a given dependency graph. - /// - public static class DotCommandBuilder - { - public static string Generate(DependencyGraph graph, FilterPreferences filterPreferences) - { - return Generate(graph, filterPreferences, string.Empty); - } - - public static string Generate(DependencyGraph graph, FilterPreferences filterPreferences, string extraCommands) - { - // TODO can this first loop be replaced with LINQ, maybe with a zip? - var idsByNameMap = new Dictionary(); - var id = 1; - foreach (var nodeName in graph.Nodes) - { - idsByNameMap.Add(nodeName, id); - id++; - } - - var commandText = new StringBuilder(); - commandText.Append("digraph G {\r\n"); - - // handle extra commands - if (extraCommands.Trim().Length > 0) - { - commandText.Append(" "); - commandText.Append(extraCommands.Trim()); - commandText.Append("\r\n"); - } - - var nodeLabels = new StringBuilder(); - - foreach (var dependant in graph.Nodes) - { - // make sure the dependant should be plotted - if (!filterPreferences.IncludeInPlot(dependant)) - continue; - - var dependantId = idsByNameMap[dependant]; - - // 1 [label="SampleProject",shape=circle,hight=0.12,width=0.12,fontsize=1]; - nodeLabels.AppendFormat(" {0} [label=\"{1}\"];\r\n", dependantId, dependant); - - foreach (var dependency in graph.GetDependenciesForNode(dependant)) - { - var dependencyId = idsByNameMap[dependency]; - if (!filterPreferences.IncludeInPlot(dependency)) - continue; - commandText.AppendFormat(" {0} -> {1};\r\n", dependantId, dependencyId); - } - } - - commandText.Append(nodeLabels); - commandText.Append("}"); - - return commandText.ToString(); - } - } -} \ No newline at end of file diff --git a/Source/DependencyAnalyser/InternalsVisibleTo.cs b/Source/DependencyAnalyser/InternalsVisibleTo.cs deleted file mode 100644 index d891f0b..0000000 --- a/Source/DependencyAnalyser/InternalsVisibleTo.cs +++ /dev/null @@ -1,3 +0,0 @@ -using System.Runtime.CompilerServices; - -[assembly: InternalsVisibleTo("DependencyAnalyser.Tests")] \ No newline at end of file diff --git a/Source/DependencyAnalyser/Program.cs b/Source/DependencyAnalyser/Program.cs deleted file mode 100644 index 72a9bfb..0000000 --- a/Source/DependencyAnalyser/Program.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using System.Windows.Forms; - -namespace DependencyAnalyser -{ - public static class Program - { - /// - /// The main entry point for the application. - /// - [STAThread] - private static void Main() - { - Application.EnableVisualStyles(); - Application.Run(new DependencyAnalyserForm()); - } - } -} diff --git a/Source/DependencyAnalyser/SolutionFileAnalyser.cs b/Source/DependencyAnalyser/SolutionFileAnalyser.cs deleted file mode 100644 index ab4261f..0000000 --- a/Source/DependencyAnalyser/SolutionFileAnalyser.cs +++ /dev/null @@ -1,53 +0,0 @@ -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 -{ - /// - /// Builds a DependencyGraph from a given Visual Studio solution (.sln) file. - /// - public static class SolutionFileAnalyser - { - public static async Task AnalyseAsync(string solutionPath, DependencyGraph graph, ILogger logger) - { - var instance = MSBuildLocator.QueryVisualStudioInstances().OrderByDescending(i => i.Version).First(); - - logger.WriteLine($"Located MSBuild at: {instance.MSBuildPath}"); - - MSBuildLocator.RegisterInstance(instance); - - using var workspace = MSBuildWorkspace.Create(); - - workspace.WorkspaceFailed += (o, e) => logger.WriteLine(e.Diagnostic.Message); - - logger.WriteLine($"Loading solution: {solutionPath}"); - - var solution = await workspace.OpenSolutionAsync(solutionPath); - - logger.WriteLine($"Finished loading solution: {solutionPath}"); - - var projectById = new Dictionary(); - - foreach (var project in solution.Projects) - { - projectById.Add(project.Id.Id, project); - } - - foreach (var project in solution.Projects) - { - foreach (var projectReference in project.ProjectReferences) - { - var referencedProject = projectById[projectReference.ProjectId.Id]; - - graph.AddDependency(project.Name, referencedProject.Name); - } - } - } - } -} \ No newline at end of file diff --git a/Source/DependencyAnalyser/TemporaryFileManager.cs b/Source/DependencyAnalyser/TemporaryFileManager.cs deleted file mode 100644 index cf26e3b..0000000 --- a/Source/DependencyAnalyser/TemporaryFileManager.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using System.IO; - -namespace DependencyAnalyser -{ - public static class TemporaryFileManager - { - private static readonly Random _random = new Random(); - - public static string CreateTemporaryFile() - { - var tempFilename = GenerateTemporaryFilename(); - while (File.Exists(tempFilename)) - tempFilename = GenerateTemporaryFilename(); - File.Create(tempFilename).Close(); - return tempFilename; - } - - private static string GenerateTemporaryFilename() - { - // TODO loop until file not found - return Path.Combine(Path.GetTempPath(), $"DependencyAnaylser.{_random.Next(int.MaxValue >> 4):X}.tmp"); - } - - public static void DeleteAllTemporaryFiles() - { - var filenames = GetExistingTemporaryFilenames(); - foreach (var filename in filenames) - { - try - { - File.Delete(filename); - } - catch - { - // file is probably locked - } - } - } - - public static string[] GetExistingTemporaryFilenames() - { - var fileInfoArray = new DirectoryInfo(Path.GetTempPath()).GetFiles("DependencyAnaylser.*.tmp"); - var filenames = new string[fileInfoArray.Length]; - for (var index = 0; index < fileInfoArray.Length; index++) - filenames[index] = fileInfoArray[index].FullName; - return filenames; - } - } -} \ No newline at end of file diff --git a/Source/Notes.txt b/Source/Notes.txt deleted file mode 100644 index 4f6b3ca..0000000 --- a/Source/Notes.txt +++ /dev/null @@ -1,34 +0,0 @@ -TODO: - -- compare references as dictated by compiler, versus those specified in projects/solution -- recent files list - -- analyser constructors to take filenames - -- support vs 8.0 sln files -- remember user's preferences for exclusion - -- support merging of multiple solution files -- allow multiple select in Open dialog -- allow printing of the image -- use colouring/shapes on graph - - indicate assemblies loaded from GAC, and those loaded from root directory - - colour for those with more/less/no dependencies/dependants -- reintroduce 'extra commands' to DotCommandBuilder -- progress bar -- show number of dependants/dependencies in exclusion list -- menu items to show/hide all System.* -- colour coding for nodes using wildcards (e.g. System.* = blue) - -DONE: - -- support merging additional .dll/.exe asseblies when started with a .exe or .dll -- fix 'save png' to save to temp file and copy (needed to close FileStream before calling GDI+ save method) -- use template for temp file names, so can clean up behind Wingraphviz (or dispose) -- tidy code for loading of assemblies -- extract class to create DependencyGraph (build graph in a single object instance) -- allow exclusion of selected assembly names, eg 'mscorlib' (maintain exclusion list in DotCommandBuilder) -- populate exclusion list from actual nodes in diagram -- sort exclusion list alphabetically -- set cursor to hourglass while calculating -- use DialogResult==OK instead of handling OK click event for open/save dialogs diff --git a/img/ui-filtered.png b/img/ui-filtered.png new file mode 100644 index 0000000..d18a2e1 Binary files /dev/null and b/img/ui-filtered.png differ diff --git a/img/ui-unfiltered.png b/img/ui-unfiltered.png new file mode 100644 index 0000000..f3bef00 Binary files /dev/null and b/img/ui-unfiltered.png differ