diff --git a/ICSharpCode.Decompiler.Tests/ProjectDecompiler/WpfProjectExportTests.cs b/ICSharpCode.Decompiler.Tests/ProjectDecompiler/WpfProjectExportTests.cs
new file mode 100644
index 000000000..c47af7f48
--- /dev/null
+++ b/ICSharpCode.Decompiler.Tests/ProjectDecompiler/WpfProjectExportTests.cs
@@ -0,0 +1,216 @@
+// Copyright (c) 2026 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;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading;
+
+using ICSharpCode.Decompiler.CSharp.ProjectDecompiler;
+using ICSharpCode.Decompiler.Metadata;
+using ICSharpCode.Decompiler.TypeSystem;
+
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.Text;
+
+using NUnit.Framework;
+
+namespace ICSharpCode.Decompiler.Tests.ProjectDecompiler;
+
+///
+/// Exporting a WPF assembly has to produce a project the .NET SDK actually builds. The inputs are
+/// compiled on the fly: what the export writes depends on the assembly references and the
+/// versioning attributes of the module, so the tests need real modules carrying them, not stubs.
+///
+[TestFixture]
+public sealed class WpfProjectExportTests
+{
+ readonly List openedModules = new();
+ string tempDirectory;
+ string presentationFramework;
+ string presentationCore;
+ int assemblyCounter;
+
+ [OneTimeSetUp]
+ public void SetUp()
+ {
+ tempDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
+ Directory.CreateDirectory(tempDirectory);
+
+ // Stand-ins for the two WPF assemblies the exporter recognizes by name. They only have to
+ // carry the right assembly name and the types the fixture assemblies derive from; the real
+ // ones are not available on non-Windows hosts, which is exactly the situation that made the
+ // export depend on the machine it ran on.
+ presentationFramework = CompileTo("PresentationFramework.dll", "PresentationFramework", """
+ namespace System.Windows
+ {
+ public class Application { }
+ public class Window { }
+ }
+ """);
+ presentationCore = CompileTo("PresentationCore.dll", "PresentationCore", """
+ namespace System.Windows.Media
+ {
+ public class Brush { }
+ }
+ """);
+ }
+
+ [OneTimeTearDown]
+ public void TearDown()
+ {
+ foreach (var module in openedModules)
+ module.Dispose();
+ if (Directory.Exists(tempDirectory))
+ Directory.Delete(tempDirectory, recursive: true);
+ }
+
+ ///
+ /// The markup compiler generates the program entry point into the application definition, so
+ /// the XAML file of the Application subclass is the one item that must not be a Page (#2253).
+ ///
+ [Test]
+ public void ApplicationSubclassOfAnExecutableIsTheApplicationDefinition()
+ {
+ var (module, typeSystem) = Load(WpfApplication(targetFramework: ".NETCoreApp,Version=v10.0", targetPlatform: "Windows7.0"));
+
+ using (Assert.EnterMultipleScope())
+ {
+ Assert.That(WholeProjectDecompiler.IsApplicationDefinition(FindType(typeSystem, "Fixture.App"), module), Is.True);
+ Assert.That(WholeProjectDecompiler.IsApplicationDefinition(FindType(typeSystem, "Fixture.MainWindow"), module), Is.False,
+ "a Window is a Page like any other");
+ }
+ }
+
+ ///
+ /// A library has no entry point for the markup compiler to generate a Main into; an Application
+ /// subclass shipped inside one stays a Page.
+ ///
+ [Test]
+ public void ApplicationSubclassOfALibraryIsNotTheApplicationDefinition()
+ {
+ var (module, typeSystem) = Load(Compile("WpfLibrary", """
+ namespace Fixture
+ {
+ public class App : System.Windows.Application { }
+ }
+ """, OutputKind.DynamicallyLinkedLibrary, presentationFramework));
+
+ Assert.That(WholeProjectDecompiler.IsApplicationDefinition(FindType(typeSystem, "Fixture.App"), module), Is.False);
+ }
+
+ static ITypeDefinition FindType(IDecompilerTypeSystem typeSystem, string fullTypeName)
+ {
+ var type = typeSystem.FindType(new FullTypeName(fullTypeName)).GetDefinition();
+ Assert.That(type, Is.Not.Null, $"the fixture assembly is expected to contain {fullTypeName}");
+ return type;
+ }
+
+ string WpfApplication(string targetFramework, string targetPlatform)
+ {
+ return Compile("WpfFixture", AssemblyAttributes(targetFramework, targetPlatform) + """
+ namespace Fixture
+ {
+ public class App : System.Windows.Application { }
+ public class MainWindow : System.Windows.Window
+ {
+ public System.Windows.Media.Brush Brush;
+ }
+ public static class Program
+ {
+ public static void Main() { }
+ }
+ }
+ """, OutputKind.WindowsApplication, presentationFramework, presentationCore);
+ }
+
+ static string AssemblyAttributes(string targetFramework, string targetPlatform, string supportedOSPlatform = null)
+ {
+ var attributes = new StringBuilder();
+ attributes.AppendLine($"[assembly: System.Runtime.Versioning.TargetFramework(\"{targetFramework}\")]");
+ if (targetPlatform != null)
+ attributes.AppendLine($"[assembly: System.Runtime.Versioning.TargetPlatform(\"{targetPlatform}\")]");
+ if (supportedOSPlatform != null)
+ attributes.AppendLine($"[assembly: System.Runtime.Versioning.SupportedOSPlatform(\"{supportedOSPlatform}\")]");
+ return attributes.ToString();
+ }
+
+ ///
+ /// Each compilation lands in a file of its own: the modules stay open until the fixture is torn
+ /// down, so the same path must not be written twice.
+ ///
+ string Compile(string assemblyName, string source, OutputKind outputKind = OutputKind.DynamicallyLinkedLibrary, params string[] references)
+ {
+ string extension = outputKind == OutputKind.DynamicallyLinkedLibrary ? ".dll" : ".exe";
+ string fileName = assemblyName + "." + Interlocked.Increment(ref assemblyCounter) + extension;
+ return CompileTo(fileName, assemblyName, source, outputKind, references);
+ }
+
+ string CompileTo(string fileName, string assemblyName, string source, OutputKind outputKind = OutputKind.DynamicallyLinkedLibrary, params string[] references)
+ {
+ var compilation = CSharpCompilation.Create(assemblyName,
+ new[] { CSharpSyntaxTree.ParseText(SourceText.From(source, Encoding.UTF8)) },
+ RuntimeReferences.Concat(references.Select(r => MetadataReference.CreateFromFile(r))),
+ new CSharpCompilationOptions(outputKind, deterministic: true));
+
+ string fullPath = Path.Combine(tempDirectory, fileName);
+ var result = compilation.Emit(fullPath);
+ Assert.That(result.Success, Is.True, () => string.Join(Environment.NewLine, result.Diagnostics));
+ return fullPath;
+ }
+
+ static IEnumerable RuntimeReferences
+ => ((string)AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES"))
+ .Split(Path.PathSeparator)
+ .Where(path => path.EndsWith(".dll", StringComparison.OrdinalIgnoreCase))
+ .Select(path => MetadataReference.CreateFromFile(path));
+
+ (MetadataFile Module, IDecompilerTypeSystem TypeSystem) Load(string fileName)
+ {
+ var module = OpenModule(fileName);
+ return (module, new DecompilerTypeSystem(module, new UniversalAssemblyResolver(fileName, throwOnError: false, targetFramework: null)));
+ }
+
+ MetadataFile OpenModule(string fileName)
+ {
+ var module = new PEFile(fileName);
+ openedModules.Add(module);
+ return module;
+ }
+
+ string WriteProjectFile(string fileName)
+ {
+ StringWriter output = new();
+ ProjectFileWriterSdkStyle.Default.Write(output, new TestProjectInfoProvider(tempDirectory), Array.Empty(), OpenModule(fileName));
+ return output.ToString();
+ }
+
+ sealed class TestProjectInfoProvider(string targetDirectory) : IProjectInfoProvider
+ {
+ public IAssemblyResolver AssemblyResolver { get; } = new UniversalAssemblyResolver(null, false, null);
+ public IAssemblyReferenceClassifier AssemblyReferenceClassifier { get; } = new AssemblyReferenceClassifier();
+ public CSharp.LanguageVersion LanguageVersion => CSharp.LanguageVersion.Latest;
+ public bool CheckForOverflowUnderflow => false;
+ public Guid ProjectGuid { get; } = Guid.NewGuid();
+ public string TargetDirectory => targetDirectory;
+ public string StrongNameKeyFile => null;
+ }
+}
diff --git a/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/WholeProjectDecompiler.cs b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/WholeProjectDecompiler.cs
index 3f5da58e5..5cb703218 100644
--- a/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/WholeProjectDecompiler.cs
+++ b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/WholeProjectDecompiler.cs
@@ -1177,6 +1177,27 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler
{
return TargetServices.DetectTargetFramework(module).Moniker != null;
}
+
+ ///
+ /// Determines whether the XAML file whose root object is belongs
+ /// into an MSBuild <ApplicationDefinition> item instead of a <Page> item.
+ /// The WPF markup compiler generates the program entry point from the application definition,
+ /// so a module that has no entry point of its own - a library that happens to contain an
+ /// Application subclass - must not get one.
+ ///
+ public static bool IsApplicationDefinition(ITypeDefinition? rootType, MetadataFile? module)
+ {
+ if (rootType == null)
+ return false;
+ if (module is not PEFile { Reader.PEHeaders.CorHeader.EntryPointTokenOrRelativeVirtualAddress: not 0 })
+ return false;
+ foreach (var baseType in rootType.GetNonInterfaceBaseTypes())
+ {
+ if (baseType.FullName == "System.Windows.Application")
+ return true;
+ }
+ return false;
+ }
}
public record struct ProjectItemInfo(string ItemType, string FileName)
diff --git a/ICSharpCode.ILSpyCmd/BamlAwareWholeProjectDecompiler.cs b/ICSharpCode.ILSpyCmd/BamlAwareWholeProjectDecompiler.cs
index c03334e09..4f450a35f 100644
--- a/ICSharpCode.ILSpyCmd/BamlAwareWholeProjectDecompiler.cs
+++ b/ICSharpCode.ILSpyCmd/BamlAwareWholeProjectDecompiler.cs
@@ -83,7 +83,10 @@ namespace ICSharpCode.ILSpyCmd
Directory.CreateDirectory(directory);
result.Xaml.Save(fullPath);
- var item = new ProjectItemInfo("Page", xamlFileName)
+ string itemType = IsApplicationDefinition(typeDefinition, bamlTypeSystem.MainModule.MetadataFile)
+ ? "ApplicationDefinition"
+ : "Page";
+ var item = new ProjectItemInfo(itemType, xamlFileName)
.With("Generator", "MSBuild:Compile")
.With("SubType", "Designer");
if (partialTypeInfo != null)
diff --git a/ILSpy/Languages/CSharpLanguage.cs b/ILSpy/Languages/CSharpLanguage.cs
index 10a46c120..5b77f9310 100644
--- a/ILSpy/Languages/CSharpLanguage.cs
+++ b/ILSpy/Languages/CSharpLanguage.cs
@@ -616,7 +616,7 @@ namespace ICSharpCode.ILSpy.Languages
continue;
entryStream.Position = 0;
fileName = handler.WriteResourceToFile(assembly, fileName, entryStream, context);
- var item = new ProjectItemInfo(handler.EntryType, fileName) { PartialTypes = context.PartialTypes };
+ var item = new ProjectItemInfo(context.EntryTypeOverride ?? handler.EntryType, fileName) { PartialTypes = context.PartialTypes };
item = item.With(context.AdditionalProperties);
return new[] { item };
}
diff --git a/ILSpy/Languages/IResourceFileHandler.cs b/ILSpy/Languages/IResourceFileHandler.cs
index 98ebddd1b..5937dc206 100644
--- a/ILSpy/Languages/IResourceFileHandler.cs
+++ b/ILSpy/Languages/IResourceFileHandler.cs
@@ -68,6 +68,13 @@ namespace ICSharpCode.ILSpy.Languages
public DecompilationOptions DecompilationOptions { get; }
+ ///
+ /// MSBuild item type for the entry currently being written, when it differs from the
+ /// handler's . XAML recovered from BAML is a
+ /// "Page", except for the file defining the application, which is an "ApplicationDefinition".
+ ///
+ public string? EntryTypeOverride { get; set; }
+
public ResourceFileHandlerContext(DecompilationOptions options)
{
this.DecompilationOptions = options;
diff --git a/ILSpy/TreeNodes/BamlResourceNodeFactory.cs b/ILSpy/TreeNodes/BamlResourceNodeFactory.cs
index 8d38f741d..def7ad3b9 100644
--- a/ILSpy/TreeNodes/BamlResourceNodeFactory.cs
+++ b/ILSpy/TreeNodes/BamlResourceNodeFactory.cs
@@ -92,6 +92,8 @@ namespace ICSharpCode.ILSpy.Baml
foreach (var member in result.GeneratedMembers)
partialTypeInfo.AddDeclaredMember(member);
context.AddPartialTypeInfo(partialTypeInfo);
+ if (WholeProjectDecompiler.IsApplicationDefinition(typeDefinition, typeSystem.MainModule.MetadataFile))
+ context.EntryTypeOverride = "ApplicationDefinition";
}
else
{