// // Copyright (c) Microsoft Corporation. All rights reserved. // namespace Microsoft.Activities.Presentation.Xaml { using System; using System.Activities; using System.Activities.Debugger; using System.Activities.Debugger.Symbol; using System.Activities.Presentation; using System.Activities.Presentation.ViewState; using System.Activities.Presentation.Xaml; using System.Activities.XamlIntegration; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Runtime.Versioning; using System.Xaml; internal class WorkflowDesignerXamlHelper { private IWorkflowDesignerXamlHelperExecutionContext executionContext; private XamlMember dynamicActivityPropertyNameMember; private XamlMember dynamicActivityPropertyValueMember; internal WorkflowDesignerXamlHelper(IWorkflowDesignerXamlHelperExecutionContext executionContext) { this.executionContext = executionContext; SharedFx.Assert(this.executionContext != null, "this.executionContext != null"); SharedFx.Assert(this.executionContext.XamlSchemaContext != null, "this.executionContext.XamlSchemaContext != null"); this.dynamicActivityPropertyNameMember = new XamlMember(typeof(DynamicActivityProperty).GetProperty("Name"), this.XamlSchemaContext); this.dynamicActivityPropertyValueMember = new XamlMember(typeof(DynamicActivityProperty).GetProperty("Value"), this.XamlSchemaContext); } private delegate void SourceLocationFoundCallback(object obj, SourceLocation sourceLocation); internal enum DeserializationMode { Default, ErrorTolerant, } public FrameworkName FrameworkName { get { return this.executionContext.FrameworkName; } } public WorkflowDesignerXamlSchemaContext XamlSchemaContext { get { return this.executionContext.XamlSchemaContext; } } public ViewStateIdManager IdManager { get { return this.executionContext.IdManager; } } public WorkflowSymbol LastWorkflowSymbol { get { return this.executionContext.LastWorkflowSymbol; } set { this.executionContext.LastWorkflowSymbol = value; } } public string LocalAssemblyName { get { return this.executionContext.LocalAssemblyName; } } public void OnSerializationCompleted(Dictionary sourceLocationObjectToModelItemObjectMapping) { this.executionContext.OnSerializationCompleted(sourceLocationObjectToModelItemObjectMapping); } public void OnBeforeDeserialize() { this.executionContext.OnBeforeDeserialize(); } public void OnSourceLocationFound(object target, SourceLocation sourceLocation) { this.executionContext.OnSourceLocationFound(target, sourceLocation); } public void OnAfterDeserialize(Dictionary viewStateDataSourceLocationMapping) { this.executionContext.OnAfterDeserialize(viewStateDataSourceLocationMapping); } // Get root Activity. Currently only handle when the object is ActivityBuilder or Activity. // May return null if it does not know how to get the root activity. internal static Activity GetRootWorkflowElement(object rootModelObject) { SharedFx.Assert(rootModelObject != null, "Cannot pass null as rootModelObject"); Activity rootWorkflowElement; IDebuggableWorkflowTree debuggableWorkflowTree = rootModelObject as IDebuggableWorkflowTree; if (debuggableWorkflowTree != null) { rootWorkflowElement = debuggableWorkflowTree.GetWorkflowRoot(); } else { // Loose xaml case. rootWorkflowElement = rootModelObject as Activity; } return rootWorkflowElement; } internal static Activity GetRootElementForSymbol(object rootInstance, Activity documentRootElement) { ActivityBuilder activityBuilder = rootInstance as ActivityBuilder; if (activityBuilder != null) { documentRootElement = activityBuilder.ConvertToDynamicActivity(); } return documentRootElement; } // Copy the root namespaces from a reader to a writer. // DesignTimeXamlWriter follows proper XAML convention by omitting the assembly name from // clr-namespaces in the local assembly. However, VB Expressions aren't local-assembly-aware, // and require an assembly name. So for every clr-namespace with no assembly name, we add an // additional namespace record with an assembly name, to support VB. // We only do this at the root level, since the designer only writes out namespaces at the root level. internal void CopyNamespacesAndAddLocalAssembly(System.Xaml.XamlReader activityBuilderReader, System.Xaml.XamlWriter objectWriter) { // Designer loads alwas provide line info IXamlLineInfo lineInfo = (IXamlLineInfo)activityBuilderReader; IXamlLineInfoConsumer lineInfoConsumer = (IXamlLineInfoConsumer)objectWriter; HashSet definedPrefixes = new HashSet(); List localAsmNamespaces = null; while (activityBuilderReader.Read()) { lineInfoConsumer.SetLineInfo(lineInfo.LineNumber, lineInfo.LinePosition); if (activityBuilderReader.NodeType == XamlNodeType.NamespaceDeclaration) { definedPrefixes.Add(activityBuilderReader.Namespace.Prefix); if (this.XamlSchemaContext.IsClrNamespaceWithNoAssembly(activityBuilderReader.Namespace.Namespace)) { if (localAsmNamespaces == null) { localAsmNamespaces = new List(); } localAsmNamespaces.Add(activityBuilderReader.Namespace); } objectWriter.WriteNode(activityBuilderReader); } else { if (localAsmNamespaces != null) { foreach (NamespaceDeclaration ns in localAsmNamespaces) { string prefix = null; int i = 0; do { i++; prefix = ns.Prefix + i.ToString(CultureInfo.InvariantCulture); } while (definedPrefixes.Contains(prefix)); string fullNs = this.XamlSchemaContext.AddLocalAssembly(ns.Namespace); objectWriter.WriteNamespace(new NamespaceDeclaration(fullNs, prefix)); definedPrefixes.Add(prefix); } } objectWriter.WriteNode(activityBuilderReader); return; } } } internal string SerializeToString(object obj, string fileName) { FrameworkName targetFramework = this.FrameworkName; string sourceFile = null; Activity rootWorkflowElement = GetRootWorkflowElement(obj); Dictionary modelItemObjectSequence = null; // If the current target is 4.5 or Higher, let us not write the filename attribute as DebugSymbol eliminates the need for it. // We will serialize without the Filename by removing it from the element and adding it back after serialization if (targetFramework.Is45OrHigher()) { if (AttachablePropertyServices.TryGetProperty(rootWorkflowElement, XamlDebuggerXmlReader.FileNameName, out sourceFile)) { AttachablePropertyServices.RemoveProperty(rootWorkflowElement, XamlDebuggerXmlReader.FileNameName); } } TextWriter textWriter = new StringWriter(CultureInfo.InvariantCulture); WorkflowDesignerXamlSchemaContext schemaContext = obj is ActivityBuilder ? this.XamlSchemaContext : new WorkflowDesignerXamlSchemaContext(null); bool shouldWriteDebugSymbol = true; if (targetFramework.IsLessThan45()) { shouldWriteDebugSymbol = false; } System.Xaml.XamlReader outerReader; XamlObjectReaderWithSequence innerReader; using (textWriter) { UsingXamlWriter( new DesignTimeXamlWriter(textWriter, schemaContext, shouldWriteDebugSymbol), delegate(DesignTimeXamlWriter designTimeXamlWriter) { UsingXamlWriter( ActivityXamlServices.CreateBuilderWriter(designTimeXamlWriter), delegate(System.Xaml.XamlWriter activityBuilderWriter) { UsingXamlWriter( new ActivityTemplateFactoryBuilderWriter(activityBuilderWriter, schemaContext), delegate(System.Xaml.XamlWriter writer) { // If ViewStateManager property is attached, remove it. It needs to be regenerated if we target 4.5. // It should be removed if we're targeting 4.0. AttachablePropertyServices.RemoveProperty(obj, WorkflowViewState.ViewStateManagerProperty); this.CreateXamlObjectReaders(obj, schemaContext, out outerReader, out innerReader); using (innerReader) { using (outerReader) { #if ERROR_TOLERANT_SUPPORT if (ErrorActivity.GetHasErrorActivities(obj)) { ErrorTolerantObjectWriter.TransformAndStripErrors(outerReader, writer); } else { #endif XamlServices.Transform(outerReader, writer); #if ERROR_TOLERANT_SUPPORT } #endif } } modelItemObjectSequence = innerReader.SequenceNumberToObjectMap; }); }); }); } string retVal = textWriter.ToString(); if (targetFramework.IsLessThan45()) { if (sourceFile != null) { XamlDebuggerXmlReader.SetFileName(rootWorkflowElement, sourceFile); } } IList loadErrors; Dictionary sourceLocations; object deserializedObject = this.DeserializeString(retVal, out loadErrors, out sourceLocations); if (!string.IsNullOrEmpty(fileName) && targetFramework.Is45OrHigher()) { this.LastWorkflowSymbol = this.GetWorkflowSymbol(fileName, deserializedObject, sourceLocations); if (this.LastWorkflowSymbol != null) { retVal = retVal.Replace(DesignTimeXamlWriter.EmptyWorkflowSymbol, this.LastWorkflowSymbol.Encode()); } } // The symbol is actually removed in GetAttachedWorkflowSymbol() after deserialization completes. System.Xaml.AttachablePropertyServices.RemoveProperty(GetRootWorkflowElement(deserializedObject), DebugSymbol.SymbolName); this.CreateXamlObjectReaders(deserializedObject, schemaContext, out outerReader, out innerReader); Dictionary sourceLocationObjectToModelItemObjectMapping = new Dictionary(ObjectReferenceEqualityComparer.Default); using (innerReader) { using (outerReader) { while (outerReader.Read()) { } Dictionary sourceLocationObjectSequence = innerReader.SequenceNumberToObjectMap; foreach (KeyValuePair sourceLocationObjectEntry in sourceLocationObjectSequence) { int key = sourceLocationObjectEntry.Key; object sourceLocationObject = sourceLocationObjectEntry.Value; object modelItemObject; if (modelItemObjectSequence.TryGetValue(key, out modelItemObject)) { sourceLocationObjectToModelItemObjectMapping.Add(sourceLocationObject, modelItemObject); } } } } this.OnSerializationCompleted(sourceLocationObjectToModelItemObjectMapping); return retVal; } internal void CreateXamlObjectReaders(object deserializedObject, XamlSchemaContext schemaContext, out XamlReader newWorkflowReader, out XamlObjectReaderWithSequence deserializedObjectSequenceBuilder) { deserializedObjectSequenceBuilder = new XamlObjectReaderWithSequence(deserializedObject, schemaContext); if (this.FrameworkName.Is45OrHigher()) { newWorkflowReader = ViewStateXamlHelper.ConvertAttachedPropertiesToViewState(deserializedObjectSequenceBuilder, this.IdManager); } else { newWorkflowReader = ViewStateXamlHelper.RemoveIdRefs(deserializedObjectSequenceBuilder); } } internal object DeserializeString(string text) { IList loadErrors; Dictionary sourceLocations; return this.DeserializeString(text, out loadErrors, out sourceLocations); } internal object DeserializeString(string text, out IList loadErrors, out Dictionary sourceLocations) { try { return this.DeserializeString(text, DeserializationMode.Default, out loadErrors, out sourceLocations); } catch (XamlObjectWriterException) { // Fall back to error-tolerant path. We don't do this by default for perf reasons. return this.DeserializeString(text, DeserializationMode.ErrorTolerant, out loadErrors, out sourceLocations); } } //// XAML writer may throw exception during dispose, therefore the following code will cause exception masking //// //// using (XamlWriter xamlWriter) //// { //// ... //// } //// //// If there are any exception A thrown within the block, and if xamlWriter.Dispose() throws an exception B //// The exception B will mask exception A and the exception is the ErrorTolerant scenario will be broken. //// //// The fix to this problem is to ---- any XamlException thrown during Dispose(), we are in general not //// interested in those exceptions. private static void UsingXamlWriter(T xamlWriter, Action work) where T : XamlWriter { if (xamlWriter != null) { try { work(xamlWriter); } finally { try { xamlWriter.Close(); } catch (XamlException e) { // ignore any XAML exception during closing a XamlWriter Trace.WriteLine(e.Message); } } } } // there are two kind of attribute: // 1) in lined : argument="some value" // 2) // // // here, for (1) return the source location of "some value". // for (2) return null private static SourceLocation GetInlineAttributeValueLocation(LineColumnPair startPoint, SourceTextScanner sourceTextScanner) { const char SingleQuote = '\''; const char DoubleQuote = '"'; const char StartAngleBracket = '<'; Tuple start = sourceTextScanner.SearchCharAfter(startPoint, SingleQuote, DoubleQuote, StartAngleBracket); if (start == null) { return null; } if (start.Item2 == StartAngleBracket) { return null; } Tuple end = sourceTextScanner.SearchCharAfter(start.Item1, start.Item2); if (end == null) { SharedFx.Assert("end of SourceLocation is not found"); return null; } return new SourceLocation(null, start.Item1.LineNumber, start.Item1.ColumnNumber, end.Item1.LineNumber, end.Item1.ColumnNumber); } private WorkflowSymbol GetWorkflowSymbol(string fileName, object deserializedObject, Dictionary sourceLocations) { if (deserializedObject != null) { Activity deserializedRootElement = GetRootWorkflowElement(deserializedObject); if (deserializedRootElement != null) { try { deserializedRootElement = GetRootElementForSymbol(deserializedObject, deserializedRootElement); return new WorkflowSymbol { FileName = fileName, Symbols = SourceLocationProvider.GetSymbols(deserializedRootElement, sourceLocations) }; } catch (Exception ex) { if (SharedFx.IsFatal(ex)) { throw; } // This happens when the workflow is invalid so GetSymbols fails. // ---- exception here. } } } return null; } private object DeserializeString(string text, DeserializationMode mode, out IList loadErrors, out Dictionary sourceLocations) { object result = null; loadErrors = null; Dictionary collectingSourceLocations = new Dictionary(ObjectReferenceEqualityComparer.Default); SourceLocationFoundCallback sourceLocationFoundCallback = new SourceLocationFoundCallback((obj, sourceLocation) => { // If an object appear more than once in the XAML stream (e.g. System.Type, which is cached by reflection) // we count the first occurrence. if (!collectingSourceLocations.ContainsKey(obj)) { collectingSourceLocations.Add(obj, sourceLocation); } this.OnSourceLocationFound(obj, sourceLocation); }); this.XamlSchemaContext.ContainsConversionRequiredType = false; Dictionary viewStateDataSourceLocationMapping = null; using (XamlDebuggerXmlReader debuggerReader = new XamlDebuggerXmlReader(new StringReader(text), this.XamlSchemaContext)) { using (System.Xaml.XamlReader activityBuilderReader = ActivityXamlServices.CreateBuilderReader(debuggerReader)) { using (System.Xaml.XamlReader activityTemplateFactoryBuilderReader = new ActivityTemplateFactoryBuilderReader(activityBuilderReader, this.XamlSchemaContext)) { debuggerReader.SourceLocationFound += delegate(object sender, SourceLocationFoundEventArgs args) { sourceLocationFoundCallback(args.Target, args.SourceLocation); }; this.OnBeforeDeserialize(); debuggerReader.CollectNonActivitySourceLocation = this.FrameworkName.Is45OrHigher(); using (System.Xaml.XamlReader reader = ViewStateXamlHelper.ConvertViewStateToAttachedProperties(activityTemplateFactoryBuilderReader, this.IdManager, out viewStateDataSourceLocationMapping)) { switch (mode) { #if ERROR_TOLERANT_SUPPORT case DeserializationMode.ErrorTolerant: { ErrorTolerantObjectWriter tolerantWriter = new ErrorTolerantObjectWriter(reader.SchemaContext); tolerantWriter.LocalAssemblyName = this.LocalAssemblyName; XamlServices.Transform(reader, tolerantWriter); loadErrors = this.CheckFileFormatError(tolerantWriter.LoadErrors); result = tolerantWriter.Result; ErrorActivity.SetHasErrorActivities(result, true); } break; #endif case DeserializationMode.Default: { result = this.TransformAndGetPropertySourceLocation(reader, new SourceTextScanner(text), sourceLocationFoundCallback); loadErrors = this.CheckFileFormatError(loadErrors); } break; } } } } } sourceLocations = collectingSourceLocations; this.OnAfterDeserialize(viewStateDataSourceLocationMapping); return result; } // For dynamic activity property, we needs to collect the source location of // its default value when the value is inlined. private KeyValuePair TransformDynamicActivityProperty( XamlReader reader, XamlObjectWriter objectWriter, SourceTextScanner sourceTextScanner) { // (Number of SM -Number of EM) since SM DAP.Name is read. // SO DAP ---nameReadingLevel=0 // SM NAME ---nameReadingLevel=1 // SO String ---nameReadingLevel=1 // SM Initialize ---nameReadingLevel=2 // VA StringValue ---nameReadingLevel=2 // EM ---nameReadingLevel=1 // SO ---nameReadingLevel=1 // EM ---nameReadingLevel=0 // EO ---nameReadingLevel=0 int nameReadingLevel = 0; IXamlLineInfo lineInfo = (IXamlLineInfo)reader; SourceLocation defaultValueLocation = null; string propertyName = null; while (reader.Read()) { switch (reader.NodeType) { case XamlNodeType.StartMember: if (nameReadingLevel > 0 || reader.Member == this.dynamicActivityPropertyNameMember) { ++nameReadingLevel; } else if (reader.Member == this.dynamicActivityPropertyValueMember) { LineColumnPair startPoint = new LineColumnPair(lineInfo.LineNumber, lineInfo.LinePosition); defaultValueLocation = GetInlineAttributeValueLocation(startPoint, sourceTextScanner); } break; case XamlNodeType.EndMember: if (nameReadingLevel > 0) { --nameReadingLevel; } break; case XamlNodeType.Value: if (nameReadingLevel > 0) { propertyName = reader.Value as string; } break; } objectWriter.WriteNode(reader); } if (propertyName != null && defaultValueLocation != null) { return new KeyValuePair(propertyName, defaultValueLocation); } return new KeyValuePair(); } private object TransformAndGetPropertySourceLocation(XamlReader reader, SourceTextScanner sourceTextScanner, SourceLocationFoundCallback sourceLocationFoundCallback) { // Dictionary propertyValueLocationMapping = new Dictionary(); object deserializedObject = null; object earlyResult = null; UsingXamlWriter( new XamlObjectWriter(reader.SchemaContext), delegate(XamlObjectWriter objectWriter) { if (this.XamlSchemaContext.HasLocalAssembly) { this.CopyNamespacesAndAddLocalAssembly(reader, objectWriter); } if (!(reader is IXamlLineInfo)) { XamlServices.Transform(reader, objectWriter); earlyResult = objectWriter.Result; return; } XamlType dynamicActivityPropertyType = this.XamlSchemaContext.GetXamlType(typeof(DynamicActivityProperty)); while (reader.Read()) { // read SubTree will moves the reader pointed to // element after its EO. So, we need to use a while while (!reader.IsEof && reader.NodeType == XamlNodeType.StartObject && dynamicActivityPropertyType == reader.Type) { KeyValuePair nameSourceLocation = this.TransformDynamicActivityProperty(reader.ReadSubtree(), objectWriter, sourceTextScanner); if (nameSourceLocation.Key != null && nameSourceLocation.Value != null && !propertyValueLocationMapping.ContainsKey(nameSourceLocation.Key)) { propertyValueLocationMapping.Add(nameSourceLocation.Key, nameSourceLocation.Value); } } if (!reader.IsEof) { objectWriter.WriteNode(reader); } } deserializedObject = objectWriter.Result; }); if (earlyResult != null) { return earlyResult; } ActivityBuilder activityBuilder = deserializedObject as ActivityBuilder; if (activityBuilder == null) { return deserializedObject; } foreach (KeyValuePair propertyValueLocation in propertyValueLocationMapping) { string propertyName = propertyValueLocation.Key; SourceLocation propertyLocation = propertyValueLocation.Value; if (!activityBuilder.Properties.Contains(propertyName)) { SharedFx.Assert(string.Format(CultureInfo.CurrentCulture, "no such property:{0}", propertyName)); continue; } DynamicActivityProperty property = activityBuilder.Properties[propertyName]; if (property == null || property.Value == null) { SharedFx.Assert(string.Format(CultureInfo.CurrentCulture, "no such property value:{0}", propertyName)); continue; } object expression = (property.Value is Argument) ? ((Argument)property.Value).Expression : null; if (expression != null) { sourceLocationFoundCallback(expression, propertyLocation); } else { sourceLocationFoundCallback(property.Value, propertyLocation); } } return deserializedObject; } private IList CheckFileFormatError(IList loadErrors) { IList result = loadErrors; if (this.XamlSchemaContext.ContainsConversionRequiredType) { if (result == null) { result = new List(); } result.Add(new XamlLoadErrorInfo(SharedSR.FileFormatError, 0, 0)); } return result; } } }