//------------------------------------------------------------ // Copyright (c) Microsoft Corporation. All rights reserved. //------------------------------------------------------------ namespace Microsoft.Build.Tasks.Xaml { using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.Linq; using System.Xaml; using System.Xaml.Schema; using System.ComponentModel; using System.Runtime; using System.Windows.Markup; using XamlBuildTask; class ClassImporter { bool DefaultClassIsPublic; MemberVisibility DefaultFieldVisibility; string xamlFileName; string localAssemblyName; string rootNamespace; NamespaceTable namespaceTable; public ClassImporter(string xamlFileName, string localAssemblyName, string rootNamespace) { this.xamlFileName = xamlFileName; this.localAssemblyName = localAssemblyName; this.rootNamespace = rootNamespace; this.DefaultClassIsPublic = true; this.DefaultFieldVisibility = MemberVisibility.Assembly; this.namespaceTable = new NamespaceTable(localAssemblyName); } // Throws InvalidOperationException at DesignTime: Input XAML contains invalid constructs for generating a class. For example, unexpected content or unknown class or field modifiers. public ClassData ReadFromXaml(XamlNodeList nodes) { if (nodes == null) { throw FxTrace.Exception.ArgumentNull("nodeList"); } Stack currentTypes = new Stack(); XamlReader reader = nodes.GetReader(); XamlSchemaContext xsc = reader.SchemaContext; XamlNodeList strippedXamlNodes = new XamlNodeList(xsc); XamlWriter strippedXamlNodesWriter = strippedXamlNodes.Writer; ClassData result = new ClassData() { FileName = this.xamlFileName, IsPublic = this.DefaultClassIsPublic, RootNamespace = this.rootNamespace }; // We loop through the provided XAML; for each node, we do two things: // 1. If it's a directive that's relevant to x:Class, we extract the data. // 2. Unless it's a directive that's exclusively relevant to x:Class, we write it to strippedXamlNodes. // The result is two outputs: class data, and stripped XAML that can be used to initialize the // an instance of the class. bool readNextNode = false; while (readNextNode || reader.Read()) { bool stripNodeFromXaml = false; readNextNode = false; namespaceTable.ManageNamespace(reader); switch (reader.NodeType) { case XamlNodeType.StartObject: if (result.BaseType == null) { result.BaseType = reader.Type; } currentTypes.Push(new NamedObject() { Type = reader.Type, Visibility = DefaultFieldVisibility, }); break; case XamlNodeType.EndObject: currentTypes.Pop(); break; case XamlNodeType.StartMember: XamlMember member = reader.Member; if (member.IsDirective) { bool isRootElement = (currentTypes.Count == 1); stripNodeFromXaml = ProcessDirective(reader, result, currentTypes.Peek(), isRootElement, strippedXamlNodes, out readNextNode); } else { NamedObject currentType = currentTypes.Peek(); XamlType currentXamlType = currentType.Type; if (currentXamlType.IsUnknown) { result.RequiresCompilationPass2 = true; } } break; case XamlNodeType.EndMember: break; case XamlNodeType.Value: break; case XamlNodeType.NamespaceDeclaration: break; case XamlNodeType.None: break; case XamlNodeType.GetObject: //Push a dummy NamedObject so that it gets popped when you see the corresponding EndObject currentTypes.Push(new NamedObject()); break; default: Debug.Fail("Unrecognized XamlNodeType value" + reader.NodeType.ToString()); break; } if (!stripNodeFromXaml) { WritestrippedXamlNode(reader, strippedXamlNodesWriter); } } // ClassData.Name should be initialized to a non-null non-empty value if // the file contains x:Class. Throw an error if neither is found. if (result.Name == null) { string xClassDirectiveName = "{" + XamlLanguage.Class.PreferredXamlNamespace + "}" + XamlLanguage.Class.Name; throw FxTrace.Exception.AsError(LogInvalidOperationException(null, SR.TaskCannotProcessFileWithoutType(xClassDirectiveName))); } strippedXamlNodes.Writer.Close(); strippedXamlNodes = RewriteRootNode(strippedXamlNodes, result.Name, result.Namespace); result.EmbeddedResourceXaml = strippedXamlNodes; return result; } IList UpdateTypeArgs(IList typeArgs, XamlSchemaContext xsc) { if (typeArgs != null) { IList updatedTypeArgs = new List(); foreach (var typeArg in typeArgs) { IList typeArgTypeArgs = UpdateTypeArgs(typeArg.TypeArguments, xsc); string typeArgXmlns = XamlBuildTaskServices.UpdateClrNamespaceUriWithLocalAssembly(typeArg.PreferredXamlNamespace, this.localAssemblyName); updatedTypeArgs.Add(new XamlType(typeArgXmlns, typeArg.Name, typeArgTypeArgs, xsc)); } return updatedTypeArgs; } return typeArgs; } void WritestrippedXamlNode(XamlReader reader, XamlWriter writer) { switch (reader.NodeType) { case XamlNodeType.StartObject: XamlType xamlType = reader.Type; if (xamlType.IsUnknown) { IList typeArgs = UpdateTypeArgs(xamlType.TypeArguments, reader.SchemaContext); string xmlns = XamlBuildTaskServices.UpdateClrNamespaceUriWithLocalAssembly(xamlType.PreferredXamlNamespace, this.localAssemblyName); xamlType = new XamlType(xmlns, xamlType.Name, typeArgs, reader.SchemaContext); } writer.WriteStartObject(xamlType); break; case XamlNodeType.StartMember: XamlMember member = reader.Member; if (member.IsUnknown && !member.IsDirective) { string xmlns = XamlBuildTaskServices.UpdateClrNamespaceUriWithLocalAssembly(member.DeclaringType.PreferredXamlNamespace, this.localAssemblyName); XamlType memberXamlType = new XamlType(xmlns, member.DeclaringType.Name, member.DeclaringType.TypeArguments, reader.SchemaContext); member = new XamlMember(member.Name, memberXamlType, member.IsAttachable); } writer.WriteStartMember(member); break; case XamlNodeType.NamespaceDeclaration: NamespaceDeclaration ns = new NamespaceDeclaration( XamlBuildTaskServices.UpdateClrNamespaceUriWithLocalAssembly(reader.Namespace.Namespace, this.localAssemblyName), reader.Namespace.Prefix); writer.WriteNamespace(ns); break; case XamlNodeType.GetObject: case XamlNodeType.EndObject: case XamlNodeType.EndMember: case XamlNodeType.Value: case XamlNodeType.None: writer.WriteNode(reader); break; default: Debug.Fail("Unrecognized XamlNodeType value" + reader.NodeType.ToString()); break; } } XamlNodeList RewriteRootNode(XamlNodeList strippedXamlNodes, string name, string @namespace) { // Rewrite the root node to have the name of class declared via x:Class (rather than the base class) // Also, for any properties on the root object that are declared in this class, need to rewrite the // namespace to include the root namespace, if there is one. string oldNamespace = null; if (!string.IsNullOrEmpty(this.rootNamespace)) { oldNamespace = @namespace; if (!string.IsNullOrEmpty(@namespace)) { @namespace = this.rootNamespace + "." + @namespace; } else { @namespace = this.rootNamespace; } } string namespaceName = string.Format(CultureInfo.InvariantCulture, "{0}{1};{2}{3}", XamlBuildTaskServices.ClrNamespaceUriNamespacePart, @namespace, XamlBuildTaskServices.ClrNamespaceUriAssemblyPart, this.localAssemblyName); XamlReader reader = strippedXamlNodes.GetReader(); XamlSchemaContext xsc = reader.SchemaContext; XamlNodeList newStrippedXamlNodes = new XamlNodeList(xsc); XamlWriter writer = newStrippedXamlNodes.Writer; int depth = 0; XamlType rootXamlType = null; while (reader.Read()) { switch (reader.NodeType) { case XamlNodeType.StartObject: case XamlNodeType.GetObject: depth++; break; case XamlNodeType.EndObject: depth--; break; } if (reader.NodeType == XamlNodeType.StartObject && depth == 1) { rootXamlType = new XamlType(namespaceName, name, null, xsc); writer.WriteStartObject(rootXamlType); } else if (reader.NodeType == XamlNodeType.StartMember && depth == 1 && reader.Member.IsUnknown && reader.Member.DeclaringType != null && reader.Member.DeclaringType.Name == rootXamlType.Name) { string clrNs; XamlMember member = reader.Member; if (XamlBuildTaskServices.TryExtractClrNs(member.PreferredXamlNamespace, out clrNs) && clrNs == oldNamespace) { // This is a member defined on the document root type, but missing the project root namespace. Fix it. XamlMember newMember = new XamlMember(member.Name, rootXamlType, member.IsAttachable); Fx.Assert(rootXamlType != null, "First StartObject should already have been processed"); writer.WriteStartMember(newMember); } else { writer.WriteNode(reader); } } else { writer.WriteNode(reader); } } writer.Close(); return newStrippedXamlNodes; } bool ProcessDirective(XamlReader reader, ClassData classData, NamedObject currentObject, bool isRootElement, XamlNodeList strippedXamlNodes, out bool readNextNode) { Fx.Assert(reader.NodeType == XamlNodeType.StartMember, "Current node should be a Start Member Node"); XamlMember member = reader.Member; bool directiveRecognized = false; readNextNode = false; switch (member.Name) { case "Name": // Unlike all the other directives that we process, x:Name should be written // to the stripped output. strippedXamlNodes.Writer.WriteStartMember(member); string objectName = ReadAtom(reader, XamlLanguage.Name.Name); if (!objectName.StartsWith(XamlBuildTaskServices.SerializerReferenceNamePrefix, StringComparison.Ordinal)) { currentObject.Name = objectName; classData.NamedObjects.Add(currentObject); } strippedXamlNodes.Writer.WriteValue(objectName); strippedXamlNodes.Writer.WriteEndMember(); directiveRecognized = true; break; case "Class": if (isRootElement) { string fullClassName = ReadAtom(reader, XamlLanguage.Class.Name); SetClassName(fullClassName, classData); directiveRecognized = true; } break; case "ClassModifier": if (isRootElement) { string classModifier = ReadAtom(reader, XamlLanguage.ClassModifier.Name); classData.IsPublic = XamlBuildTaskServices.IsPublic(classModifier); directiveRecognized = true; } break; case "FieldModifier": string fieldModifier = ReadAtom(reader, XamlLanguage.FieldModifier.Name); currentObject.Visibility = XamlBuildTaskServices.GetMemberVisibility(fieldModifier); directiveRecognized = true; break; case "Code": string codeSnippet = ReadAtom(reader, XamlLanguage.Code.Name); classData.CodeSnippets.Add(codeSnippet); directiveRecognized = true; break; case "Members": foreach (PropertyData property in ReadProperties(reader.ReadSubtree())) { classData.Properties.Add(property); } if (!classData.RequiresCompilationPass2) { foreach (PropertyData property in classData.Properties) { if (property.Type.IsUnknown) { classData.RequiresCompilationPass2 = true; break; } } } directiveRecognized = true; readNextNode = true; break; case "ClassAttributes": foreach (AttributeData attribute in ReadAttributesCollection(reader.ReadSubtree())) { classData.Attributes.Add(attribute); } directiveRecognized = true; readNextNode = true; break; } if (directiveRecognized == true && readNextNode == false) { reader.Read(); Fx.Assert(reader.NodeType == XamlNodeType.EndMember, "Current node should be a XamlEndmember"); } return directiveRecognized; } private IList ReadAttributesCollection(XamlReader reader) { IList attributes = new List(); bool nextNodeRead = false; while (nextNodeRead || reader.Read()) { this.namespaceTable.ManageNamespace(reader); nextNodeRead = false; if (reader.NodeType == XamlNodeType.StartObject && reader.Type != null) { AttributeData attribute = null; try { attribute = AttributeData.LoadAttributeData(reader.ReadSubtree(), this.namespaceTable, this.rootNamespace); } catch (InvalidOperationException e) { throw FxTrace.Exception.AsError(LogInvalidOperationException(reader, e.Message)); } nextNodeRead = true; attributes.Add(attribute); } } return attributes; } IEnumerable ReadProperties(XamlReader reader) { IDictionary members = new Dictionary(); bool nextNodeRead = false; while (nextNodeRead || reader.Read()) { namespaceTable.ManageNamespace(reader); nextNodeRead = false; if (reader.NodeType == XamlNodeType.StartObject) { if (reader.Type == XamlLanguage.Property) { PropertyData xProperty = LoadProperty(reader.ReadSubtree()); nextNodeRead = true; if (members.ContainsKey(xProperty.Name)) { throw FxTrace.Exception.AsError(LogInvalidOperationException(reader, SR.DuplicatePropertyDefinition(xProperty.Name))); } members.Add(xProperty.Name, xProperty); } } } return members.Values; } PropertyData LoadProperty(XamlReader xamlReader) { if (xamlReader == null) { throw FxTrace.Exception.ArgumentNull("xamlReader"); } PropertyData property = new PropertyData(); while (xamlReader.Read()) { if (xamlReader.NodeType == XamlNodeType.StartMember) { XamlMember member = xamlReader.Member; switch (member.Name) { case "Name": property.Name = ReadValueAsString(xamlReader.ReadSubtree()); break; case "Type": property.Type = ReadPropertyType(xamlReader.ReadSubtree()); break; case "Attributes": foreach (AttributeData attribute in ReadAttributesCollection(xamlReader.ReadSubtree())) { property.Attributes.Add(attribute); } break; case "Modifier": string propertyModifier = ReadValueAsString(xamlReader.ReadSubtree()); property.Visibility = XamlBuildTaskServices.GetMemberVisibility(propertyModifier); break; default: // Ignore AttachedProperties on property if (!member.IsAttachable) { throw FxTrace.Exception.AsError(LogInvalidOperationException(xamlReader, SR.UnknownPropertyMember(member.Name))); } break; } } } if (string.IsNullOrEmpty(property.Name)) { throw FxTrace.Exception.AsError(LogInvalidOperationException(xamlReader, SR.PropertyNameRequired)); } if (property.Type == null) { throw FxTrace.Exception.AsError(LogInvalidOperationException(xamlReader, SR.PropertyTypeRequired(property.Name))); } return property; } XamlType ReadPropertyType(XamlReader xamlReader) { while (xamlReader.Read()) { if (xamlReader.NodeType == XamlNodeType.Value && xamlReader.Value is string) { return XamlBuildTaskServices.GetXamlTypeFromString((string)xamlReader.Value, this.namespaceTable, xamlReader.SchemaContext); } } return null; } string ReadValueAsString(XamlReader xamlReader) { while (xamlReader.Read()) { if (xamlReader.NodeType == XamlNodeType.Value) { return xamlReader.Value as string; } } return string.Empty; } string ReadAtom(XamlReader reader, string propertyName) { reader.Read(); if (reader.NodeType != XamlNodeType.Value) { throw FxTrace.Exception.AsError(LogInvalidOperationException(reader, SR.TextRepresentationExpected(propertyName))); } return (string)reader.Value; } void SetClassName(string fullClassName, ClassData classData) { int lastIndex = fullClassName.LastIndexOf('.'); if (lastIndex != -1) { string classNamespace = fullClassName.Substring(0, lastIndex); string className = fullClassName.Substring(lastIndex + 1); classData.Name = className; classData.Namespace = classNamespace; } else { classData.Name = fullClassName; classData.Namespace = String.Empty; } if (string.IsNullOrEmpty(classData.Name)) { throw FxTrace.Exception.AsError(LogInvalidOperationException(null, SR.ClassNameMustBeNonEmpty)); } } Exception LogInvalidOperationException(XamlReader reader, string exceptionMessage) { IXamlLineInfo lineInfo = reader == null ? null : reader as IXamlLineInfo; if (lineInfo != null && lineInfo.HasLineInfo) { return new LoggableException(new InvalidOperationException(exceptionMessage)) { Source = this.xamlFileName, LineNumber = lineInfo.LineNumber, LinePosition = lineInfo.LinePosition }; } else { return new LoggableException(new InvalidOperationException(exceptionMessage)) { Source = this.xamlFileName }; } } } }