diff --git a/Directory.Packages.props b/Directory.Packages.props
index 106d4c6a5..ad0401986 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -20,6 +20,7 @@
+
diff --git a/ILSpy/AssemblyTree/AssemblyListPane.axaml b/ILSpy/AssemblyTree/AssemblyListPane.axaml
index 2485e13a5..5dba352e9 100644
--- a/ILSpy/AssemblyTree/AssemblyListPane.axaml
+++ b/ILSpy/AssemblyTree/AssemblyListPane.axaml
@@ -15,14 +15,21 @@
CanUserResizeColumns="False"
SelectionMode="Single">
-
-
+
+
-
+
+
+
+
-
-
+
+
diff --git a/ILSpy/AssemblyTree/AssemblyListPane.axaml.cs b/ILSpy/AssemblyTree/AssemblyListPane.axaml.cs
index dfb425444..a937e5a9c 100644
--- a/ILSpy/AssemblyTree/AssemblyListPane.axaml.cs
+++ b/ILSpy/AssemblyTree/AssemblyListPane.axaml.cs
@@ -54,7 +54,15 @@ namespace ILSpy.AssemblyTree
void BindTree(SharpTreeNode root)
{
var options = new HierarchicalOptions {
- ChildrenSelector = node => node.Children,
+ // 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,
diff --git a/ILSpy/Assets/Icons/Assembly.svg b/ILSpy/Assets/Icons/Assembly.svg
new file mode 100644
index 000000000..f07e9ec99
--- /dev/null
+++ b/ILSpy/Assets/Icons/Assembly.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ILSpy/Assets/Icons/AssemblyWarning.svg b/ILSpy/Assets/Icons/AssemblyWarning.svg
new file mode 100644
index 000000000..1a7d200c1
--- /dev/null
+++ b/ILSpy/Assets/Icons/AssemblyWarning.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ILSpy/Assets/Icons/Class.svg b/ILSpy/Assets/Icons/Class.svg
new file mode 100644
index 000000000..e553c3633
--- /dev/null
+++ b/ILSpy/Assets/Icons/Class.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ILSpy/Assets/Icons/Constructor.svg b/ILSpy/Assets/Icons/Constructor.svg
new file mode 100644
index 000000000..3ca6aedac
--- /dev/null
+++ b/ILSpy/Assets/Icons/Constructor.svg
@@ -0,0 +1,70 @@
+
+
diff --git a/ILSpy/Assets/Icons/Delegate.svg b/ILSpy/Assets/Icons/Delegate.svg
new file mode 100644
index 000000000..1ba71123f
--- /dev/null
+++ b/ILSpy/Assets/Icons/Delegate.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ILSpy/Assets/Icons/Enum.svg b/ILSpy/Assets/Icons/Enum.svg
new file mode 100644
index 000000000..010d59d77
--- /dev/null
+++ b/ILSpy/Assets/Icons/Enum.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ILSpy/Assets/Icons/Event.svg b/ILSpy/Assets/Icons/Event.svg
new file mode 100644
index 000000000..e874ec217
--- /dev/null
+++ b/ILSpy/Assets/Icons/Event.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ILSpy/Assets/Icons/Field.svg b/ILSpy/Assets/Icons/Field.svg
new file mode 100644
index 000000000..e1b5aa5e3
--- /dev/null
+++ b/ILSpy/Assets/Icons/Field.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ILSpy/Assets/Icons/Interface.svg b/ILSpy/Assets/Icons/Interface.svg
new file mode 100644
index 000000000..0c08c8d50
--- /dev/null
+++ b/ILSpy/Assets/Icons/Interface.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ILSpy/Assets/Icons/Method.svg b/ILSpy/Assets/Icons/Method.svg
new file mode 100644
index 000000000..9706fa408
--- /dev/null
+++ b/ILSpy/Assets/Icons/Method.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ILSpy/Assets/Icons/Namespace.svg b/ILSpy/Assets/Icons/Namespace.svg
new file mode 100644
index 000000000..772b9152c
--- /dev/null
+++ b/ILSpy/Assets/Icons/Namespace.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ILSpy/Assets/Icons/Operator.svg b/ILSpy/Assets/Icons/Operator.svg
new file mode 100644
index 000000000..806381c45
--- /dev/null
+++ b/ILSpy/Assets/Icons/Operator.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ILSpy/Assets/Icons/Property.svg b/ILSpy/Assets/Icons/Property.svg
new file mode 100644
index 000000000..6b5b18a08
--- /dev/null
+++ b/ILSpy/Assets/Icons/Property.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ILSpy/Assets/Icons/Struct.svg b/ILSpy/Assets/Icons/Struct.svg
new file mode 100644
index 000000000..811f389f8
--- /dev/null
+++ b/ILSpy/Assets/Icons/Struct.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ILSpy/ILSpy.csproj b/ILSpy/ILSpy.csproj
index 105dbbcaf..aae364bc3 100644
--- a/ILSpy/ILSpy.csproj
+++ b/ILSpy/ILSpy.csproj
@@ -24,6 +24,7 @@
+ NoneAll
diff --git a/ILSpy/Images.cs b/ILSpy/Images.cs
new file mode 100644
index 000000000..0b4bc788a
--- /dev/null
+++ b/ILSpy/Images.cs
@@ -0,0 +1,48 @@
+// 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 Avalonia.Media;
+using Avalonia.Svg.Skia;
+
+namespace ILSpy.Images
+{
+ public static class Images
+ {
+ static IImage Load(string name)
+ {
+ return new SvgImage {
+ Source = SvgSource.Load($"avares://ILSpy/Assets/Icons/{name}.svg", null)
+ };
+ }
+
+ public static readonly IImage Assembly = Load(nameof(Assembly));
+ 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));
+ }
+}
diff --git a/ILSpy/TreeNodes/AssemblyTreeNode.cs b/ILSpy/TreeNodes/AssemblyTreeNode.cs
index c886dd264..aead80803 100644
--- a/ILSpy/TreeNodes/AssemblyTreeNode.cs
+++ b/ILSpy/TreeNodes/AssemblyTreeNode.cs
@@ -17,16 +17,21 @@
// DEALINGS IN THE SOFTWARE.
using System;
+using System.IO;
using System.Linq;
using ICSharpCode.Decompiler.Metadata;
using ICSharpCode.ILSpyX;
+using ILSpy.Images;
+
namespace ILSpy.TreeNodes
{
sealed class AssemblyTreeNode : ILSpyTreeNode
{
readonly LoadedAssembly assembly;
+ bool loadFailed;
+ string? loadError;
public LoadedAssembly LoadedAssembly => assembly;
@@ -39,11 +44,41 @@ namespace ILSpy.TreeNodes
public override object Text => assembly.ShortName;
+ public override object Icon => loadFailed ? Images.Images.AssemblyWarning : Images.Images.Assembly;
+
+ public override object? ToolTip => loadFailed ? loadError : assembly.FileName;
+
+ // When a load fails we still want the user to see the entry, but with the warning glyph
+ // and no expander chevron (LoadChildren produced nothing, ShowExpander becomes false).
+ public override bool ShowExpander => !loadFailed && base.ShowExpander;
+
protected override void LoadChildren()
{
- var result = assembly.GetMetadataFileOrNullAsync().Result;
- if (result is not MetadataFile module)
+ MetadataFile? module;
+ try
+ {
+ module = assembly.GetMetadataFileOrNullAsync().Result;
+ }
+ catch (Exception ex)
+ {
+ module = null;
+ loadError = $"Failed to load '{assembly.FileName}':\n{ex.GetBaseException().Message}";
+ }
+
+ if (module == null)
+ {
+ loadFailed = true;
+ if (loadError == null)
+ {
+ loadError = File.Exists(assembly.FileName)
+ ? $"Failed to load '{assembly.FileName}'."
+ : $"File not found:\n{assembly.FileName}";
+ }
+ RaisePropertyChanged(nameof(Icon));
+ RaisePropertyChanged(nameof(ShowExpander));
+ RaisePropertyChanged(nameof(ToolTip));
return;
+ }
var metadata = module.Metadata;
var namespaces = metadata.TypeDefinitions
diff --git a/ILSpy/TreeNodes/MemberTreeNode.cs b/ILSpy/TreeNodes/MemberTreeNode.cs
index 3fb69044f..13ed8d493 100644
--- a/ILSpy/TreeNodes/MemberTreeNode.cs
+++ b/ILSpy/TreeNodes/MemberTreeNode.cs
@@ -16,18 +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;
+
namespace ILSpy.TreeNodes
{
sealed class MemberTreeNode : ILSpyTreeNode
{
readonly string name;
+ readonly IImage icon;
- public MemberTreeNode(string name)
+ public MemberTreeNode(string name, IImage icon)
{
this.name = name;
+ this.icon = icon;
}
public override object Text => name;
+ public override object Icon => icon;
public override bool ShowExpander => false;
}
}
diff --git a/ILSpy/TreeNodes/NamespaceTreeNode.cs b/ILSpy/TreeNodes/NamespaceTreeNode.cs
index ea199d684..725e28ec6 100644
--- a/ILSpy/TreeNodes/NamespaceTreeNode.cs
+++ b/ILSpy/TreeNodes/NamespaceTreeNode.cs
@@ -21,6 +21,8 @@ using System.Reflection.Metadata;
using ICSharpCode.Decompiler.Metadata;
+using ILSpy.Images;
+
namespace ILSpy.TreeNodes
{
sealed class NamespaceTreeNode : ILSpyTreeNode
@@ -39,6 +41,8 @@ namespace ILSpy.TreeNodes
public override object Text => name;
+ public override object Icon => Images.Images.Namespace;
+
protected override void LoadChildren()
{
var metadata = module.Metadata;
diff --git a/ILSpy/TreeNodes/TypeTreeNode.cs b/ILSpy/TreeNodes/TypeTreeNode.cs
index e69f944fc..e7a840320 100644
--- a/ILSpy/TreeNodes/TypeTreeNode.cs
+++ b/ILSpy/TreeNodes/TypeTreeNode.cs
@@ -17,10 +17,13 @@
// DEALINGS IN THE SOFTWARE.
using System.Linq;
+using System.Reflection;
using System.Reflection.Metadata;
using ICSharpCode.Decompiler.Metadata;
+using ILSpy.Images;
+
namespace ILSpy.TreeNodes
{
sealed class TypeTreeNode : ILSpyTreeNode
@@ -45,6 +48,18 @@ 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;
+ }
+ }
+
protected override void LoadChildren()
{
var td = module.Metadata.GetTypeDefinition(handle);
@@ -55,25 +70,31 @@ namespace ILSpy.TreeNodes
foreach (var method in td.GetMethods())
{
var md = module.Metadata.GetMethodDefinition(method);
- Children.Add(new MemberTreeNode(module.Metadata.GetString(md.Name)));
+ 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));
}
foreach (var field in td.GetFields())
{
var fd = module.Metadata.GetFieldDefinition(field);
- Children.Add(new MemberTreeNode(module.Metadata.GetString(fd.Name)));
+ Children.Add(new MemberTreeNode(module.Metadata.GetString(fd.Name), Images.Images.Field));
}
foreach (var prop in td.GetProperties())
{
var pd = module.Metadata.GetPropertyDefinition(prop);
- Children.Add(new MemberTreeNode(module.Metadata.GetString(pd.Name)));
+ Children.Add(new MemberTreeNode(module.Metadata.GetString(pd.Name), Images.Images.Property));
}
foreach (var evt in td.GetEvents())
{
var ed = module.Metadata.GetEventDefinition(evt);
- Children.Add(new MemberTreeNode(module.Metadata.GetString(ed.Name)));
+ Children.Add(new MemberTreeNode(module.Metadata.GetString(ed.Name), Images.Images.Event));
}
}
}