From 5e37f38406fb6ac739d991be5a5d19c2a4321bfa Mon Sep 17 00:00:00 2001 From: Daniel Grunwald Date: Sun, 24 Apr 2011 16:14:51 +0200 Subject: [PATCH] Avoid using element syntax for content properties when doing so can lead to problems with the {Binding} markup extension. Closes #14. --- ILSpy/BamlDecompiler.cs | 250 ++++++++++++++++++++++++++++++++++------ 1 file changed, 217 insertions(+), 33 deletions(-) diff --git a/ILSpy/BamlDecompiler.cs b/ILSpy/BamlDecompiler.cs index 1394e878a..13adf6ddb 100644 --- a/ILSpy/BamlDecompiler.cs +++ b/ILSpy/BamlDecompiler.cs @@ -14,6 +14,9 @@ using System.Xml.Linq; using ICSharpCode.AvalonEdit.Highlighting; using ICSharpCode.ILSpy.TextView; using ICSharpCode.ILSpy.TreeNodes; +using System.Diagnostics; +using System.Collections; +using System.Collections.Generic; namespace ICSharpCode.ILSpy.Baml { @@ -24,47 +27,239 @@ namespace ICSharpCode.ILSpy.Baml { } - public string DecompileBaml(MemoryStream bamlCode, string containingAssemblyFile) + abstract class XamlNode { - bamlCode.Position = 0; - TextWriter w = new StringWriter(); + public readonly List Children = new List(); - Assembly assembly = Assembly.LoadFile(containingAssemblyFile); + public abstract void WriteTo(XamlWriter writer); + } + + [Conditional("DEBUG")] + static void Log(string format, params object[] args) + { + //Debug.WriteLine(format, args); + } + + sealed class XamlObjectNode : XamlNode + { + public readonly XamlType Type; - Baml2006Reader reader = new Baml2006Reader(bamlCode, new XamlReaderSettings() { ValuesMustBeString = true, LocalAssembly = assembly }); - XDocument doc = new XDocument(); - XamlXmlWriter writer = new XamlXmlWriter(doc.CreateWriter(), reader.SchemaContext); + public XamlObjectNode(XamlType type) + { + this.Type = type; + } + + public override void WriteTo(XamlWriter writer) + { + Log("StartObject {0}", this.Type); + writer.WriteStartObject(this.Type); + Debug.Indent(); + foreach (XamlNode node in this.Children) + node.WriteTo(writer); + Debug.Unindent(); + Log("EndObject"); + writer.WriteEndObject(); + } + } + + sealed class XamlGetObjectNode : XamlNode + { + public override void WriteTo(XamlWriter writer) + { + Log("GetObject"); + writer.WriteGetObject(); + Debug.Indent(); + foreach (XamlNode node in this.Children) + node.WriteTo(writer); + Debug.Unindent(); + Log("EndObject"); + writer.WriteEndObject(); + } + } + + sealed class XamlMemberNode : XamlNode + { + public XamlMember Member; + + public XamlMemberNode(XamlMember member) + { + this.Member = member; + } + + public override void WriteTo(XamlWriter writer) + { + Log("StartMember {0}", this.Member); + writer.WriteStartMember(this.Member); + Debug.Indent(); + foreach (XamlNode node in this.Children) + node.WriteTo(writer); + Debug.Unindent(); + Log("EndMember"); + writer.WriteEndMember(); + } + } + + sealed class XamlValueNode : XamlNode + { + public readonly object Value; + + public XamlValueNode(object value) + { + this.Value = value; + } + + public override void WriteTo(XamlWriter writer) + { + Log("Value {0}", this.Value); + Debug.Assert(this.Children.Count == 0); + // requires XamlReaderSettings.ValuesMustBeString = true to work properly + writer.WriteValue(this.Value); + } + } + + sealed class XamlNamespaceDeclarationNode : XamlNode + { + public readonly NamespaceDeclaration Namespace; + + public XamlNamespaceDeclarationNode(NamespaceDeclaration @namespace) + { + this.Namespace = @namespace; + } + + public override void WriteTo(XamlWriter writer) + { + Log("NamespaceDeclaration {0}", this.Namespace); + Debug.Assert(this.Children.Count == 0); + writer.WriteNamespace(this.Namespace); + } + } + + static List Parse(XamlReader reader) + { + List currentList = new List(); + Stack> stack = new Stack>(); while (reader.Read()) { switch (reader.NodeType) { case XamlNodeType.None: - break; case XamlNodeType.StartObject: - writer.WriteStartObject(reader.Type); + XamlObjectNode obj = new XamlObjectNode(reader.Type); + currentList.Add(obj); + stack.Push(currentList); + currentList = obj.Children; break; case XamlNodeType.GetObject: - writer.WriteGetObject(); - break; - case XamlNodeType.EndObject: - writer.WriteEndObject(); + XamlGetObjectNode getObject = new XamlGetObjectNode(); + currentList.Add(getObject); + stack.Push(currentList); + currentList = getObject.Children; break; case XamlNodeType.StartMember: - writer.WriteStartMember(reader.Member); - break; - case XamlNodeType.EndMember: - writer.WriteEndMember(); + XamlMemberNode member = new XamlMemberNode(reader.Member); + currentList.Add(member); + stack.Push(currentList); + currentList = member.Children; break; case XamlNodeType.Value: - // requires XamlReaderSettings.ValuesMustBeString = true to work properly - writer.WriteValue(reader.Value); + currentList.Add(new XamlValueNode(reader.Value)); break; case XamlNodeType.NamespaceDeclaration: - writer.WriteNamespace(reader.Namespace); + currentList.Add(new XamlNamespaceDeclarationNode(reader.Namespace)); + break; + case XamlNodeType.EndObject: + case XamlNodeType.EndMember: + currentList = stack.Pop(); break; default: - throw new Exception("Invalid value for XamlNodeType"); + throw new InvalidOperationException("Invalid value for XamlNodeType"); } } + if (stack.Count != 0) + throw new InvalidOperationException("Imbalanced stack"); + return currentList; + } + + void AvoidContentProperties(XamlNode node) + { + foreach (XamlNode child in node.Children) + AvoidContentProperties(child); + + + XamlObjectNode obj = node as XamlObjectNode; + if (obj != null) { + // Visit all except for the last child: + for (int i = 0; i < obj.Children.Count - 1; i++) { + // Avoids using content property syntax for simple string values, if the content property is not the last member. + // Without this, we cannot decompile <GridViewColumn Header="Culture" DisplayMemberBinding="{Binding Culture}" />, + // because the Header property is the content property, but there is no way to represent the Binding as an element. + XamlMemberNode memberNode = obj.Children[i] as XamlMemberNode; + if (memberNode != null && memberNode.Member == obj.Type.ContentProperty) { + if (memberNode.Children.Count == 1 && memberNode.Children[0] is XamlValueNode) { + // By creating a clone of the XamlMember, we prevent WPF from knowing that it's the content property. + XamlMember member = memberNode.Member; + memberNode.Member = new XamlMember(member.Name, member.DeclaringType, member.IsAttachable); + } + } + } + // We also need to avoid using content properties that have a markup extension as value, as the XamlXmlWriter would always expand those: + for (int i = 0; i < obj.Children.Count; i++) { + XamlMemberNode memberNode = obj.Children[i] as XamlMemberNode; + if (memberNode != null && memberNode.Member == obj.Type.ContentProperty && memberNode.Children.Count == 1) { + XamlObjectNode me = memberNode.Children[0] as XamlObjectNode; + if (me != null && me.Type.IsMarkupExtension) { + // By creating a clone of the XamlMember, we prevent WPF from knowing that it's the content property. + XamlMember member = memberNode.Member; + memberNode.Member = new XamlMember(member.Name, member.DeclaringType, member.IsAttachable); + } + } + } + } + } + + /// + /// It seems like BamlReader will always output 'x:Key' as last property. However, it must be specified as attribute in valid .xaml, so we move it to the front + /// of the attribute list. + /// + void MoveXKeyToFront(XamlNode node) + { + foreach (XamlNode child in node.Children) + MoveXKeyToFront(child); + + XamlObjectNode obj = node as XamlObjectNode; + if (obj != null && obj.Children.Count > 0) { + XamlMemberNode memberNode = obj.Children[obj.Children.Count - 1] as XamlMemberNode; + if (memberNode != null && memberNode.Member == XamlLanguage.Key) { + // move memberNode in front of the first member node: + for (int i = 0; i < obj.Children.Count; i++) { + if (obj.Children[i] is XamlMemberNode) { + obj.Children.Insert(i, memberNode); + obj.Children.RemoveAt(obj.Children.Count - 1); + break; + } + } + } + } + } + + public string DecompileBaml(MemoryStream bamlCode, string containingAssemblyFile) + { + bamlCode.Position = 0; + TextWriter w = new StringWriter(); + + Assembly assembly = Assembly.LoadFile(containingAssemblyFile); + + Baml2006Reader reader = new Baml2006Reader(bamlCode, new XamlReaderSettings() { ValuesMustBeString = true, LocalAssembly = assembly }); + var xamlDocument = Parse(reader); + + foreach (var xamlNode in xamlDocument) { + AvoidContentProperties(xamlNode); + MoveXKeyToFront(xamlNode); + } + + XDocument doc = new XDocument(); + XamlXmlWriter writer = new XamlXmlWriter(doc.CreateWriter(), reader.SchemaContext, new XamlXmlWriterSettings { AssumeValidInput = true }); + foreach (var xamlNode in xamlDocument) + xamlNode.WriteTo(writer); writer.Close(); // Fix namespace references @@ -78,18 +273,7 @@ namespace ICSharpCode.ILSpy.Baml } } } - // Convert x:Key into an attribute where possible - XName xKey = XName.Get("Key", "http://schemas.microsoft.com/winfx/2006/xaml"); - foreach (XElement e in doc.Descendants(xKey).ToList()) { - if (e.Nodes().Count() != 1) - continue; - XText text = e.Nodes().Single() as XText; - if (text != null) { - e.Parent.SetAttributeValue(xKey, text.Value); - e.Remove(); - } - } - + return doc.ToString(); }