Browse Source

WPF-parity tree nodes, Field/Method/Property/Event glyphs

Brings the assembly tree closer to the WPF impl:

Assisted-by: Claude:claude-opus-4-7:Claude Code
pull/3755/head
Siegfried Pammer 2 months ago
parent
commit
28df83f9f6
  1. 12
      ILSpy/AssemblyTree/AssemblyListPane.axaml
  2. 9
      ILSpy/AssemblyTree/AssemblyListPane.axaml.cs
  3. 4
      ILSpy/AssemblyTree/AssemblyTreeModel.cs
  4. 93
      ILSpy/Assets/Icons/FindAssembly.svg
  5. 1
      ILSpy/Assets/Icons/Library.svg
  6. 73
      ILSpy/Assets/Icons/MetadataFile.svg
  7. BIN
      ILSpy/Assets/Icons/NuGet.png
  8. 1
      ILSpy/Assets/Icons/ProgramDebugDatabase.svg
  9. 8
      ILSpy/Assets/Icons/WebAssembly.svg
  10. 61
      ILSpy/Images.cs
  11. 152
      ILSpy/TreeNodes/AssemblyTreeNode.cs
  12. 18
      ILSpy/TreeNodes/EventTreeNode.cs
  13. 38
      ILSpy/TreeNodes/FieldTreeNode.cs
  14. 43
      ILSpy/TreeNodes/MethodTreeNode.cs
  15. 12
      ILSpy/TreeNodes/NamespaceTreeNode.cs
  16. 32
      ILSpy/TreeNodes/NaturalStringComparer.cs
  17. 38
      ILSpy/TreeNodes/PropertyTreeNode.cs
  18. 80
      ILSpy/TreeNodes/TypeTreeNode.cs

12
ILSpy/AssemblyTree/AssemblyListPane.axaml

@ -8,6 +8,8 @@ @@ -8,6 +8,8 @@
x:DataType="tree:AssemblyTreeModel">
<DataGrid Name="TreeGrid"
Classes="hierarchical"
AutoGenerateColumns="False"
HeadersVisibility="None"
HierarchicalRowsEnabled="True"
GridLinesVisibility="None"
@ -15,16 +17,18 @@ @@ -15,16 +17,18 @@
CanUserResizeColumns="False"
SelectionMode="Single">
<DataGrid.Columns>
<DataGridHierarchicalColumn Header="Name" Width="*">
<DataGridHierarchicalColumn Header="Name" Width="*"
x:CompileBindings="False"
Binding="{Binding Item}">
<DataGridHierarchicalColumn.CellTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" Spacing="4"
VerticalAlignment="Center" Margin="4,0"
ToolTip.Tip="{ReflectionBinding Item.ToolTip}">
<Image Source="{ReflectionBinding Item.Icon}"
ToolTip.Tip="{Binding ToolTip}">
<Image Source="{Binding Icon}"
Width="16" Height="16"
VerticalAlignment="Center" />
<TextBlock Text="{ReflectionBinding Item.Text}"
<TextBlock Text="{Binding Text}"
VerticalAlignment="Center" />
</StackPanel>
</DataTemplate>

9
ILSpy/AssemblyTree/AssemblyListPane.axaml.cs

@ -54,19 +54,12 @@ namespace ILSpy.AssemblyTree @@ -54,19 +54,12 @@ namespace ILSpy.AssemblyTree
void BindTree(SharpTreeNode root)
{
var options = new HierarchicalOptions<SharpTreeNode> {
// Force lazy children to load BEFORE returning the collection. ProDataGrid
// queries ChildrenSelector while expanding, before propagating IsExpanded to
// the source via IsExpandedSetter, so SharpTreeNode.LazyLoading wouldn't have
// triggered yet otherwise -- and an empty Children collection causes the grid
// to revert the expansion immediately.
ChildrenSelector = node => {
node.EnsureLazyChildren();
return node.Children;
},
IsLeafSelector = node => !node.ShowExpander,
IsExpandedSelector = node => node.IsExpanded,
IsExpandedSetter = (node, val) => node.IsExpanded = val,
AutoExpandRoot = true,
VirtualizeChildren = false,
};
var hierarchicalModel = new HierarchicalModel<SharpTreeNode>(options);

4
ILSpy/AssemblyTree/AssemblyTreeModel.cs

@ -102,9 +102,7 @@ namespace ILSpy.AssemblyTree @@ -102,9 +102,7 @@ namespace ILSpy.AssemblyTree
if (AssemblyList.GetAssemblies().Length == 0 && value == AssemblyListManager.DefaultListName)
LoadInitialAssemblies(AssemblyList);
var rootNode = new AssemblyListTreeNode(AssemblyList);
rootNode.IsExpanded = true;
Root = rootNode;
Root = new AssemblyListTreeNode(AssemblyList);
settingsService.SessionSettings.ActiveAssemblyList = value;
}

