diff --git a/ICSharpCode.BamlDecompiler/Xaml/XamlType.cs b/ICSharpCode.BamlDecompiler/Xaml/XamlType.cs index a0f619408..09d287c18 100644 --- a/ICSharpCode.BamlDecompiler/Xaml/XamlType.cs +++ b/ICSharpCode.BamlDecompiler/Xaml/XamlType.cs @@ -69,7 +69,7 @@ namespace ICSharpCode.BamlDecompiler.Xaml xmlNs = ctx.XmlNs.LookupXmlns(FullAssemblyName, TypeNamespace); // Sometimes there's no reference to System.Xaml even if x:Type is used if (xmlNs == null) - xmlNs = ctx.TryGetXmlNamespace(Assembly, TypeNamespace); + xmlNs = XamlContext.TryGetXmlNamespace(Assembly, TypeNamespace, elem); if (xmlNs == null) { diff --git a/ICSharpCode.BamlDecompiler/XamlContext.cs b/ICSharpCode.BamlDecompiler/XamlContext.cs index f35108c40..f65c94cbb 100644 --- a/ICSharpCode.BamlDecompiler/XamlContext.cs +++ b/ICSharpCode.BamlDecompiler/XamlContext.cs @@ -192,7 +192,7 @@ namespace ICSharpCode.BamlDecompiler public const string KnownNamespace_Presentation = "http://schemas.microsoft.com/winfx/2006/xaml/presentation"; public const string KnownNamespace_PresentationOptions = "http://schemas.microsoft.com/winfx/2006/xaml/presentation/options"; - public string TryGetXmlNamespace(IModule assembly, string typeNamespace) + public static string TryGetXmlNamespace(IModule assembly, string typeNamespace, XElement context = null) { if (assembly == null) return null; @@ -214,12 +214,35 @@ namespace ICSharpCode.BamlDecompiler possibleXmlNs.Add(xmlNs); } + // An assembly may map one CLR namespace to several XML namespaces; PresentationFramework + // for example maps its namespaces to both the winfx/2006 and the netfx/2007 presentation + // namespace. Whenever the document itself declares one of the candidates, that one has to + // win: picking a different candidate for an element whose start tag carries the xmlns + // declaration redefines the prefix within that tag, which is not valid XML. + var declared = possibleXmlNs.Where(ns => IsDeclaredIn(context, ns)).ToList(); + if (declared.Count > 0) + possibleXmlNs = new HashSet(declared); + if (possibleXmlNs.Contains(KnownNamespace_Presentation)) return KnownNamespace_Presentation; return possibleXmlNs.FirstOrDefault(); } + static bool IsDeclaredIn(XElement context, string xmlNamespace) + { + for (var elem = context; elem != null; elem = elem.Parent) + { + foreach (var attr in elem.Attributes()) + { + if (attr.IsNamespaceDeclaration && attr.Value == xmlNamespace) + return true; + } + } + + return false; + } + public XName GetKnownNamespace(string name, string xmlNamespace, XElement context = null) { var xNs = GetXmlNamespace(xmlNamespace); diff --git a/ILSpy.BamlDecompiler.Tests.Windows/BamlTestRunner.cs b/ILSpy.BamlDecompiler.Tests.Windows/BamlTestRunner.cs index 951d3c5c6..65e7abc25 100644 --- a/ILSpy.BamlDecompiler.Tests.Windows/BamlTestRunner.cs +++ b/ILSpy.BamlDecompiler.Tests.Windows/BamlTestRunner.cs @@ -135,6 +135,12 @@ namespace ILSpy.BamlDecompiler.Tests RunTest("cases/issue1547"); } + [Test] + public void Issue1688() + { + RunTest("cases/issue1688"); + } + [Test] public void Issue2052() { diff --git a/ILSpy.BamlDecompiler.Tests.Windows/Cases/Issue1688.xaml b/ILSpy.BamlDecompiler.Tests.Windows/Cases/Issue1688.xaml new file mode 100644 index 000000000..27b972b5a --- /dev/null +++ b/ILSpy.BamlDecompiler.Tests.Windows/Cases/Issue1688.xaml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/ILSpy.BamlDecompiler.Tests.Windows/Cases/Issue1688.xaml.cs b/ILSpy.BamlDecompiler.Tests.Windows/Cases/Issue1688.xaml.cs new file mode 100644 index 000000000..0393a8681 --- /dev/null +++ b/ILSpy.BamlDecompiler.Tests.Windows/Cases/Issue1688.xaml.cs @@ -0,0 +1,42 @@ +// 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.Windows; +using System.Windows.Controls; + +namespace ILSpy.BamlDecompiler.Tests.Cases +{ + /// + /// Interaction logic for Issue1688.xaml + /// + public partial class Issue1688 : ContextMenu + { + public Issue1688() + { + InitializeComponent(); + } + + void Click_AssignPlace(object sender, RoutedEventArgs e) + { + } + + void Click_AssignMove(object sender, RoutedEventArgs e) + { + } + } +} diff --git a/ILSpy.BamlDecompiler.Tests.Windows/ILSpy.BamlDecompiler.Tests.Windows.csproj b/ILSpy.BamlDecompiler.Tests.Windows/ILSpy.BamlDecompiler.Tests.Windows.csproj index 1b7afc7dd..1fa27da82 100644 --- a/ILSpy.BamlDecompiler.Tests.Windows/ILSpy.BamlDecompiler.Tests.Windows.csproj +++ b/ILSpy.BamlDecompiler.Tests.Windows/ILSpy.BamlDecompiler.Tests.Windows.csproj @@ -66,6 +66,9 @@ + + Issue1688.xaml + @@ -102,6 +105,9 @@ MSBuild:Compile + + MSBuild:Compile + MSBuild:Compile diff --git a/ILSpy.BamlDecompiler.Tests/ILSpy.BamlDecompiler.Tests.csproj b/ILSpy.BamlDecompiler.Tests/ILSpy.BamlDecompiler.Tests.csproj index 92eb7131d..219ab9ba7 100644 --- a/ILSpy.BamlDecompiler.Tests/ILSpy.BamlDecompiler.Tests.csproj +++ b/ILSpy.BamlDecompiler.Tests/ILSpy.BamlDecompiler.Tests.csproj @@ -44,6 +44,7 @@ + diff --git a/ILSpy.BamlDecompiler.Tests/XmlNamespaceResolutionTests.cs b/ILSpy.BamlDecompiler.Tests/XmlNamespaceResolutionTests.cs new file mode 100644 index 000000000..dd43459f0 --- /dev/null +++ b/ILSpy.BamlDecompiler.Tests/XmlNamespaceResolutionTests.cs @@ -0,0 +1,115 @@ +// 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 System.Xml.Linq; + +using ICSharpCode.BamlDecompiler; +using ICSharpCode.Decompiler.Metadata; +using ICSharpCode.Decompiler.TypeSystem; + +using NUnit.Framework; + +using ILSpy.BamlDecompiler.Tests; + +// This assembly plays the role of an assembly that maps one CLR namespace to two XML namespaces, +// the way PresentationFramework maps its namespaces to both presentation namespaces. +[assembly: System.Windows.Markup.XmlnsDefinition(XmlNamespaceResolutionTests.Winfx2006Presentation, XmlNamespaceResolutionTests.TestClrNamespace)] +[assembly: System.Windows.Markup.XmlnsDefinition(XmlNamespaceResolutionTests.Netfx2007Presentation, XmlNamespaceResolutionTests.TestClrNamespace)] + +namespace System.Windows.Markup +{ + /// + /// Stand-in for the WPF attribute of the same name, which is unavailable on platforms without + /// WPF. The BAML decompiler matches it by full name in metadata, so the declaring assembly does + /// not matter. + /// + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + internal sealed class XmlnsDefinitionAttribute : Attribute + { + public XmlnsDefinitionAttribute(string xmlNamespace, string clrNamespace) + { + XmlNamespace = xmlNamespace; + ClrNamespace = clrNamespace; + } + + public string XmlNamespace { get; } + public string ClrNamespace { get; } + } +} + +namespace ILSpy.BamlDecompiler.Tests +{ + /// + /// Tests for the fallback used when neither the BAML xmlns records nor the PI mappings name an + /// XML namespace for a type: which of the assembly's XmlnsDefinition mappings is picked. + /// + [TestFixture] + public class XmlNamespaceResolutionTests + { + public const string Winfx2006Presentation = "http://schemas.microsoft.com/winfx/2006/xaml/presentation"; + public const string Netfx2007Presentation = "http://schemas.microsoft.com/netfx/2007/xaml/presentation"; + public const string TestClrNamespace = "ILSpy.BamlDecompiler.Tests"; + + static IModule GetTestAssemblyModule() + { + var location = typeof(XmlNamespaceResolutionTests).Assembly.Location; + using var stream = new FileStream(location, FileMode.Open, FileAccess.Read); + var file = new PEFile(location, stream, streamOptions: PEStreamOptions.PrefetchEntireImage); + var resolver = new UniversalAssemblyResolver(location, throwOnError: false, + file.DetectTargetFrameworkId(), file.DetectRuntimePack()); + return new BamlDecompilerTypeSystem(file, resolver).MainModule; + } + + [Test] + public void PrefersPresentationNamespace_WhenDocumentDeclaresNothing() + { + var xmlNs = XamlContext.TryGetXmlNamespace(GetTestAssemblyModule(), TestClrNamespace); + + Assert.That(xmlNs, Is.EqualTo(Winfx2006Presentation)); + } + + [Test] + public void PrefersNamespaceDeclaredByDocument_OverPresentationNamespace() + { + // Issue #1688: the document binds the default prefix to the netfx/2007 presentation + // namespace. Resolving its elements to the winfx/2006 one made the root start tag both + // declare and redefine the default prefix, which XmlWriter rejects. + var root = new XElement(XName.Get("Root", Netfx2007Presentation), + new XAttribute("xmlns", Netfx2007Presentation)); + + var xmlNs = XamlContext.TryGetXmlNamespace(GetTestAssemblyModule(), TestClrNamespace, root); + + Assert.That(xmlNs, Is.EqualTo(Netfx2007Presentation)); + } + + [Test] + public void PrefersNamespaceDeclaredByAncestor() + { + var child = new XElement(XName.Get("Child", Netfx2007Presentation)); + new XElement(XName.Get("Root", Netfx2007Presentation), + new XAttribute("xmlns", Netfx2007Presentation), child); + + var xmlNs = XamlContext.TryGetXmlNamespace(GetTestAssemblyModule(), TestClrNamespace, child); + + Assert.That(xmlNs, Is.EqualTo(Netfx2007Presentation)); + } + } +}