Fix the parts of #2253 that still reproduce - #4108
Merged
Merged
Conversation
A WPF assembly keeps its windows and pages as BAML, so a project exported without converting them back is missing the parts that make it a WPF application - and the reader has no XAML to look at either. The CLI could do the conversion since --decompile-baml was added, but only if asked, which meant that everything the project exporter learned about WPF (Page and ApplicationDefinition items, resources, generated members removed from the code-behind) was invisible to anyone following issue #2253 from the command line. The flag is kept and ignored: it is documented and scripted against, and asking for what is now the default has to keep working. The test fixture is a real .g.resources container rather than a directly embedded .baml stream, because only entries inside a container reach WriteResourceToFile - a standalone .baml is copied out untouched, which is worth its own look. Assisted-by: Claude:claude-opus-5:Claude Code
Two of the defects reported on issue #2253 come from the decompiler writing text that means something else when it is read again. A markup extension is written as a single attribute value, and its grammar gives ',' '=' '{' '}' and the quote characters a meaning. Values went out unquoted, so an argument carrying any of them was read back as further name/value pairs: {DXBinding Expr='Price - Prev > 0 ? ...'}, the reported case, no longer compiles at all (MC3042, MC3045). Values without such a character stay unquoted, because quoting them would rewrite every document that never needed it. A clr-namespace declaration names the CLR namespace it maps, but nothing read that name out of it, so no lookup by namespace could match a declaration the document itself had made. Every type in such a namespace then got a second prefix declared on the element that used it. The assembly is the second half of the same lookup, and there the document records the name it was written against while a well-known type carries the assembly it resolves to now - "mscorlib" against "System.Private.CoreLib" - so the two are also accepted as the same when the recorded assembly forwards the type. Assisted-by: Claude:claude-opus-5:Claude Code
The project exporter wrote every XAML document to the project root under a fully-qualified name while the code-behind class went into a directory named after its namespace, so the two halves of one partial class ended up in different places. WPF tooling pairs MainWindow.xaml with MainWindow.xaml.cs by name and location; anything else is an unrelated file to it, and --nested-directories made the split wider still by moving only the C# half. Both now go through one function that decides where a type's files live, so the document lands where the type's own C# file would have, and the code-behind is named after the document. The BAML writers of the UI and of the command line had grown their own copies of the naming, which is how they came to disagree with the C# writer in the first place. Assisted-by: Claude:claude-opus-5:Claude Code
Metadata as attributes on an item element is MSBuild 15 syntax. The non-SDK project format is what an export falls back to for toolchains that predate the SDK, and those reject an unknown attribute on an item element, so a Page item carrying Generator and SubType as attributes undoes the reason to write that format at all. Every non-SDK project written by anything else keeps metadata in child elements. The SDK-style writer keeps attributes: there the syntax is a given and it is what the format's own tooling produces. Assisted-by: Claude:claude-opus-5:Claude Code
.NET ships a WindowsBase facade on every platform. It resolves under the name BAML means, so no synthetic stand-in was substituted for it, and it carries none of the WPF types, because those live in the WindowsDesktop runtime pack. System.Windows.Point and Size then resolved to nothing and the whole resource was lost with a NullReferenceException - ten of the BAML entries in one DevExpress theme assembly, on any machine without WPF, which is every Linux and macOS user and every CI run. A well-known assembly now counts as resolved only if it defines a type it is expected to have, so a facade gives way to the stand-in the way an assembly that does not resolve at all does. Nothing else changes: over 1158 documents of that assembly the output is identical, with the ten that used to be lost added back. Assisted-by: Claude:claude-opus-5:Claude Code
Two things #2253 costs an exported application, both found by decompiling one that was built from published source. StartupUri is written in App.xaml but never reaches the BAML: the markup compiler turns the attribute into an assignment inside InitializeComponent, and the project decompiler deletes the generated members. The exported application then builds and opens no window. The assignment is read back out of the generated method and written to the document root again. Where the compiler emits no app.baml at all - App.xaml carrying nothing but attributes - there is no document to write it on, and the export has no ApplicationDefinition either; that is a larger gap of its own. x:Name is recorded as the runtime name property of an element, which is FrameworkElement.Name for everything WPF - a property of the framework. Any property called "Name" on a type of the assembly being decompiled was written back as the directive, so <local:Helper Name="theName" /> came back as x:Name: the name gets registered and the property stays unset, which still compiles and quietly means something else. The directive is now written only for a name the element does not declare itself. Assisted-by: Claude:claude-opus-5:Claude Code
siegfriedpammer
force-pushed
the
fix/2253-baml-project-export
branch
from
September 4, 2026 17:42
d2df2a6 to
8747055
Compare
This was referenced Sep 20, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Works through the parts of #2253 that still reproduced on master, after every claim in the thread
was rebuilt and re-tested. Five of the twelve were already fixed, three are fixed here, one is
closed as not worth its cost, and one turns out not to reproduce. Verifying the result against a
real WPF application then turned up a defect nobody had reported.
-pconverts BAML on its ownA WPF assembly keeps its windows and pages as BAML, so a project exported without converting them
back is missing the parts that make it a WPF application. The CLI could do it since
--decompile-bamlwas added, but only if asked - which meant everything the exporter knows aboutWPF (Page and ApplicationDefinition items, resources, generated members removed from the
code-behind) was invisible to anyone following #2253 from the command line. The flag is kept and
ignored, because it is documented and scripted against.
A XAML parser can read the output back
Markup-extension values (claim 9) went out unquoted, so an argument carrying
,={}aquote or a backslash was read back as further name/value pairs. This is the DevExpress
{DXBinding Expr='...'}report, and it is the only claim of the twelve whose output does notcompile. Values without such a character stay unquoted, so documents that never needed quoting are
unchanged.
Namespace declarations (claim 11) were minted on the element that used them, for namespaces the
document had already declared at its root. Two causes: a
clr-namespace:declaration names the CLRnamespace it maps and nothing read that name out of the URI, so no lookup by namespace could match
the declaration the document made; and the document records the assembly it was written against
while a well-known type carries the assembly it resolves to now -
mscorlibagainstSystem.Private.CoreLib- so those are now accepted as the same when the recorded assemblyforwards the type.
A document and its code-behind share a path (claims 2 and 7)
The XAML went to the project root under a fully-qualified name while the partial class went into a
namespace directory, so the two halves of one class sat in different places and WPF tooling paired
neither;
--nested-directoriesmoved only the C# half, widening the split. Both now go through onefunction that decides where a type's files live. The BAML writers of the UI and of the command line
had each grown their own copy of the naming, which is how they came to disagree with the C# writer.
What an exported application was still missing
StartupUri is written in App.xaml but never reaches the BAML: the markup compiler turns the
attribute into an assignment inside
InitializeComponent, and the project decompiler deletes thegenerated members, so the exported application built and opened no window. The assignment is read
back out of the generated method and written to the document root again. Where the compiler emits
no
app.bamlat all - an App.xaml carrying nothing but attributes - there is no document to writeit on, and the export has no
ApplicationDefinitioneither; that is a larger gap of its own.A CLR property called
Nameon a type of the assembly being decompiled was written back as thex:Namedirective. x:Name is recorded as the runtime name property of an element, which isFrameworkElement.Namefor everything WPF - a property of the framework.<local:Helper Name="h" />therefore came back as
x:Name="h": the name gets registered, the property stays unset, and itstill compiles. The directive is now written only for a name the element does not declare itself.
Two things found while verifying
A facade standing in for WindowsBase cost every WPF assembly its geometry documents. .NET ships
a
WindowsBasefacade on every platform: it resolves under the name BAML means, so no syntheticstand-in was substituted, and it carries none of the WPF types because those live in the
WindowsDesktop runtime pack.
System.Windows.PointandSizethen resolved to nothing and thewhole resource was lost to a
NullReferenceException. Ten of the BAML entries in one DevExpresstheme assembly, on every machine without WPF - which is every Linux and macOS user and every CI
run. A well-known assembly now counts as resolved only if it defines a type it is expected to have.
Item metadata in the non-SDK project format was written as attributes, which is MSBuild 15
syntax; that format exists for the toolchains that predate it. It now writes child elements, the way
every non-SDK project keeps metadata. The SDK-style writer keeps attributes.
Verification
ILSpy.BamlDecompiler.Testsand 5 inICSharpCode.ILSpyCmd.Tests, each writtenfailing first. The quoting rule has nine cases; the xmlns and facade tests reproduce their
defects from BAML records, so they need no WPF and run everywhere.
output identical to master over all 1158 documents master could produce, plus the ten it used to
lose to the facade bug.
DevExpress.StockMarketTrader.Wpfand the components it builds against are on nuget.org, so itcan be built and its BAML compared against the XAML it came from:
{DXBinding Expr='ThemeIndex == 0', BackExpr='0'}came back from master asExpr=ThemeIndex == 0, BackExpr=0- two name/value pairs where the author wrote one - and comesback correct here. Its
App.xamlalso gets itsStartupUriback, byte-identical to the source.property of its own called
Name, and an App.xaml with a StartupUri. Both round-trip identicallyto the source now; before, one lost its property to the directive and the other lost the attribute
entirely.
ICSharpCode.Decompiler.Tests3599,ILSpy.Tests1262,ICSharpCode.ILSpyCmd.Tests47, nofailures.
Deliberately not done
XmlWriterSettings.NewLineOnAttributeswould turn this on inone line, but it applies to every element, so
<TextBlock Text="a" />becomes three lines too.Wrapping only long start tags means hand-writing the serializer -
XmlWritercloses the start tagas soon as whitespace is written between attributes - and a hand-written writer owns attribute
escaping, which is where this decompiler has had real bugs. Not worth it for a cosmetic
complaint.
Generator(claim 8). The reported replacement,XamlIntelliSenseFileGenerator, is wrong inboth project formats: real projects of either kind write
MSBuild:Compile.the application it was reported against, no element is lost; what is there is a declaring-type
shift (
UserControl.ResourcesbecomesFrameworkElement.Resources), which is legal and compiles.App.xamlthat was never compiled into BAML. When the markup compiler emitsno
app.baml, the export has no application definition to produce, StartupUri included. That is abigger piece of work than recovering an attribute onto a document that exists.
Prepared by an AI agent (Claude, claude-opus-5, via Claude Code) and reviewed by @siegfriedpammer.