93
ILSpy/Assets/Icons/FindAssembly.svg

@ -0,0 +1,93 @@ @@ -0,0 +1,93 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
viewBox="0 0 16 16"
version="1.1"
id="svg12"
sodipodi:docname="FindAssembly.svg"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)">
<metadata
id="metadata18">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs16" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1017"
id="namedview14"
showgrid="false"
inkscape:zoom="41.7193"
inkscape:cx="10.353315"
inkscape:cy="7.8745385"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="svg12" />
<style
id="style2">.st0{opacity:0}.st0,.st1{fill:#f6f6f6}.st2{fill:#424242}</style>
<path
style="opacity:0;fill:#f6f6f6"
inkscape:connector-curvature="0"
id="path4"
d="M 0,0 H 16 V 16 H 0 Z"
class="st0" />
<path
style="fill:#f6f6f6"
inkscape:connector-curvature="0"
id="path6"
d="m 16,3 v 9 H 8 V 9 H 6 v 2 H 0 V 4 H 6 V 6 H 8 V 3 Z"
class="st1" />
<style
id="style20">.icon-canvas-transparent{opacity:0;fill:#f6f6f6}.icon-vs-out{fill:#f6f6f6}.icon-vs-bg{fill:#424242}.icon-vs-fg{fill:#f0eff1}</style>
<path
style="opacity:0;fill:#f6f6f6;stroke-width:0.56247044"
inkscape:connector-curvature="0"
class="icon-canvas-transparent"
d="M 16,15.999054 H 7 V 7 h 9 z"
id="canvas" />
<path
style="fill:#424242"
inkscape:connector-curvature="0"
id="path9"
d="m 1,5 h 4 v 5 H 1 Z M 9,4 h 6 v 7 H 9 Z M 6,7 H 8 V 8 H 6 Z"
class="st2" />
<path
style="opacity:1;fill:#f6f6f6;stroke-width:0.56247044"
d="M 12.71875 7 C 10.909188 7 9.4375 8.4719028 9.4375 10.28125 A 3.200625 3.2002886 0 0 0 9.8867188 11.917969 L 7.2460938 14.558594 A 0.8443125 0.84422376 0 0 0 7.2460938 15.751953 A 0.840375 0.84028667 0 0 0 8.4394531 15.751953 L 11.080078 13.111328 A 3.1995 3.1991637 0 0 0 12.71875 13.560547 A 3.285 3.2846547 0 0 0 16 10.28125 C 16 8.4724398 14.528313 7 12.71875 7 z M 12.71875 8.125 A 2.158875 2.1586481 0 0 1 14.875 10.28125 A 2.1583125 2.1580857 0 0 1 12.71875 12.4375 C 11.529626 12.4375 10.5625 11.469687 10.5625 10.28125 C 10.5625 9.0928121 11.529626 8.125 12.71875 8.125 z "
id="outline-6" />
<path
style="opacity:0.75;fill:#f0eff1;stroke-width:0.56247044"
inkscape:connector-curvature="0"
class="icon-vs-fg"
d="m 14.875,10.280718 a 2.1583125,2.1580857 0 0 1 -2.156063,2.155836 c -1.189124,0 -2.156062,-0.967399 -2.156062,-2.155836 0,-1.1884379 0.966938,-2.1558362 2.156062,-2.1558362 A 2.158875,2.1586481 0 0 1 14.875,10.280718 Z"
id="iconFg" />
<path
style="fill:#424242;stroke-width:0.56247044"
inkscape:connector-curvature="0"
class="icon-vs-bg"
d="M 12.718937,7.5624409 A 2.7225,2.7222139 0 0 0 9.9998125,10.280718 c 0,0.647932 0.2373755,1.235682 0.6170625,1.703071 l -2.97225,2.9725 a 0.28160528,0.28157568 0 0 0 0.39825,0.398208 l 2.97225,-2.9725 c 0.468,0.38021 1.055812,0.61756 1.703812,0.61756 1.499063,0 2.718563,-1.219372 2.718563,-2.718277 0,-1.4989049 -1.2195,-2.7188391 -2.718563,-2.7188391 z m 0,4.8746751 c -1.189124,0 -2.156062,-0.967398 -2.156062,-2.155836 0,-1.1884375 0.966938,-2.1563982 2.156062,-2.1563982 1.188563,0 2.156063,0.9673983 2.156063,2.1558362 0,1.188437 -0.9675,2.156398 -2.156063,2.156398 z"
id="iconBg" />
</svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

1
ILSpy/Assets/Icons/Library.svg

@ -0,0 +1 @@ @@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><style>.st0{opacity:0}.st0,.st1{fill:#f6f6f6}.st2{fill:#424242}.st3{fill:#f0eff1}</style><g id="outline"><path class="st0" d="M0 0h16v16H0z"/><path class="st1" d="M11 2V1H5v1H0v13h16V2z"/></g><g id="icon_x5F_bg"><path class="st2" d="M11 3v11h4V3h-4zm3 10h-2v-1h2v1zm0-2h-2v-1h2v1zm0-5h-2V4h2v2zM6 14h4V2H6v12zM7 3h2v1H7V3zm0 7h2v1H7v-1zm0 2h2v1H7v-1zM1 14h4V3H1v11zM2 4h2v2H2V4zm0 6h2v1H2v-1zm0 2h2v1H2v-1z"/></g><path class="st3" d="M14 13h-2v-1h2v1zm0-3h-2v1h2v-1zm0-6h-2v2h2V4zm-5 8H7v1h2v-1zm0-2H7v1h2v-1zm0-7H7v1h2V3zm-5 9H2v1h2v-1zm0-2H2v1h2v-1zm0-6H2v2h2V4z" id="icon_x5F_fg"/></svg>

After

Width:  |  Height:  |  Size: 650 B

73
ILSpy/Assets/Icons/MetadataFile.svg

@ -0,0 +1,73 @@ @@ -0,0 +1,73 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 16 16"
version="1.1"
id="svg4"
sodipodi:docname="MetadataFile.svg"
xml:space="preserve"
inkscape:version="1.3.1 (91b66b0783, 2023-11-16)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs4" /><sodipodi:namedview
id="namedview4"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="12.828125"
inkscape:cx="24.672351"
inkscape:cy="7.6004872"
inkscape:window-width="1920"
inkscape:window-height="1017"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="svg4" /><style
id="style1">.st0{opacity:0}.st0,.st1{fill:#f6f6f6}.st2{fill:#424242}.st3{fill:#f0eff1}</style><g
id="outline"
style="display:inline"><path
class="st0"
d="M 0,0 H 16 V 16 H 0 Z"
id="path1"
style="display:inline" /><path
class="st1"
d="M 10.023,1 H 3.964 C 3.012,1 2,1.701 2,3 V 8.556 C 0.81,9.25 0,10.525 0,12 c 0,2.206 1.794,4 4,4 a 3.971,3.971 0 0 0 2.618,-1 h 5.347 C 13.407,15 13.972,13.825 14,13 V 4.552 Z"
id="path2"
style="display:inline" /></g><g
id="icon_x5F_bg"><path
class="st2"
d="M 6,6 H 8 V 7 H 6 Z M 6,8 V 8.555 C 6.222,8.684 6.428,8.832 6.62,9 H 10 V 8 Z m 1.858,3 H 10 V 10 H 7.445 c 0.181,0.311 0.321,0.644 0.413,1 z"
id="path3"
style="display:inline" /><path
class="st2"
d="M 9.641,2 H 3.964 C 3.964,2 3,2 3,3 V 8.142 A 3.978,3.978 0 0 1 4,8 V 3 h 5 v 3 h 3 v 7 H 7.858 a 3.945,3.945 0 0 1 -0.414,1 h 4.521 C 12.965,14 13,13 13,13 V 5 Z"
id="path4"
style="display:inline" /></g><path
class="st3"
d="M9 6V3H4v5c.735 0 1.417.213 2.009.562L6 8.555V8h4v1H6.62c-.005-.005-.012-.008-.018-.013.344.298.633.649.86 1.047-.006-.011-.01-.023-.017-.034H10v1H7.858l-.005-.017c.086.328.147.663.147 1.017 0 .348-.059.679-.142 1H12V6H9zM8 7H6V6h2v1z"
id="icon_x5F_fg" /><style
id="style1-9">.st0{opacity:0}.st0,.st1{fill:#f6f6f6}.st2{fill:#424242}.st3{fill:#f0eff1}</style><style
type="text/css"
id="style1-2">.icon-canvas-transparent{opacity:0;fill:#F6F6F6;} .icon-vs-out{fill:#F6F6F6;} .icon-vs-fg{fill:#F0EFF1;} .icon-vs-bg{fill:#424242;}</style><g
id="g4"
transform="matrix(0.5,0,0,0.5,0,8)"
style="display:inline"><g
id="outline-9"><path
class="st0"
d="M 0,0 H 16 V 16 H 0 Z"
id="path1-3" /><path
class="st1"
d="m 15,4.465 v 7.07 L 8.303,16 H 7.697 L 1,11.535 V 4.465 L 7.697,0 h 0.605 z"
id="path2-8" /></g><path
class="st2"
d="M 8,1 2,5 v 6 l 6,4 6,-4 V 5 Z m 0,1.201 4.648,3.1 -1.892,1.261 -4.654,-3.095 z m -0.5,11.264 -4.5,-3 v -1.53 l 4.5,3 z m 0,-2.733 -4.5,-3 V 6.268 l 4.5,3 z M 3.353,5.301 5.202,4.069 9.855,7.163 8,8.398 Z m 9.647,5.164 -4.5,3 v -1.53 l 4.5,-3 z m 0,-2.733 -4.5,3 V 9.268 l 4.5,-3 z"
id="icon_x5F_bg-2" /><g
id="icon_x5F_fg-1"><path
class="st3"
d="m 8,2.201 4.648,3.1 -1.892,1.261 -4.654,-3.095 z m -0.5,11.264 -4.5,-3 v -1.53 l 4.5,3 z m 0,-2.733 -4.5,-3 V 6.268 l 4.5,3 z M 3.353,5.301 5.201,4.068 9.854,7.162 8,8.398 Z m 9.647,5.164 -4.5,3 v -1.53 l 4.5,-3 z m 0,-2.733 -4.5,3 V 9.268 l 4.5,-3 z"
id="path3-0" /></g></g></svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

BIN
ILSpy/Assets/Icons/NuGet.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

1
ILSpy/Assets/Icons/ProgramDebugDatabase.svg

@ -0,0 +1 @@ @@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><style>.st0{opacity:0}.st0,.st1{fill:#f6f6f6}.st2{fill:#424242}.st3{fill:#f0eff1}</style><g id="outline"><path class="st0" d="M0 0h16v16H0z"/><path class="st1" d="M10.023 1H3.964C3.012 1 2 1.701 2 3v5.556C.81 9.25 0 10.525 0 12c0 2.206 1.794 4 4 4a3.971 3.971 0 0 0 2.618-1h5.347c1.442 0 2.007-1.175 2.035-2V4.552L10.023 1z"/></g><g id="icon_x5F_bg"><path class="st2" d="M6 6h2v1H6zM6 8v.555c.222.129.428.277.62.445H10V8H6zM7.858 11H10v-1H7.445c.181.311.321.644.413 1z"/><circle class="st2" cx="4" cy="12" r="3"/><path class="st2" d="M9.641 2H3.964S3 2 3 3v5.142A3.978 3.978 0 0 1 4 8V3h5v3h3v7H7.858a3.945 3.945 0 0 1-.414 1h4.521c1 0 1.035-1 1.035-1V5L9.641 2z"/></g><path class="st3" d="M9 6V3H4v5c.735 0 1.417.213 2.009.562L6 8.555V8h4v1H6.62c-.005-.005-.012-.008-.018-.013.344.298.633.649.86 1.047-.006-.011-.01-.023-.017-.034H10v1H7.858l-.005-.017c.086.328.147.663.147 1.017 0 .348-.059.679-.142 1H12V6H9zM8 7H6V6h2v1z" id="icon_x5F_fg"/></svg>

After

Width:  |  Height:  |  Size: 1010 B

8
ILSpy/Assets/Icons/WebAssembly.svg

@ -0,0 +1,8 @@ @@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created by AtomCrusher for the English Wikipedia -->
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="612" height="612">
<!-- Block -->
<path d="m376 0c0 1.08 0 2.16 0 3.3 0 38.76-31.42 70.17-70.17 70.17-38.76 0-70.17-31.42-70.17-70.17l0 0c0-1.14 0-2.22 0-3.3L0 0l0 612 612 0 0-612z" fill="#654ff0"/>
<!-- Letters -->
<path d="m142.16 329.81 40.56 0 27.69 147.47 0.5 0 33.28-147.47 37.94 0 30.06 149.28 0.59 0 31.56-149.28 39.78 0-51.69 216.69-40.25 0-29.81-147.47-0.78 0-31.91 147.47-41 0zm287.69 0 63.94 0 63.5 216.69-41.84 0-13.81-48.22-72.84 0-10.66 48.22-40.75 0zm24.34 53.41-17.69 79.5 55.06 0-20.31-79.5z" fill="#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 709 B

61
ILSpy/Images.cs

@ -16,34 +16,61 @@ @@ -16,34 +16,61 @@
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
using Avalonia.Svg.Skia;
namespace ILSpy.Images
{
public static class Images
{
static IImage Load(string name)
const string AssetBase = "avares://ILSpy/Assets/Icons/";
static IImage LoadSvg(string name)
{
return new SvgImage {
Source = SvgSource.Load($"avares://ILSpy/Assets/Icons/{name}.svg", null)
Source = SvgSource.Load(AssetBase + name + ".svg", null)
};
}
public static readonly IImage Assembly = Load(nameof(Assembly));
public static readonly IImage AssemblyLoading = Load(nameof(AssemblyLoading));
public static readonly IImage AssemblyWarning = Load(nameof(AssemblyWarning));
public static readonly IImage Namespace = Load(nameof(Namespace));
public static readonly IImage Class = Load(nameof(Class));
public static readonly IImage Interface = Load(nameof(Interface));
public static readonly IImage Struct = Load(nameof(Struct));
public static readonly IImage Enum = Load(nameof(Enum));
public static readonly IImage Delegate = Load(nameof(Delegate));
public static readonly IImage Method = Load(nameof(Method));
public static readonly IImage Constructor = Load(nameof(Constructor));
public static readonly IImage Operator = Load(nameof(Operator));
public static readonly IImage Field = Load(nameof(Field));
public static readonly IImage Property = Load(nameof(Property));
public static readonly IImage Event = Load(nameof(Event));
static IImage LoadPng(string name)
{
using var stream = AssetLoader.Open(new Uri(AssetBase + name + ".png"));
return new Bitmap(stream);
}
// Assemblies / packages
public static readonly IImage Assembly = LoadSvg(nameof(Assembly));
public static readonly IImage AssemblyLoading = LoadSvg(nameof(AssemblyLoading));
public static readonly IImage AssemblyWarning = LoadSvg(nameof(AssemblyWarning));
public static readonly IImage FindAssembly = LoadSvg(nameof(FindAssembly));
public static readonly IImage Library = LoadSvg(nameof(Library));
public static readonly IImage NuGet = LoadPng(nameof(NuGet));
public static readonly IImage MetadataFile = LoadSvg(nameof(MetadataFile));
public static readonly IImage WebAssemblyFile = LoadSvg("WebAssembly");
public static readonly IImage ProgramDebugDatabase = LoadSvg(nameof(ProgramDebugDatabase));
// Containers
public static readonly IImage Namespace = LoadSvg(nameof(Namespace));
// Types
public static readonly IImage Class = LoadSvg(nameof(Class));
public static readonly IImage Interface = LoadSvg(nameof(Interface));
public static readonly IImage Struct = LoadSvg(nameof(Struct));
public static readonly IImage Enum = LoadSvg(nameof(Enum));
public static readonly IImage Delegate = LoadSvg(nameof(Delegate));
// Methods
public static readonly IImage Method = LoadSvg(nameof(Method));
public static readonly IImage Constructor = LoadSvg(nameof(Constructor));
public static readonly IImage Operator = LoadSvg(nameof(Operator));
// Fields / properties / events
public static readonly IImage Field = LoadSvg(nameof(Field));
public static readonly IImage Property = LoadSvg(nameof(Property));
public static readonly IImage Event = LoadSvg(nameof(Event));
}
}

152
ILSpy/TreeNodes/AssemblyTreeNode.cs

@ -1,3 +1,4 @@ @@ -1,3 +1,4 @@
// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
@ -21,19 +22,18 @@ using System.IO; @@ -21,19 +22,18 @@ using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Controls.Documents;
using Avalonia.Media;
using ICSharpCode.Decompiler.Metadata;
using ICSharpCode.ILSpyX;
using ILSpy.Images;
namespace ILSpy.TreeNodes
{
sealed class AssemblyTreeNode : ILSpyTreeNode
{
enum LoadState { Loading, Loaded, Failed }
readonly LoadedAssembly assembly;
LoadState loadState = LoadState.Loading;
string? loadError;
MetadataFile? cachedModule;
@ -44,42 +44,24 @@ namespace ILSpy.TreeNodes @@ -44,42 +44,24 @@ namespace ILSpy.TreeNodes
ArgumentNullException.ThrowIfNull(assembly);
this.assembly = assembly;
LazyLoading = true;
_ = StartLoadingAsync();
_ = InitAsync();
}
public override object Text => assembly.ShortName;
public override object Icon => loadState switch {
LoadState.Failed => Images.Images.AssemblyWarning,
LoadState.Loading => Images.Images.AssemblyLoading,
_ => Images.Images.Assembly,
};
public override object? ToolTip => loadState == LoadState.Failed ? loadError : assembly.FileName;
// Hide the chevron once a load has failed -- LoadChildren has nothing to show.
public override bool ShowExpander => loadState != LoadState.Failed && base.ShowExpander;
async Task StartLoadingAsync()
async Task InitAsync()
{
try
{
cachedModule = await assembly.GetMetadataFileOrNullAsync();
if (cachedModule == null)
var loadResult = await assembly.GetLoadResultAsync();
cachedModule = loadResult.MetadataFile;
if (cachedModule == null && loadResult.Package == null)
{
loadState = LoadState.Failed;
loadError = File.Exists(assembly.FileName)
? $"Failed to load '{assembly.FileName}'."
: $"File not found:\n{assembly.FileName}";
}
else
{
loadState = LoadState.Loaded;
}
}
catch (Exception ex)
{
loadState = LoadState.Failed;
loadError = $"Failed to load '{assembly.FileName}':\n{ex.GetBaseException().Message}";
}
RaisePropertyChanged(nameof(Icon));
@ -87,33 +69,107 @@ namespace ILSpy.TreeNodes @@ -87,33 +69,107 @@ namespace ILSpy.TreeNodes
RaisePropertyChanged(nameof(ShowExpander));
}
public override object Text => assembly.ShortName;
public override object Icon {
get {
if (assembly.HasLoadError || loadError != null)
return Images.Images.AssemblyWarning;
if (!assembly.IsLoaded)
return Images.Images.AssemblyLoading;
var loadResult = assembly.GetLoadResultAsync().GetAwaiter().GetResult();
if (loadResult.Package != null)
{
return loadResult.Package.Kind switch {
LoadedPackage.PackageKind.Zip => Images.Images.NuGet,
_ => Images.Images.Library,
};
}
if (loadResult.MetadataFile != null)
{
return loadResult.MetadataFile.Kind switch {
MetadataFile.MetadataFileKind.PortableExecutable => Images.Images.Assembly,
MetadataFile.MetadataFileKind.ProgramDebugDatabase => Images.Images.ProgramDebugDatabase,
MetadataFile.MetadataFileKind.WebCIL => Images.Images.WebAssemblyFile,
_ => Images.Images.MetadataFile,
};
}
return Images.Images.Assembly;
}
}
public override object? ToolTip {
get {
if (assembly.HasLoadError || loadError != null)
return loadError ?? "Assembly could not be loaded.";
if (!assembly.IsLoaded || cachedModule == null)
return assembly.FileName;
var tb = new TextBlock { TextWrapping = TextWrapping.Wrap };
var metadata = cachedModule.Metadata;
if (metadata?.IsAssembly == true && metadata.TryGetFullAssemblyName(out var assemblyName))
{
tb.Inlines!.Add(BoldRun("Name: "));
tb.Inlines.Add(new Run(assemblyName));
tb.Inlines.Add(new LineBreak());
}
tb.Inlines!.Add(BoldRun("Location: "));
tb.Inlines.Add(new Run(assembly.FileName));
if (cachedModule is PEFile peFile)
{
tb.Inlines.Add(new LineBreak());
tb.Inlines.Add(BoldRun("Architecture: "));
tb.Inlines.Add(new Run(GetPlatformDisplayName(peFile)));
}
string? runtime = GetRuntimeDisplayName(cachedModule);
if (runtime != null)
{
tb.Inlines.Add(new LineBreak());
tb.Inlines.Add(BoldRun("Runtime: "));
tb.Inlines.Add(new Run(runtime));
}
return tb;
}
}
public override bool ShowExpander => !assembly.HasLoadError && loadError == null && base.ShowExpander;
protected override void LoadChildren()
{
// If the user expands before the background load finishes, fall back to a sync wait.
// Once StartLoadingAsync runs, cachedModule / loadState are authoritative.
MetadataFile? module = cachedModule;
if (module == null && loadState == LoadState.Loading)
var module = cachedModule;
if (module == null)
{
try
{
module = assembly.GetMetadataFileOrNullAsync().Result;
module = assembly.GetLoadResultAsync().GetAwaiter().GetResult().MetadataFile;
}
catch
{
module = null;
return;
}
}
if (module == null)
return;
if (module.Kind != MetadataFile.MetadataFileKind.PortableExecutable
&& module.Kind != MetadataFile.MetadataFileKind.WebCIL)
{
return;
}
var metadata = module.Metadata;
var namespaces = metadata.TypeDefinitions
.Where(t => metadata.GetTypeDefinition(t).GetDeclaringType().IsNil)
.Select(t => metadata.GetString(metadata.GetTypeDefinition(t).Namespace))
.Where(ns => !string.IsNullOrEmpty(ns))
.Distinct()
.OrderBy(ns => ns);
.OrderBy(ns => ns, NaturalStringComparer.Instance);
foreach (var ns in namespaces)
Children.Add(new NamespaceTreeNode(ns, module));
@ -128,5 +184,29 @@ namespace ILSpy.TreeNodes @@ -128,5 +184,29 @@ namespace ILSpy.TreeNodes
foreach (var t in globalTypes)
Children.Add(new TypeTreeNode(t, module));
}
static Run BoldRun(string text) => new(text) { FontWeight = FontWeight.Bold };
static string GetPlatformDisplayName(PEFile file)
{
return file.Reader.PEHeaders.CoffHeader.Machine switch {
System.Reflection.PortableExecutable.Machine.I386 => "x86",
System.Reflection.PortableExecutable.Machine.Amd64 => "x64",
System.Reflection.PortableExecutable.Machine.IA64 => "Itanium",
System.Reflection.PortableExecutable.Machine.Arm => "ARM",
System.Reflection.PortableExecutable.Machine.Arm64 => "ARM64",
_ => file.Reader.PEHeaders.CoffHeader.Machine.ToString(),
};
}
static string? GetRuntimeDisplayName(MetadataFile module)
{
return module.Metadata.MetadataKind switch {
System.Reflection.Metadata.MetadataKind.Ecma335 => module.Metadata.MetadataVersion,
System.Reflection.Metadata.MetadataKind.WindowsMetadata => "WinRT",
System.Reflection.Metadata.MetadataKind.ManagedWindowsMetadata => "Managed WinRT",
_ => null,
};
}
}
}

18
ILSpy/TreeNodes/MemberTreeNode.cs → ILSpy/TreeNodes/EventTreeNode.cs

@ -16,23 +16,23 @@ @@ -16,23 +16,23 @@
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using Avalonia.Media;
using System;
using ICSharpCode.Decompiler.TypeSystem;
namespace ILSpy.TreeNodes
{
sealed class MemberTreeNode : ILSpyTreeNode
sealed class EventTreeNode : ILSpyTreeNode
{
readonly string name;
readonly IImage icon;
public IEvent EventDefinition { get; }
public MemberTreeNode(string name, IImage icon)
public EventTreeNode(IEvent ev)
{
this.name = name;
this.icon = icon;
EventDefinition = ev ?? throw new ArgumentNullException(nameof(ev));
}
public override object Text => name;
public override object Icon => icon;
public override object Text => EventDefinition.Name;
public override object Icon => Images.Images.Event;
public override bool ShowExpander => false;
}
}

38
ILSpy/TreeNodes/FieldTreeNode.cs

@ -0,0 +1,38 @@ @@ -0,0 +1,38 @@
// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
// to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
using ICSharpCode.Decompiler.TypeSystem;
namespace ILSpy.TreeNodes
{
sealed class FieldTreeNode : ILSpyTreeNode
{
public IField FieldDefinition { get; }
public FieldTreeNode(IField field)
{
FieldDefinition = field ?? throw new ArgumentNullException(nameof(field));
}
public override object Text => FieldDefinition.Name;
public override object Icon => Images.Images.Field;
public override bool ShowExpander => false;
}
}

43
ILSpy/TreeNodes/MethodTreeNode.cs

@ -0,0 +1,43 @@ @@ -0,0 +1,43 @@
// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
// to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
using ICSharpCode.Decompiler.TypeSystem;
namespace ILSpy.TreeNodes
{
sealed class MethodTreeNode : ILSpyTreeNode
{
public IMethod MethodDefinition { get; }
public MethodTreeNode(IMethod method)
{
MethodDefinition = method ?? throw new ArgumentNullException(nameof(method));
}
public override object Text => MethodDefinition.Name;
public override object Icon =>
MethodDefinition.IsConstructor ? Images.Images.Constructor :
MethodDefinition.IsOperator ? Images.Images.Operator :
Images.Images.Method;
public override bool ShowExpander => false;
}
}

12
ILSpy/TreeNodes/NamespaceTreeNode.cs

@ -16,13 +16,11 @@ @@ -16,13 +16,11 @@
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
using System.Linq;
using System.Reflection.Metadata;
using ICSharpCode.Decompiler.Metadata;
using ILSpy.Images;
namespace ILSpy.TreeNodes
{
sealed class NamespaceTreeNode : ILSpyTreeNode
@ -34,12 +32,12 @@ namespace ILSpy.TreeNodes @@ -34,12 +32,12 @@ namespace ILSpy.TreeNodes
public NamespaceTreeNode(string name, MetadataFile module)
{
this.name = name;
this.module = module;
this.name = name ?? throw new ArgumentNullException(nameof(name));
this.module = module ?? throw new ArgumentNullException(nameof(module));
LazyLoading = true;
}
public override object Text => name;
public override object Text => name.Length == 0 ? "-" : name;
public override object Icon => Images.Images.Namespace;
@ -52,7 +50,7 @@ namespace ILSpy.TreeNodes @@ -52,7 +50,7 @@ namespace ILSpy.TreeNodes
return td.GetDeclaringType().IsNil
&& metadata.GetString(td.Namespace) == name;
})
.OrderBy(t => metadata.GetString(metadata.GetTypeDefinition(t).Name));
.OrderBy(t => metadata.GetString(metadata.GetTypeDefinition(t).Name), NaturalStringComparer.Instance);
foreach (var t in types)
Children.Add(new TypeTreeNode(t, module));

32
ILSpy/TreeNodes/NaturalStringComparer.cs

@ -0,0 +1,32 @@ @@ -0,0 +1,32 @@
// Copyright (c) 2015 Siegfried Pammer
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
// to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System.Collections.Generic;
using System.Globalization;
namespace ILSpy.TreeNodes
{
/// <summary>
/// .NET natural string comparison.
/// </summary>
internal static class NaturalStringComparer
{
public static readonly IComparer<string> Instance =
System.StringComparer.Create(CultureInfo.CurrentCulture, CompareOptions.NumericOrdering);
}
}

38
ILSpy/TreeNodes/PropertyTreeNode.cs

@ -0,0 +1,38 @@ @@ -0,0 +1,38 @@
// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
// to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
using ICSharpCode.Decompiler.TypeSystem;
namespace ILSpy.TreeNodes
{
sealed class PropertyTreeNode : ILSpyTreeNode
{
public IProperty PropertyDefinition { get; }
public PropertyTreeNode(IProperty property)
{
PropertyDefinition = property ?? throw new ArgumentNullException(nameof(property));
}
public override object Text => PropertyDefinition.Name;
public override object Icon => Images.Images.Property;
public override bool ShowExpander => false;
}
}

80
ILSpy/TreeNodes/TypeTreeNode.cs

@ -16,13 +16,13 @@ @@ -16,13 +16,13 @@
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
using System.Linq;
using System.Reflection;
using System.Reflection.Metadata;
using ICSharpCode.Decompiler.Metadata;
using ILSpy.Images;
using ICSharpCode.Decompiler.TypeSystem;
using ICSharpCode.ILSpyX;
namespace ILSpy.TreeNodes
{
@ -37,7 +37,7 @@ namespace ILSpy.TreeNodes @@ -37,7 +37,7 @@ namespace ILSpy.TreeNodes
public TypeTreeNode(TypeDefinitionHandle handle, MetadataFile module)
{
this.handle = handle;
this.module = module;
this.module = module ?? throw new ArgumentNullException(nameof(module));
LazyLoading = true;
}
@ -50,51 +50,57 @@ namespace ILSpy.TreeNodes @@ -50,51 +50,57 @@ namespace ILSpy.TreeNodes
public override object Icon {
get {
var td = module.Metadata.GetTypeDefinition(handle);
var attrs = td.Attributes;
if ((attrs & TypeAttributes.ClassSemanticsMask) == TypeAttributes.Interface)
return Images.Images.Interface;
// crude detection: real distinction needs base-type lookups (System.Enum, ValueType,
// MulticastDelegate). Refining will land with the broader tree-node port.
return Images.Images.Class;
var typeDef = ResolveTypeDefinition();
return typeDef?.Kind switch {
TypeKind.Interface => Images.Images.Interface,
TypeKind.Struct or TypeKind.Void => Images.Images.Struct,
TypeKind.Delegate => Images.Images.Delegate,
TypeKind.Enum => Images.Images.Enum,
_ => Images.Images.Class,
};
}
}
protected override void LoadChildren()
public override bool CanExpandRecursively => true;
ITypeDefinition? ResolveTypeDefinition()
{
var td = module.Metadata.GetTypeDefinition(handle);
var typeSystem = module.GetTypeSystemOrNull();
if (typeSystem == null)
return null;
return ((MetadataModule)typeSystem.MainModule).GetDefinition(handle);
}
foreach (var nestedType in td.GetNestedTypes())
Children.Add(new TypeTreeNode(nestedType, module));
protected override void LoadChildren()
{
var typeDef = ResolveTypeDefinition();
if (typeDef == null)
return;
foreach (var method in td.GetMethods())
foreach (var nestedType in typeDef.NestedTypes
.OrderBy(t => t.Name, NaturalStringComparer.Instance))
{
var md = module.Metadata.GetMethodDefinition(method);
var name = module.Metadata.GetString(md.Name);
var icon = name == ".ctor" || name == ".cctor"
? Images.Images.Constructor
: ((md.Attributes & MethodAttributes.SpecialName) != 0 && name.StartsWith("op_"))
? Images.Images.Operator
: Images.Images.Method;
Children.Add(new MemberTreeNode(name, icon));
Children.Add(new TypeTreeNode((TypeDefinitionHandle)nestedType.MetadataToken, module));
}
foreach (var field in td.GetFields())
{
var fd = module.Metadata.GetFieldDefinition(field);
Children.Add(new MemberTreeNode(module.Metadata.GetString(fd.Name), Images.Images.Field));
}
// Enums look more useful in declaration order than alphabetical.
var fields = typeDef.Kind == TypeKind.Enum
? typeDef.Fields
: typeDef.Fields.OrderBy(f => f.Name, NaturalStringComparer.Instance);
foreach (var field in fields)
Children.Add(new FieldTreeNode(field));
foreach (var prop in td.GetProperties())
{
var pd = module.Metadata.GetPropertyDefinition(prop);
Children.Add(new MemberTreeNode(module.Metadata.GetString(pd.Name), Images.Images.Property));
}
foreach (var prop in typeDef.Properties.OrderBy(p => p.Name, NaturalStringComparer.Instance))
Children.Add(new PropertyTreeNode(prop));
foreach (var ev in typeDef.Events.OrderBy(e => e.Name, NaturalStringComparer.Instance))
Children.Add(new EventTreeNode(ev));
foreach (var evt in td.GetEvents())
foreach (var method in typeDef.Methods.OrderBy(m => m.Name, NaturalStringComparer.Instance))
{
var ed = module.Metadata.GetEventDefinition(evt);
Children.Add(new MemberTreeNode(module.Metadata.GetString(ed.Name), Images.Images.Event));
if (method.MetadataToken.IsNil || method.IsAccessor)
continue;
Children.Add(new MethodTreeNode(method));
}
}
}

Loading…
Cancel
Save