diff --git a/ICSharpCode.BamlDecompiler/Handlers/Records/PropertyHandler.cs b/ICSharpCode.BamlDecompiler/Handlers/Records/PropertyHandler.cs
index a469bdf35..214557049 100644
--- a/ICSharpCode.BamlDecompiler/Handlers/Records/PropertyHandler.cs
+++ b/ICSharpCode.BamlDecompiler/Handlers/Records/PropertyHandler.cs
@@ -49,11 +49,37 @@ namespace ICSharpCode.BamlDecompiler.Handlers
if (xamlProp.IsAttachedTo(elemType))
return new XAttribute(xamlProp.ToXName(ctx, parent.Xaml, true), value);
- if (xamlProp.PropertyName == "Name" && elemType.ResolvedType.GetDefinition()?.ParentModule.IsMainModule == true)
+ if (IsRuntimeNameOfElement(xamlProp, elemType))
return new XAttribute(ctx.GetKnownNamespace("Name", XamlContext.KnownNamespace_Xaml), value);
return new XAttribute(xamlProp.ToXName(ctx, parent.Xaml, false), value);
}
}
+
+ ///
+ /// Whether is the name of as
+ /// x:Name means it, so that the directive can be written instead of the property.
+ ///
+ /// x:Name is recorded as the runtime name property of the element, which is
+ /// FrameworkElement.Name for everything WPF - a property of the framework, not of the
+ /// assembly being decompiled. A type of that assembly declaring a property of its own called
+ /// "Name" is an ordinary property: writing the directive for it registers a name and leaves
+ /// the property unset, which still compiles and silently means something else (issue #2253).
+ ///
+ ///
+ internal static bool IsRuntimeNameOfElement(XamlProperty property, XamlType elementType)
+ {
+ if (property.PropertyName != "Name")
+ return false;
+ if (elementType?.ResolvedType.GetDefinition()?.ParentModule.IsMainModule != true)
+ return false;
+ // The type that declares the property, not the one the document names as the owner of
+ // the attribute: a control of the assembly being decompiled inherits Name from the
+ // framework, and the document names the control. Only a Name the type declares itself
+ // is a property of its own rather than the runtime name.
+ var declaringType = property.ResolvedMember?.DeclaringTypeDefinition
+ ?? property.DeclaringType?.ResolvedType?.GetDefinition();
+ return declaringType?.ParentModule.IsMainModule != true;
+ }
}
}
\ No newline at end of file
diff --git a/ICSharpCode.BamlDecompiler/Rewrite/StartupUriRewritePass.cs b/ICSharpCode.BamlDecompiler/Rewrite/StartupUriRewritePass.cs
new file mode 100644
index 000000000..af5dd1c87
--- /dev/null
+++ b/ICSharpCode.BamlDecompiler/Rewrite/StartupUriRewritePass.cs
@@ -0,0 +1,130 @@
+// 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.Linq;
+using System.Reflection.Metadata;
+using System.Reflection.Metadata.Ecma335;
+using System.Xml.Linq;
+
+using ICSharpCode.Decompiler.Disassembler;
+using ICSharpCode.Decompiler.TypeSystem;
+
+namespace ICSharpCode.BamlDecompiler.Rewrite
+{
+ ///
+ /// Recovers the StartupUri of an application from the code the markup compiler generated for it.
+ ///
+ /// StartupUri is written in App.xaml, but it does not reach the BAML: the markup compiler turns
+ /// the attribute into an assignment inside InitializeComponent. The project decompiler deletes
+ /// the generated members, so a document decompiled without this would build into an application
+ /// that starts and shows nothing.
+ ///
+ ///
+ internal class StartupUriRewritePass : IRewritePass
+ {
+ const string StartupUriPropertyName = "StartupUri";
+
+ public void Run(XamlContext ctx, XDocument document)
+ {
+ var root = document.Elements().FirstOrDefault()?.Elements().FirstOrDefault();
+ if (root == null || root.Attribute(StartupUriPropertyName) != null)
+ return;
+ // The type of the document, which the x:Class pass has recorded by the time this runs.
+ if (ctx.XClassNames.FirstOrDefault() is not string className)
+ return;
+ var typeDefinition = ctx.TypeSystem.MainModule.GetTypeDefinition(new FullTypeName(className).TopLevelTypeName);
+ if (typeDefinition == null)
+ return;
+
+ string startupUri = FindAssignedStartupUri(typeDefinition);
+ if (startupUri != null)
+ root.Add(new XAttribute(StartupUriPropertyName, startupUri));
+ }
+
+ ///
+ /// The string assigned to a StartupUri property in InitializeComponent, if there is one.
+ /// The generated code reads
+ /// StartupUri = new Uri("MainWindow.xaml", UriKind.Relative), so the string wanted is
+ /// the last one loaded before the call to the setter.
+ ///
+ static string FindAssignedStartupUri(ITypeDefinition typeDefinition)
+ {
+ var method = typeDefinition.Methods.FirstOrDefault(
+ m => m.Name == "InitializeComponent" && m.Parameters.Count == 0);
+ if (method?.MetadataToken.IsNil != false)
+ return null;
+ var module = typeDefinition.ParentModule?.MetadataFile;
+ if (module == null)
+ return null;
+
+ try
+ {
+ var metadata = module.Metadata;
+ var methodDefinition = metadata.GetMethodDefinition((MethodDefinitionHandle)method.MetadataToken);
+ if (methodDefinition.RelativeVirtualAddress == 0)
+ return null;
+ var body = module.GetMethodBody(methodDefinition.RelativeVirtualAddress);
+ var reader = body.GetILReader();
+ string lastLoadedString = null;
+ while (reader.RemainingBytes > 0)
+ {
+ var opCode = reader.DecodeOpCode();
+ switch (opCode)
+ {
+ case ILOpCode.Ldstr:
+ lastLoadedString = metadata.GetUserString(
+ MetadataTokens.UserStringHandle(reader.ReadInt32()));
+ break;
+ case ILOpCode.Call:
+ case ILOpCode.Callvirt:
+ var target = MetadataTokens.EntityHandle(reader.ReadInt32());
+ if (lastLoadedString != null && IsStartupUriSetter(metadata, target))
+ return lastLoadedString;
+ break;
+ default:
+ ILParser.SkipOperand(ref reader, opCode);
+ break;
+ }
+ }
+ }
+ catch (BadImageFormatException)
+ {
+ // A method body nobody can read says nothing about the StartupUri.
+ }
+ return null;
+ }
+
+ static bool IsStartupUriSetter(MetadataReader metadata, EntityHandle handle)
+ {
+ StringHandle name;
+ switch (handle.Kind)
+ {
+ case HandleKind.MethodDefinition:
+ name = metadata.GetMethodDefinition((MethodDefinitionHandle)handle).Name;
+ break;
+ case HandleKind.MemberReference:
+ name = metadata.GetMemberReference((MemberReferenceHandle)handle).Name;
+ break;
+ default:
+ return false;
+ }
+ return metadata.StringComparer.Equals(name, "set_" + StartupUriPropertyName);
+ }
+ }
+}
diff --git a/ICSharpCode.BamlDecompiler/XamlDecompiler.cs b/ICSharpCode.BamlDecompiler/XamlDecompiler.cs
index 7a1e842c2..d0f37ad09 100644
--- a/ICSharpCode.BamlDecompiler/XamlDecompiler.cs
+++ b/ICSharpCode.BamlDecompiler/XamlDecompiler.cs
@@ -40,6 +40,8 @@ namespace ICSharpCode.BamlDecompiler
{
static readonly IRewritePass[] rewritePasses = new IRewritePass[] {
new XClassRewritePass(),
+ // After the x:Class pass, which is what establishes the type of the document.
+ new StartupUriRewritePass(),
new MarkupExtensionRewritePass(),
new AttributeRewritePass(),
new ConnectionIdRewritePass(),
diff --git a/ILSpy.BamlDecompiler.Tests/ILSpy.BamlDecompiler.Tests.csproj b/ILSpy.BamlDecompiler.Tests/ILSpy.BamlDecompiler.Tests.csproj
index 949c7f36c..97dde68e0 100644
--- a/ILSpy.BamlDecompiler.Tests/ILSpy.BamlDecompiler.Tests.csproj
+++ b/ILSpy.BamlDecompiler.Tests/ILSpy.BamlDecompiler.Tests.csproj
@@ -47,6 +47,8 @@
+
+
diff --git a/ILSpy.BamlDecompiler.Tests/RuntimeNamePropertyTests.cs b/ILSpy.BamlDecompiler.Tests/RuntimeNamePropertyTests.cs
new file mode 100644
index 000000000..0ac542d4d
--- /dev/null
+++ b/ILSpy.BamlDecompiler.Tests/RuntimeNamePropertyTests.cs
@@ -0,0 +1,131 @@
+// 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.IO;
+using System.Linq;
+using System.Reflection;
+using System.Reflection.PortableExecutable;
+
+using ICSharpCode.BamlDecompiler;
+using ICSharpCode.BamlDecompiler.Handlers;
+using ICSharpCode.BamlDecompiler.Xaml;
+using ICSharpCode.Decompiler.Metadata;
+using ICSharpCode.Decompiler.TypeSystem;
+
+using NUnit.Framework;
+
+namespace ILSpy.BamlDecompiler.Tests
+{
+ ///
+ /// A type of the assembly being decompiled with a CLR property of its own called "Name".
+ ///
+ public class HelperWithItsOwnName
+ {
+ public string Name { get; set; }
+ }
+
+ ///
+ /// x:Name is the directive that registers a name for an element, and it is recorded as the
+ /// runtime name property of that element - FrameworkElement.Name for everything WPF, a property
+ /// of the framework rather than of the assembly being decompiled. Writing the directive for a
+ /// type's own property of the same name registers a name and leaves the property unset, which
+ /// still compiles and silently means something else (issue #2253).
+ ///
+ [TestFixture]
+ public class RuntimeNamePropertyTests
+ {
+ static ICompilation compilation;
+
+ [OneTimeSetUp]
+ public void LoadTestAssembly()
+ {
+ string location = typeof(RuntimeNamePropertyTests).Assembly.Location;
+ using var fileStream = new FileStream(location, FileMode.Open, FileAccess.Read);
+ var file = new PEFile(location, fileStream, streamOptions: PEStreamOptions.PrefetchEntireImage);
+ var resolver = new UniversalAssemblyResolver(location, throwOnError: false,
+ file.DetectTargetFrameworkId(), file.DetectRuntimePack());
+ compilation = new BamlDecompilerTypeSystem(file, resolver);
+ }
+
+ static XamlType XamlTypeOf(Type type)
+ {
+ var definition = compilation.FindType(type).GetDefinition();
+ return new XamlType(definition.ParentModule, definition.ParentModule.FullAssemblyName,
+ definition.Namespace, definition.Name) {
+ ResolvedType = definition
+ };
+ }
+
+ ///
+ /// A property of , the way the decompiler resolves one from
+ /// the type a BAML attribute record names as its owner.
+ ///
+ static XamlProperty PropertyOf(Type declaringType, string propertyName)
+ {
+ var declaring = XamlTypeOf(declaringType);
+ return new XamlProperty(declaring, propertyName) {
+ ResolvedMember = declaring.ResolvedType.GetDefinition()
+ .GetProperties(p => p.Name == propertyName).FirstOrDefault()
+ };
+ }
+
+ [Test]
+ public void ATypesOwnNamePropertyIsNotTheRuntimeName()
+ {
+ // : the property belongs to the assembly being
+ // decompiled, and the value has to reach it.
+ var property = PropertyOf(typeof(HelperWithItsOwnName), "Name");
+
+ Assert.That(PropertyHandler.IsRuntimeNameOfElement(property, XamlTypeOf(typeof(HelperWithItsOwnName))),
+ Is.False);
+ }
+
+ [Test]
+ public void ANameInheritedFromTheFrameworkIsTheRuntimeName()
+ {
+ // : the document names the control as the owner of
+ // the attribute, because that is the element it sits on, but the property comes from
+ // the framework type the control derives from.
+ var property = new XamlProperty(XamlTypeOf(typeof(HelperWithItsOwnName)), "Name") {
+ ResolvedMember = compilation.FindType(typeof(MemberInfo)).GetDefinition()
+ .GetProperties(p => p.Name == "Name").First()
+ };
+
+ Assert.That(PropertyHandler.IsRuntimeNameOfElement(property, XamlTypeOf(typeof(HelperWithItsOwnName))),
+ Is.True);
+ }
+
+ [Test]
+ public void AnElementFromOutsideTheAssemblyKeepsItsProperty()
+ {
+ var property = PropertyOf(typeof(Uri), "Name");
+
+ Assert.That(PropertyHandler.IsRuntimeNameOfElement(property, XamlTypeOf(typeof(Uri))), Is.False);
+ }
+
+ [Test]
+ public void APropertyOfAnotherNameIsNeverTheRuntimeName()
+ {
+ var property = PropertyOf(typeof(Uri), "Title");
+
+ Assert.That(PropertyHandler.IsRuntimeNameOfElement(property, XamlTypeOf(typeof(HelperWithItsOwnName))),
+ Is.False);
+ }
+ }
+}
diff --git a/ILSpy.BamlDecompiler.Tests/StartupUriTests.cs b/ILSpy.BamlDecompiler.Tests/StartupUriTests.cs
new file mode 100644
index 000000000..1e28c7b72
--- /dev/null
+++ b/ILSpy.BamlDecompiler.Tests/StartupUriTests.cs
@@ -0,0 +1,130 @@
+// 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.IO;
+using System.Reflection.PortableExecutable;
+
+using ICSharpCode.BamlDecompiler;
+using ICSharpCode.BamlDecompiler.Baml;
+using ICSharpCode.Decompiler.Metadata;
+
+using NUnit.Framework;
+
+namespace ILSpy.BamlDecompiler.Tests
+{
+ ///
+ /// StartupUri is written in App.xaml but compiled into App.g.cs, not into the BAML: the markup
+ /// compiler turns the attribute into an assignment inside InitializeComponent. The project
+ /// exporter deletes the generated members, so without recovering the assignment the exported
+ /// application builds and then opens no window (issue #2253).
+ ///
+ public class TestApplication
+ {
+ public Uri StartupUri { get; set; }
+ }
+
+ ///
+ /// What the markup compiler generates for an Application with a StartupUri.
+ ///
+ public class AppWithStartupUri : TestApplication
+ {
+ public void InitializeComponent()
+ {
+ StartupUri = new Uri("MainWindow.xaml", UriKind.Relative);
+ Uri resourceLocator = new Uri("/Demo;component/app.xaml", UriKind.Relative);
+ GC.KeepAlive(resourceLocator);
+ }
+ }
+
+ ///
+ /// The same without one, which must stay without one.
+ ///
+ public class AppWithoutStartupUri : TestApplication
+ {
+ public void InitializeComponent()
+ {
+ Uri resourceLocator = new Uri("/Demo;component/app.xaml", UriKind.Relative);
+ GC.KeepAlive(resourceLocator);
+ }
+ }
+
+ [TestFixture]
+ public class StartupUriTests
+ {
+ static MemoryStream CreateBaml(params BamlRecord[] records)
+ {
+ var version = new BamlDocument.BamlVersion { Major = 0, Minor = 0x60 };
+ var document = new BamlDocument {
+ Signature = "MSBAML",
+ ReaderVersion = version,
+ UpdaterVersion = version,
+ WriterVersion = version
+ };
+ document.Add(new DocumentStartRecord());
+ document.AddRange(records);
+ document.Add(new DocumentEndRecord());
+
+ var stream = new MemoryStream();
+ BamlWriter.WriteDocument(document, stream);
+ stream.Position = 0;
+ return stream;
+ }
+
+ static string Decompile(Stream baml)
+ {
+ var location = typeof(StartupUriTests).Assembly.Location;
+ using var fileStream = new FileStream(location, FileMode.Open, FileAccess.Read);
+ var file = new PEFile(location, fileStream, streamOptions: PEStreamOptions.PrefetchEntireImage);
+ var resolver = new UniversalAssemblyResolver(location, throwOnError: false,
+ file.DetectTargetFrameworkId(), file.DetectRuntimePack());
+ var decompiler = new XamlDecompiler(new BamlDecompilerTypeSystem(file, resolver),
+ new BamlDecompilerSettings());
+ return decompiler.Decompile(baml).Xaml.ToString();
+ }
+
+ ///
+ /// A document whose root is of this assembly, which is what
+ /// makes the BAML decompiler treat it as the code-behind class of the document.
+ ///
+ static string DecompileDocumentOf(string typeName)
+ {
+ return Decompile(CreateBaml(
+ new AssemblyInfoRecord { AssemblyId = 0, AssemblyFullName = "ILSpy.BamlDecompiler.Tests" },
+ new TypeInfoRecord { TypeId = 0, AssemblyId = 0, TypeFullName = "ILSpy.BamlDecompiler.Tests." + typeName },
+ new ElementStartRecord { TypeId = 0 },
+ new ElementEndRecord()));
+ }
+
+ [Test]
+ public void TheStartupUriOfTheGeneratedCodeComesBackAsAnAttribute()
+ {
+ string xaml = DecompileDocumentOf(nameof(AppWithStartupUri));
+
+ Assert.That(xaml, Does.Contain(@"StartupUri=""MainWindow.xaml"""), xaml);
+ }
+
+ [Test]
+ public void NoStartupUriIsInventedForADocumentThatHasNone()
+ {
+ string xaml = DecompileDocumentOf(nameof(AppWithoutStartupUri));
+
+ Assert.That(xaml, Does.Not.Contain("StartupUri=\""), xaml);
+ }
+ }
+}