Code:
/ 4.0 / 4.0 / DEVDIV_TFS / Dev10 / Releases / RTMRel / wpf / src / Framework / System / Windows / Documents / TextRangeSerialization.cs / 1305600 / TextRangeSerialization.cs
//---------------------------------------------------------------------------- // // File: TextRangeSerialization.cs // // Copyright (C) Microsoft Corporation. All rights reserved. // // Description: Set of static methods implementing text range serialization // //--------------------------------------------------------------------------- namespace System.Windows.Documents { using MS.Internal; using System.Text; using System.Xml; using System.IO; using System.Windows.Markup; // TypeConvertContext, ParserContext using System.Windows.Controls; using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Globalization; using System.Security; ////// TextRangeSerialization is a static class containing /// an implementation for TextRange serialization functionality. /// It is only used from TextRange.GetXml/AppendXml methods. /// internal static class TextRangeSerialization { // ------------------------------------------------------------- // // Internal Methods // // ------------------------------------------------------------- #region Internal Methods internal static void WriteXaml(XmlWriter xmlWriter, ITextRange range, bool useFlowDocumentAsRoot, WpfPayload wpfPayload) { WriteXaml(xmlWriter, range, useFlowDocumentAsRoot, wpfPayload, false); } ////// Writes a content of current range in form of valid xml. /// Places an artificial element xaml:FlowDocument as a root of output xml. /// /// /// XmlWriter to which the range will be serialized /// /// /// TextRange whose content is copied into XmlWriter xmlWriter. /// /// /// true means that we need to serialize the whole FlowDocument - used in FileSave scenario; /// false means that we are in copy-paste scenario and will use Section or Span as a root - depending on context. /// /// /// When this parameter is not null, images are serialized. When null, images are stripped out. /// /// /// When TRUE, TextElements are serialized as-is. When FALSE, they're upcast to their base type. /// internal static void WriteXaml(XmlWriter xmlWriter, ITextRange range, bool useFlowDocumentAsRoot, WpfPayload wpfPayload, bool preserveTextElements) { // Set unindented formatting to avoid inserting insignificant whitespaces as significant ones Formatting saveWriterFormatting = Formatting.None; if (xmlWriter is XmlTextWriter) { saveWriterFormatting = ((XmlTextWriter)xmlWriter).Formatting; ((XmlTextWriter)xmlWriter).Formatting = Formatting.None; } // Get the default xamlTypeMapper. XamlTypeMapper xamlTypeMapper = XmlParserDefaults.DefaultMapper; // Identify structural scope of selection - nearest common ancestor ITextPointer commonAncestor = FindSerializationCommonAncestor(range); // Decide whether we need last paragraph merging or not bool lastParagraphMustBeMerged = !TextPointerBase.IsAfterLastParagraph(range.End) && range.End.GetPointerContext(LogicalDirection.Backward) != TextPointerContext.ElementStart; // Write wrapping element with contextual properties WriteRootFlowDocument(range, commonAncestor, xmlWriter, xamlTypeMapper, lastParagraphMustBeMerged, useFlowDocumentAsRoot); // The ignoreWriteHyperlinkEnd flag will be set after call WriteOpeningTags. // If ignoreWriteHyperlinkEnd is true, WriteXamlTextSegment won't write Hyperlink end element // since Hyperlink writing opening tag is ignored by selecting the partial of Hyperlink. bool ignoreWriteHyperlinkEnd; ListignoreList = new List (); // Start counting tags needed to be closed. // EmptyDocumentDepth==1 - counts FlowDocument opened in WriteRootFlowDocument above. int elementLevel = EmptyDocumentDepth + WriteOpeningTags(range, range.Start, commonAncestor, xmlWriter, xamlTypeMapper, /*reduceElement:*/wpfPayload == null, out ignoreWriteHyperlinkEnd, ref ignoreList, preserveTextElements); if (range.IsTableCellRange) { WriteXamlTableCellRange(xmlWriter, range, xamlTypeMapper, ref elementLevel, wpfPayload, preserveTextElements); } else { WriteXamlTextSegment(xmlWriter, range.Start, range.End, xamlTypeMapper, ref elementLevel, wpfPayload, ignoreWriteHyperlinkEnd, ignoreList, preserveTextElements); } // Close all remaining tags - scoping its End position Invariant.Assert(elementLevel >= 0, "elementLevel cannot be negative"); while (elementLevel-- > 0) { xmlWriter.WriteFullEndElement(); } // Restore xmlWriter's Formatting property if (xmlWriter is XmlTextWriter) { ((XmlTextWriter)xmlWriter).Formatting = saveWriterFormatting; } } /// /// Reads a well-formed xml representing a serialized text range. /// It expects a root element xaml:FlowDocument and two range markers /// The result of reading is pasting this text into End position /// of text range. /// /// /// TextRange designating the target position for pasting. /// The existing content of a range will be deleted and new content /// will be inserted at the end. /// Resulting locations of Start/End positions depend on their gravities. /// Normally (when gravity=Backward/Forward respectively) the resulting /// range will embrace the inserted content. /// /// /// Represents a portion of xml to insert into the range. /// ////// We are expecting to be called with xmlReader on opening tag /// of root text range element - xaml:FlowDocument. /// Some insignificant stuff may occur before the root though. /// Otherwise exception will be thrown. /// internal static void PasteXml(TextRange range, TextElement fragment) { Invariant.Assert(fragment != null); // Check a special case for pasing a single embedded element if (PasteSingleEmbeddedElement(range, fragment)) { // All done. Return successfully. return; } // Set default value for an indicator of whether we need to merge last paragraph or not. // It depends on a state of a range, so do it before emptying the range. AdjustFragmentForTargetRange(fragment, range); // Delete current content of a range if (!range.IsEmpty) { range.Text = String.Empty; } Invariant.Assert(range.IsEmpty, "range must be empty in the beginning of pasting"); // Chek special case of empty pasted fragment if (((ITextPointer)fragment.ContentStart).CompareTo(fragment.ContentEnd) == 0) { // Pasted fragment is empty. Nothing to insert. return; } // Transfer the content from reader to writer and merge elements on both ends PasteTextFragment(fragment, range); } #endregion Internal Methods // -------------------------------------------------------------- // // Private Methods // // ------------------------------------------------------------- #region Private Methods // ............................................................. // // Serialization // // ............................................................. ////// This function serializes text segment formed by rangeStart and rangeEnd to valid xml using xmlWriter. /// ////// To mask the security exception from XamlWriter.Save in partial trust case, /// this function checks if the current call stack has the all clipboard permission. /// private static void WriteXamlTextSegment(XmlWriter xmlWriter, ITextPointer rangeStart, ITextPointer rangeEnd, XamlTypeMapper xamlTypeMapper, ref int elementLevel, WpfPayload wpfPayload, bool ignoreWriteHyperlinkEnd, ListignoreList, bool preserveTextElements) { // Special case for pure text selection - we need a Run wrapper for it. if (elementLevel == EmptyDocumentDepth && typeof(Run).IsAssignableFrom(rangeStart.ParentType)) { elementLevel++; xmlWriter.WriteStartElement(typeof(Run).Name); } // Create text navigator for reading the range's content ITextPointer textReader = rangeStart.CreatePointer(); // Exclude last opening tag from serialization - we don't need to create extra element // is cases when we have whole paragraphs/cells selected. // NOTE: We do this slightly differently than in TextRangeEdit.AdjustRangeEnd, where we use normalization for adjusted position. // In this case normalized position does not work, because we need to keep information about crossed paragraph boundary. while (rangeEnd.GetPointerContext(LogicalDirection.Backward) == TextPointerContext.ElementStart) { rangeEnd = rangeEnd.GetNextContextPosition(LogicalDirection.Backward); } // Write the range internal contents while (textReader.CompareTo(rangeEnd) < 0) { TextPointerContext runType = textReader.GetPointerContext(LogicalDirection.Forward); switch (runType) { case TextPointerContext.ElementStart: TextElement nextElement = (TextElement)textReader.GetAdjacentElement(LogicalDirection.Forward); if (nextElement is Hyperlink) { // Don't write Hyperlink start element if Hyperlink is invalid // in case of having a UiElement except Image or stated the range end // position before the end position of the Hyperlink. if (IsHyperlinkInvalid(textReader, rangeEnd)) { ignoreWriteHyperlinkEnd = true; textReader.MoveToNextContextPosition(LogicalDirection.Forward); continue; } } else if (nextElement != null) { // TextElementEditingBehaviorAttribute att = (TextElementEditingBehaviorAttribute)Attribute.GetCustomAttribute(nextElement.GetType(), typeof(TextElementEditingBehaviorAttribute)); if (att != null && !att.IsTypographicOnly) { if (IsPartialNonTypographic(textReader, rangeEnd)) { // Add pointer to ignore list ITextPointer ptr = textReader.CreatePointer(); ptr.MoveToElementEdge(ElementEdge.BeforeEnd); ignoreList.Add(ptr.Offset); textReader.MoveToNextContextPosition(LogicalDirection.Forward); continue; } } } elementLevel++; textReader.MoveToNextContextPosition(LogicalDirection.Forward); WriteStartXamlElement(/*range:*/null, textReader, xmlWriter, xamlTypeMapper, /*reduceElement:*/wpfPayload == null, preserveTextElements); break; case TextPointerContext.ElementEnd: // Don't write Hyperlink end element if Hyperlink include the invalid // in case of having a UiElement except Image or stated the range end // before the end position of the Hyperlink or Hyperlink opening tag is // skipped from WriteOpeningTags by selecting of the partial of Hyperlink. if (ignoreWriteHyperlinkEnd && (textReader.GetAdjacentElement(LogicalDirection.Forward) is Hyperlink)) { // Reset the flag to keep walk up the next Hyperlink tag ignoreWriteHyperlinkEnd = false; textReader.MoveToNextContextPosition(LogicalDirection.Forward); continue; } // Check the ignore list ITextPointer endPointer = textReader.CreatePointer(); endPointer.MoveToElementEdge(ElementEdge.BeforeEnd); // if (ignoreList.Contains(endPointer.Offset)) { ignoreList.Remove(endPointer.Offset); textReader.MoveToNextContextPosition(LogicalDirection.Forward); continue; } elementLevel--; if (TextSchema.IsBreak(textReader.ParentType)) { // For LineBreak, etc. use empty element syntax xmlWriter.WriteEndElement(); } else { // // For all other textelements use explicit closing tag. xmlWriter.WriteFullEndElement(); } textReader.MoveToNextContextPosition(LogicalDirection.Forward); break; case TextPointerContext.Text: int textLength = textReader.GetTextRunLength(LogicalDirection.Forward); char[] text = new Char[textLength]; textLength = TextPointerBase.GetTextWithLimit(textReader, LogicalDirection.Forward, text, 0, textLength, rangeEnd); // XmlWriter will throw an ArgumentException if text contains // any invalid surrogates, so strip them out now. textLength = StripInvalidSurrogateChars(text, textLength); xmlWriter.WriteChars(text, 0, textLength); textReader.MoveToNextContextPosition(LogicalDirection.Forward); break; case TextPointerContext.EmbeddedElement: object embeddedObject = textReader.GetAdjacentElement(LogicalDirection.Forward); textReader.MoveToNextContextPosition(LogicalDirection.Forward); WriteEmbeddedObject(embeddedObject, xmlWriter, wpfPayload); break; default: Invariant.Assert(false, "unexpected value of runType"); textReader.MoveToNextContextPosition(LogicalDirection.Forward); break; } } } /// /// Serializes a rectagular table range /// private static void WriteXamlTableCellRange(XmlWriter xmlWriter, ITextRange range, XamlTypeMapper xamlTypeMapper, ref int elementLevel, WpfPayload wpfPayload, bool preserveTextElements) { Invariant.Assert(range.IsTableCellRange, "range is expected to be in IsTableCellRange state"); ListtextSegments = range.TextSegments; int checkElementLevel = -1; // negative value as an indicator that it is not yet initialized // Set ignoreWriteHyperlinkEnd as false initially bool ignoreWriteHyperlinkEnd = false; List ignoreList = new List (); for (int i = 0; i < textSegments.Count; i++) { TextSegment textSegment = textSegments[i]; // Open a row for this segment (except for the very first one, for which we opened a row in a WriteOpeningTags method) if (i > 0) { ITextPointer pointer = textSegment.Start.CreatePointer(); while (!typeof(TableRow).IsAssignableFrom(pointer.ParentType)) { Invariant.Assert(typeof(TextElement).IsAssignableFrom(pointer.ParentType), "pointer must be still in a scope of TextElement"); pointer.MoveToElementEdge(ElementEdge.BeforeStart); } Invariant.Assert(typeof(TableRow).IsAssignableFrom(pointer.ParentType), "pointer must be in a scope of TableRow"); pointer.MoveToElementEdge(ElementEdge.BeforeStart); ITextRange textRange = new TextRange(textSegment.Start, textSegment.End); elementLevel += WriteOpeningTags(textRange, textSegment.Start, pointer, xmlWriter, xamlTypeMapper, /*reduceElement:*/wpfPayload == null, out ignoreWriteHyperlinkEnd, ref ignoreList, preserveTextElements); } // Output the cell segment for one row WriteXamlTextSegment(xmlWriter, textSegment.Start, textSegment.End, xamlTypeMapper, ref elementLevel, wpfPayload, ignoreWriteHyperlinkEnd, ignoreList, preserveTextElements); Invariant.Assert(elementLevel >= 4, "At the minimun we expected to stay within four elements: Section(wrapper),Table,TableRowGroup,TableRow"); if (checkElementLevel < 0) checkElementLevel = elementLevel; // initialize level checking variable Invariant.Assert(checkElementLevel == elementLevel, "elementLevel is supposed to be unchanged between segments of table cell range"); // Assuming that the element is TableRow - close it. // NOTE: Such assumption is valid because WriteXamlTextSegment moves end pointer out of all opening tags, // so it ends serialization immediately after the last cell's closing tag. // This means that we only need to close one level - for TableRow. // elementLevel--; xmlWriter.WriteFullEndElement(); } } /// /// Walks the tree up from current position and writes all scoping tags /// in their natural order - from root to leafs. /// /// /// Range identifying the whole selection. /// Needed for /// - table cell range case: proper column processing: to output only columns related to the selection /// - text segement case: hyperlink serialization heuristics /// /// /// ITextPointer identifying an element. /// /// /// A position identifying the scope which should be used for serialization. /// All tags outside of this scope will be ignored. /// /// /// XmlWriter to write element tags. /// /// /// ////// /// /// /// /// /// /// Number of opening tags written into XmlWriter. /// This number should be used afterwards to close all opened tags. /// private static int WriteOpeningTags(ITextRange range, ITextPointer thisElement, ITextPointer scope, XmlWriter xmlWriter, XamlTypeMapper xamlTypeMapper, bool reduceElement, out bool ignoreWriteHyperlinkEnd, ref ListignoreList, bool preserveTextElements) { ignoreWriteHyperlinkEnd = false; // Recursion ends when we reach the scope level. We will write tags on returing path from the recursion if (thisElement.HasEqualScope(scope)) { return 0; // no elements have opened at this level. Return elementCount==0. } Invariant.Assert(typeof(TextElement).IsAssignableFrom(thisElement.ParentType), "thisElement is expected to be a TextElement"); ITextPointer previousLevel = thisElement.CreatePointer(); previousLevel.MoveToElementEdge(ElementEdge.BeforeStart); // Recurse into the parent element int elementLevel = WriteOpeningTags(range, previousLevel, scope, xmlWriter, xamlTypeMapper, reduceElement, out ignoreWriteHyperlinkEnd, ref ignoreList, preserveTextElements); // After returning from the recursion - when all parent tags have been written, // write the opening tag for this element // Hyperlink open tag will be skipped since the range selection of Hyperlink is the partial // of Hyperlink range or Hyperlink include invalid UIElement except Image. bool ignoreHyperlink = false; bool isPartialNonTypographic = false; if (thisElement.ParentType == typeof(Hyperlink)) { if (TextPointerBase.IsAtNonMergeableInlineStart(range.Start)) { ITextPointer position = thisElement.CreatePointer(); position.MoveToElementEdge(ElementEdge.BeforeStart); ignoreHyperlink = IsHyperlinkInvalid(position, range.End); } else { ignoreHyperlink = true; } } else { // TextElementEditingBehaviorAttribute att = (TextElementEditingBehaviorAttribute)Attribute.GetCustomAttribute(thisElement.ParentType, typeof(TextElementEditingBehaviorAttribute)); if (att != null && !att.IsTypographicOnly) { if (TextPointerBase.IsAtNonMergeableInlineStart(range.Start)) { ITextPointer position = thisElement.CreatePointer(); position.MoveToElementEdge(ElementEdge.BeforeStart); isPartialNonTypographic = IsPartialNonTypographic(position, range.End); } else { isPartialNonTypographic = true; } } } int count; if (ignoreHyperlink) { // Ignore writing Hyperlink opening tag ignoreWriteHyperlinkEnd = true; // Set elementLevel without adding it count = elementLevel; } else if (isPartialNonTypographic) { // Add the end pointer to the list ITextPointer position = thisElement.CreatePointer(); position.MoveToElementEdge(ElementEdge.BeforeEnd); ignoreList.Add(position.Offset); // Set elementLevel without adding to it count = elementLevel; } else { // Write the opening tag WriteStartXamlElement(range, thisElement, xmlWriter, xamlTypeMapper, reduceElement, preserveTextElements); // Each opening tag adds one to the level count count = elementLevel + 1; } // Return the opening tag count return count; } /// /// Writes an opening tag of an element together with all attributes /// representing Avalon properties. /// /// /// Parameter used for top-level Table element - to decide what columns to output. /// For all other elements it's ignored. /// /// /// TextPointer positioned in the scope of element whose /// start tag is going to be written. /// /// /// XmlWriter to output element opening tag. /// /// /// /// True value of this parameter indicates that /// serialization goes into XamlPackage, so all elements /// can be preserved as is; otherwise some of them must be /// reduced into simpler representations (such as InlineUIContainer -> Run /// and BlockUIContainer -> Paragraph). /// /// /// If TRUE, TextElements are serialized as-is. If FALSE, they're upcast /// to their base types. /// private static void WriteStartXamlElement(ITextRange range, ITextPointer textReader, XmlWriter xmlWriter, XamlTypeMapper xamlTypeMapper, bool reduceElement, bool preserveTextElements) { Type elementType = textReader.ParentType; Type elementTypeStandardized = TextSchema.GetStandardElementType(elementType, reduceElement); // Get rid f UIContainers when their child is not an image if (elementTypeStandardized == typeof(InlineUIContainer) || elementTypeStandardized == typeof(BlockUIContainer)) { Invariant.Assert(!reduceElement); InlineUIContainer inlineUIContainer = textReader.GetAdjacentElement(LogicalDirection.Backward) as InlineUIContainer; BlockUIContainer blockUIContainer = textReader.GetAdjacentElement(LogicalDirection.Backward) as BlockUIContainer; if ((inlineUIContainer == null || !(inlineUIContainer.Child is Image)) && (blockUIContainer == null || !(blockUIContainer.Child is Image))) { // Even when we serialize for DataFormats.XamlPackage we strip out UIElement // different from Images. // Note that this condition is consistent with the one in WriteEmbeddedObject - // so that when we reduce the element type fromm UIContainer to Run/Paragraph // we also output just a space instead of the embedded object conntained in it. elementTypeStandardized = TextSchema.GetStandardElementType(elementType, /*reduceElement:*/true); } } else if (preserveTextElements) { elementTypeStandardized = elementType; } bool customTextElement = preserveTextElements && !TextSchema.IsKnownType(elementType); if (customTextElement) { // If the element is not from PresentationFramework, we'll need to serialize a namespace // int index = elementTypeStandardized.Module.Name.LastIndexOf('.'); string assembly = (index == -1 ? elementTypeStandardized.Module.Name : elementTypeStandardized.Module.Name.Substring(0, index)); string nameSpace = "clr-namespace:" + elementTypeStandardized.Namespace + ";" + "assembly=" + assembly; string prefix = elementTypeStandardized.Namespace; xmlWriter.WriteStartElement(prefix, elementTypeStandardized.Name, nameSpace); } else { xmlWriter.WriteStartElement(elementTypeStandardized.Name); } // Write properties DependencyObject complexProperties = new DependencyObject(); WriteInheritableProperties(elementTypeStandardized, textReader, xmlWriter, /*onlyAffected:*/true, complexProperties); WriteNoninheritableProperties(elementTypeStandardized, textReader, xmlWriter, /*onlyAffected:*/true, complexProperties); if (customTextElement) { WriteLocallySetProperties(elementTypeStandardized, textReader, xmlWriter, complexProperties); } WriteComplexProperties(xmlWriter, complexProperties, elementTypeStandardized); // Special case for Table element serialization if (elementTypeStandardized == typeof(Table) && textReader is TextPointer) { // Write the columns text. WriteTableColumnsInformation(range, (Table)((TextPointer)textReader).Parent, xmlWriter, xamlTypeMapper); } } // Write columns related to the given table cell range. private static void WriteTableColumnsInformation(ITextRange range, Table table, XmlWriter xmlWriter, XamlTypeMapper xamlTypeMapper) { TableColumnCollection columns = table.Columns; int startColumn; int endColumn; if (!TextRangeEditTables.GetColumnRange(range, table, out startColumn, out endColumn)) { startColumn = 0; endColumn = columns.Count - 1; } Invariant.Assert(startColumn >= 0, "startColumn index is supposed to be non-negative"); if(columns.Count > 0) { // Build an appropriate name for the complex property string complexPropertyName = table.GetType().Name + ".Columns"; // Write the start element for the complex property. xmlWriter.WriteStartElement(complexPropertyName); for (int i = startColumn; i <= endColumn && i < columns.Count; i++) { WriteXamlAtomicElement(columns[i], xmlWriter, /*reduceElement:*/false); } // Close the element for the complex property xmlWriter.WriteEndElement(); } } ////// Creates a FlowDocument element wrapping copied content and storing its contextual properties. /// /// /// /// /// /// /// /// true means that we need to serialize the whole FlowDocument - used in FileSave scenario; /// false means that we are in copy-paste scenario and will use Section or Span as a root - depending on context. /// private static void WriteRootFlowDocument(ITextRange range, ITextPointer context, XmlWriter xmlWriter, XamlTypeMapper xamlTypeMapper, bool lastParagraphMustBeMerged, bool useFlowDocumentAsRoot) { Type rootType; const string xmlNamespace = "http://schemas.microsoft.com/winfx/2006/xaml/presentation"; const string xmlns = "xmlns"; // Decide what root element to use if (useFlowDocumentAsRoot) { rootType = typeof(FlowDocument); } else { Type contextType = context.ParentType; if (contextType == null || typeof(Paragraph).IsAssignableFrom(contextType) || typeof(Inline).IsAssignableFrom(contextType) && !typeof(AnchoredBlock).IsAssignableFrom(contextType)) { rootType = typeof(Span); } else { rootType = typeof(Section); } } // Create a root element FlowDocument xmlWriter.WriteStartElement(rootType.Name, xmlNamespace); // Define default namespace as Avalon namespace xmlWriter.WriteAttributeString(xmlns, xmlNamespace); // Set the value of xml:space to "preserve" to consider all spaces as significant // Note that Xml treats whitespaces as significant if they belong to some nonempty line // (neighbored by non-whitespace characters at least from one side) // That's why we only loose whitespaces if they occupy the whole textrun in xml. // So alternative solution for whitespace preservation could be setting xml:space="preserve" // attribute to only empty runs - this would make our whitespace preservation more // narrowed... // xmlWriter.WriteAttributeString("xml:space", "preserve"); // Write all contextual properties as attributes of root fragment DependencyObject complexProperties = new DependencyObject(); if (useFlowDocumentAsRoot) { WriteInheritablePropertiesForFlowDocument((DependencyObject)((TextPointer)context).Parent, xmlWriter, complexProperties); } else { WriteInheritableProperties(rootType, context, xmlWriter, /*onlyAffected:*/false, complexProperties); } if (rootType == typeof(Span)) { // Root element is not real element to paste. It is just a property bag for contextual properties. // So we collect non-inheritable properties only for inline content; not needing it for block one. WriteNoninheritableProperties(typeof(Span), context, xmlWriter, /*onlyAffected:*/false, complexProperties); } // Write an indicator that last paragraph must be merged on paste if (rootType == typeof(Section) && lastParagraphMustBeMerged) { xmlWriter.WriteAttributeString(Section.HasTrailingParagraphBreakOnPastePropertyName, "False"); } // Note that we are skipping background property, because we only want to transfer it for the whole document. // WriteComplexProperties(xmlWriter, complexProperties, rootType); } private static void WriteInheritablePropertiesForFlowDocument(DependencyObject context, XmlWriter xmlWriter, DependencyObject complexProperties) { DependencyProperty[] inheritableProperties = TextSchema.GetInheritableProperties(typeof(FlowDocument)); for (int i = 0; i < inheritableProperties.Length; i++) { DependencyProperty property = inheritableProperties[i]; object value = context.ReadLocalValue(property); if (value != DependencyProperty.UnsetValue) { string stringValue = DPTypeDescriptorContext.GetStringValue(property, value); if (stringValue != null) { stringValue = FilterNaNStringValueForDoublePropertyType(stringValue, property.PropertyType); string propertyName; if (property == FrameworkContentElement.LanguageProperty) { // Special case for CultureInfo property that must be represented in xaml as xml:lang attribute propertyName = "xml:lang"; } else { // Regular case using own property name propertyName = property.OwnerType == typeof(Typography) ? "Typography." + property.Name : property.Name; } xmlWriter.WriteAttributeString(propertyName, stringValue); } else { complexProperties.SetValue(property, value); } } } } // Writes a collection of attributes representing inheritable properties // whose values has been affected by this element. // Parameter onlyAffected=true means that we serialize only properties affected by // the current element; otherwise we output all known inheritable properties. private static void WriteInheritableProperties(Type elementTypeStandardized, ITextPointer context, XmlWriter xmlWriter, bool onlyAffected, DependencyObject complexProperties) { // Create a pointer positioned immediately outside the element ITextPointer outerContext = null; if (onlyAffected) { outerContext = context.CreatePointer(); outerContext.MoveToElementEdge(ElementEdge.BeforeStart); } DependencyProperty[] inheritableProperties = TextSchema.GetInheritableProperties(elementTypeStandardized); for (int i = 0; i < inheritableProperties.Length; i++) { DependencyProperty property = inheritableProperties[i]; object innerValue = context.GetValue(property); if (innerValue == null) { // Some properties like Foreground may have null as default value. // Skip them. continue; } object outerValue = null; if (onlyAffected) { outerValue = outerContext.GetValue(property); } // The property must appear in markup if the element if (!onlyAffected || // all properties requested for saving context on root !TextSchema.ValuesAreEqual(innerValue, outerValue)) // or the element really affects the property { string stringValue = DPTypeDescriptorContext.GetStringValue(property, innerValue); if (stringValue != null) { stringValue = FilterNaNStringValueForDoublePropertyType(stringValue, property.PropertyType); string propertyName; if (property == FrameworkContentElement.LanguageProperty) { // Special case for CultureInfo property that must be represented in xaml as xml:lang attribute propertyName = "xml:lang"; } else { // Regular case: serialize a property with its own name propertyName = GetPropertyNameForElement(property, elementTypeStandardized, /*forceComplexName:*/false); } xmlWriter.WriteAttributeString(propertyName, stringValue); } else { complexProperties.SetValue(property, innerValue); } } } } // Writes a collection of attributes representing non-inheritable properties // whose values are set inline on the given element instance. // When we read properties fromContext we want all values including defaults; from text elements we only want only affected private static void WriteNoninheritableProperties(Type elementTypeStandardized, ITextPointer context, XmlWriter xmlWriter, bool onlyAffected, DependencyObject complexProperties) { DependencyProperty[] elementProperties = TextSchema.GetNoninheritableProperties(elementTypeStandardized); // We'll need a pointer to walk the tree up when onlyAffected=false ITextPointer parentContext = onlyAffected ? null : context.CreatePointer(); for (int i = 0; i < elementProperties.Length; i++) { DependencyProperty property = elementProperties[i]; Type propertyOwnerType = context.ParentType; object propertyValue; if (onlyAffected) { // propertyValue = context.GetValue(property); } else { // This is request for contextual properties - use "manual" inheritance to collect values Invariant.Assert(elementTypeStandardized == typeof(Span), "Request for contextual properties is expected for Span wrapper only"); // Get property value from this element or from one of its ancestors (the latter in case of !onlyAffeted) propertyValue = context.GetValue(property); // Get property value from its ancestors if the property is not set. // TextDecorationCollection is special-cased as its default is empty collection, // and its value source cannot be distinguished from ITextPointer. if (propertyValue == null || TextDecorationCollection.Empty.ValueEquals(propertyValue as TextDecorationCollection)) { if (property == Inline.BaselineAlignmentProperty || property == TextElement.TextEffectsProperty) { // These properties do not make sense as contextual; do not include them into context. continue; } parentContext.MoveToPosition(context); while ((propertyValue == null || TextDecorationCollection.Empty.ValueEquals(propertyValue as TextDecorationCollection)) && typeof(Inline).IsAssignableFrom(parentContext.ParentType)) { parentContext.MoveToElementEdge(ElementEdge.BeforeStart); propertyValue = parentContext.GetValue(property); propertyOwnerType = parentContext.ParentType; } } } // if ((property == Block.MarginProperty && (typeof(Paragraph).IsAssignableFrom(propertyOwnerType) || typeof(List).IsAssignableFrom(propertyOwnerType))) || (property == Block.PaddingProperty) && typeof(List).IsAssignableFrom(propertyOwnerType)) { Thickness thickness = (Thickness)propertyValue; if (Paragraph.IsMarginAuto(thickness)) { continue; } } // Write the property as attribute string or add it to a list of complex properties. WriteNoninheritableProperty(xmlWriter, property, propertyValue, propertyOwnerType, onlyAffected, complexProperties, context.ReadLocalValue(property)); } } // Writes a value of an individual non-inheritable property in form of attribute string. // If the value cannot be serialized as a string, adds the property to a collection of complexProperties. // To minimize the amount of xaml produced, the property is skipped if its value is equal to its default value // for the given element type - the propertyOwnerType. // The flag onlyAffected=false means that we want to output all properties independently on // if they are equal to their default values or not. private static void WriteNoninheritableProperty(XmlWriter xmlWriter, DependencyProperty property, object propertyValue, Type propertyOwnerType, bool onlyAffected, DependencyObject complexProperties, object localValue) { bool write = false; if (propertyValue != null && propertyValue != DependencyProperty.UnsetValue) { if (!onlyAffected) { write = true; } else { PropertyMetadata metadata = property.GetMetadata(propertyOwnerType); write = (metadata == null) || !(TextSchema.ValuesAreEqual(propertyValue, /*defaultValue*/metadata.DefaultValue) && localValue == DependencyProperty.UnsetValue); } } if (write) { string stringValue = DPTypeDescriptorContext.GetStringValue(property, propertyValue); if (stringValue != null) { stringValue = FilterNaNStringValueForDoublePropertyType(stringValue, property.PropertyType); // For the property name in this case we safe to use simple name only; // as noninheritable property would never require TypeName.PropertyName notation // for attribute syntax. xmlWriter.WriteAttributeString(property.Name, stringValue); } else { complexProperties.SetValue(property, propertyValue); } } } // Writes a collection of attributes representing properties with local values set on them. // If the value cannot be serialized as a string, adds the property to a collection of complexProperties. private static void WriteLocallySetProperties(Type elementTypeStandardized, ITextPointer context, XmlWriter xmlWriter, DependencyObject complexProperties) { TextPointer textPointer = context as TextPointer; if (textPointer == null) { // We can't have custom properties if we're not a TextPointer return; } LocalValueEnumerator locallySetProperties = context.GetLocalValueEnumerator(); DependencyProperty[] inheritableProperties = TextSchema.GetInheritableProperties(elementTypeStandardized); DependencyProperty[] nonInheritableProperties = TextSchema.GetNoninheritableProperties(elementTypeStandardized); while (locallySetProperties.MoveNext()) { DependencyProperty locallySetProperty = (DependencyProperty)locallySetProperties.Current.Property; // Don't serialize read-only properties, or any properties registered or owned by a // a class in the framework (we only want to serialize custom properties), to be // consistent with our behavior for non-custom inlines. // if (!locallySetProperty.ReadOnly && !IsPropertyKnown(locallySetProperty, inheritableProperties, nonInheritableProperties) && !TextSchema.IsKnownType(locallySetProperty.OwnerType)) { object propertyValue = context.ReadLocalValue(locallySetProperty); string stringValue = DPTypeDescriptorContext.GetStringValue(locallySetProperty, propertyValue); if (stringValue != null) { stringValue = FilterNaNStringValueForDoublePropertyType(stringValue, locallySetProperty.PropertyType); string propertyName = GetPropertyNameForElement(locallySetProperty, elementTypeStandardized, /*forceComplexName:*/false); xmlWriter.WriteAttributeString(propertyName, stringValue); } else { complexProperties.SetValue(locallySetProperty, propertyValue); } } } // *** WE NEED TO BETTER UNDERSTAND THE IMPLICATIONS OF SERIALIZING NON-DP CLR PROPERTIES, SO THE REST OF // *** THIS METHOD IS DISABLED UNTIL WE DECIDE THE BEST WAY TO HANDLE THEM. // *** CLRTypeDescriptorContext is essentially the same as DPTypeDescriptorContext. #if false // Check all CLR properties // Note that this is partially redundant. TypeDescriptor.GetProperties, when called on a // DependencyObject, will return all properties that are set-- including all those already // serialized as Inheritable, NonInheritable, or LocallySet properties. A potential // optimization, therefore, is to remove those serialization methods and simply use this one // for everything when we've opted into custom element serialization. PropertyDescriptorCollection descriptorCollection = TypeDescriptor.GetProperties(textPointer.Parent); IEnumerator descriptors = descriptorCollection.GetEnumerator(); while (descriptors.MoveNext()) { PropertyDescriptor current = (PropertyDescriptor)descriptors.Current; // ShouldSerializeValue() will return true for readonly properties that have explicitly // been told to serialize, such as Span.Inlines. If we serialize a read-only property, // however, the parser will throw an exception when we try to deserialize. So we // explicitly skip all read-only properties, and all DPs. if (!current.ShouldSerializeValue(textPointer.Parent) || current.IsReadOnly || current is MS.Internal.ComponentModel.DependencyObjectPropertyDescriptor) { continue; } // Serialize the property object propertyValue = current.GetValue(textPointer.Parent); if (propertyValue != null) { string stringValue = CLRTypeDescriptorContext.GetStringValue(current, propertyValue); if (stringValue != null) { stringValue = FilterNaNStringValueForDoublePropertyType(stringValue, current.PropertyType); xmlWriter.WriteAttributeString(current.Name, stringValue); } else { // } } } #endif } private static bool IsPropertyKnown(DependencyProperty propertyToTest, DependencyProperty[] inheritableProperties, DependencyProperty[] nonInheritableProperties) { for (int i = 0; i < inheritableProperties.Length; i++) { DependencyProperty property = inheritableProperties[i]; if (property == propertyToTest) { return true; } } for (int i = 0; i < nonInheritableProperties.Length; i++) { DependencyProperty property = nonInheritableProperties[i]; if (property == propertyToTest) { return true; } } return false; } ////// Writes complex properties in form of child elements with compound names /// ////// To mask the security exception from XamlWriter.Save in partial trust case, /// this function checks if the current call stack has unmanaged code permission. /// private static void WriteComplexProperties(XmlWriter xmlWriter, DependencyObject complexProperties, Type elementType) { if (!SecurityHelper.CheckUnmanagedCodePermission()) { // In partial trust, we cannot serialize any complex properties because // XamlWriter.Save demands UnmanagedCodePermission. // // If we're in PT, drop the properties. // // If you're here debugging a lost complex property, consider adding // code to DPTypeDescriptorContext to convert the complex property // into a non-complex property, or consider modifying XamlWriter.Save. return; } LocalValueEnumerator properties = complexProperties.GetLocalValueEnumerator(); properties.Reset(); while (properties.MoveNext()) { LocalValueEntry propertyEntry = properties.Current; // Build an appropriate name for the complex property string complexPropertyName = GetPropertyNameForElement(propertyEntry.Property, elementType, /*forceComplexName:*/true); // Write the start element for the complex property. xmlWriter.WriteStartElement(complexPropertyName); // Serialize the complex property value from SaveAsXml(). string complexPropertyXml = System.Windows.Markup.XamlWriter.Save(propertyEntry.Value); // Write the serialized complext property value as Xml. xmlWriter.WriteRaw(complexPropertyXml); // Close the element for the complex property xmlWriter.WriteEndElement(); } } // Creates a name for the property which is consistent with xaml parser logic // When forceComplexName=true produces the TypeName.PropertyName notation unconditionally, // otherwise such complex name is produced only when the TypeName is different from elementType.Name. private static string GetPropertyNameForElement(DependencyProperty property, Type elementType, bool forceComplexName) { string propertyName; if (DependencyProperty.FromName(property.Name, elementType) == property) { // The elementType is an owner of this property, so we can use its name if (forceComplexName) { propertyName = elementType.Name + "." + property.Name; } else { propertyName = property.Name; } } else { // The elementType does not own this property, so we use the property's registered owner type name. propertyName = property.OwnerType.Name + "." + property.Name; } return propertyName; } // Serializes an element assuming that it does not have any children. Used for TableColumn // private static void WriteXamlAtomicElement(DependencyObject element, XmlWriter xmlWriter, bool reduceElement) { Type elementTypeStandardized = TextSchema.GetStandardElementType(element.GetType(), reduceElement); DependencyProperty[] elementProperties = TextSchema.GetNoninheritableProperties(elementTypeStandardized); xmlWriter.WriteStartElement(elementTypeStandardized.Name); for (int i = 0; i < elementProperties.Length; i++) { DependencyProperty property = elementProperties[i]; object propertyValue = element.ReadLocalValue(property); if (propertyValue != null && propertyValue != DependencyProperty.UnsetValue) { System.ComponentModel.TypeConverter typeConverter = System.ComponentModel.TypeDescriptor.GetConverter(property.PropertyType); Invariant.Assert(typeConverter != null, "typeConverter==null: is not expected for atomic elements"); Invariant.Assert(typeConverter.CanConvertTo(typeof(string)), "type is expected to be convertable into string type"); string stringValue = (string)typeConverter.ConvertTo(/*ITypeDescriptorContext:*/null, CultureInfo.InvariantCulture, propertyValue, typeof(string)); Invariant.Assert(stringValue != null, "expecting non-null stringValue"); xmlWriter.WriteAttributeString(property.Name, stringValue); } } xmlWriter.WriteEndElement(); } ////// Writes embeded object tag. /// /// /// /// /// XmlWriter to output element opening tag. /// /// /// private static void WriteEmbeddedObject(object embeddedObject, XmlWriter xmlWriter, WpfPayload wpfPayload) { if (wpfPayload != null && embeddedObject is Image) { // Writing in WPF mode: need to create an image with a Source referring into a package Image image = (Image)embeddedObject; if (image.Source != null && !string.IsNullOrEmpty(image.Source.ToString())) { // Add the image to the Image collection in the package // and define the reference to image into the package string imageSource = wpfPayload.AddImage(image); if (imageSource != null) { Type elementTypeStandardized = typeof(Image); // Write opening tag for the element xmlWriter.WriteStartElement(elementTypeStandardized.Name); // Write all properties except for Source DependencyProperty[] imageProperties = TextSchema.ImageProperties; DependencyObject complexProperties = new DependencyObject(); for (int i = 0; i < imageProperties.Length; i++) { DependencyProperty property = imageProperties[i]; if (property != Image.SourceProperty) { object value = image.GetValue(property); // Write the property as attribute string or add it to a list of complex properties. WriteNoninheritableProperty(xmlWriter, property, value, elementTypeStandardized, /*onlyAffected:*/true, complexProperties, image.ReadLocalValue(property)); } } // Write Source property - as a local reference into the package container // Write Source property as the complex property to specify the BitmapImage // cache option as "OnLoad" instead of the default "OnDeman". Otherwise, // we couldn't load the image by disposing WpfPayload package. xmlWriter.WriteStartElement(typeof(Image).Name + "." + Image.SourceProperty.Name); xmlWriter.WriteStartElement(typeof(System.Windows.Media.Imaging.BitmapImage).Name); xmlWriter.WriteAttributeString(System.Windows.Media.Imaging.BitmapImage.UriSourceProperty.Name, imageSource); xmlWriter.WriteAttributeString(System.Windows.Media.Imaging.BitmapImage.CacheOptionProperty.Name, "OnLoad"); xmlWriter.WriteEndElement(); xmlWriter.WriteEndElement(); // Write remaining complex properties WriteComplexProperties(xmlWriter, complexProperties, elementTypeStandardized); // Close the element xmlWriter.WriteEndElement(); } } } else { // In non-package mode we ignore all UIElements. // Output a space replacing this embedded element. // Note that in this mode (DataFormats.Xaml) InlineUIContainer was // replaced by Run and BlockUIContainer - by Paragraph, // so the space output here will be significant. xmlWriter.WriteString(" "); } } // ............................................................. // // Pasting // // ............................................................. // Handles a special case for pasting a single embedded element - // needs to choose between BlockUIContainer and InlineUIContainer. private static bool PasteSingleEmbeddedElement(TextRange range, TextElement fragment) { if (fragment.ContentStart.GetOffsetToPosition(fragment.ContentEnd) == 3) { TextElement uiContainer = fragment.ContentStart.GetAdjacentElement(LogicalDirection.Forward) as TextElement; FrameworkElement embeddedElement = null; if (uiContainer is BlockUIContainer) { embeddedElement = ((BlockUIContainer)uiContainer).Child as FrameworkElement; if (embeddedElement != null) { ((BlockUIContainer)uiContainer).Child = null; } } else if (uiContainer is InlineUIContainer) { embeddedElement = ((InlineUIContainer)uiContainer).Child as FrameworkElement; if (embeddedElement != null) { ((InlineUIContainer)uiContainer).Child = null; } } if (embeddedElement != null) { range.InsertEmbeddedUIElement(embeddedElement); return true; } } return false; } private static void PasteTextFragment(TextElement fragment, TextRange range) { Invariant.Assert(range.IsEmpty, "range must be empty at this point - emptied by a caller"); Invariant.Assert(fragment is Section || fragment is Span, "The wrapper element must be a Section or Span"); // Define insertion position. TextPointer insertionPosition = TextRangeEditTables.EnsureInsertionPosition(range.End); // Check if our insertion position has a non-splittable Inline ancestor such as Hyperlink element. // Since we cannot split such Inline, we must switch to Text mode for pasting. // Note that this also has the side effect of converting paragraph breaks to space characters. if (insertionPosition.HasNonMergeableInlineAncestor) { PasteNonMergeableTextFragment(fragment, range); } else { PasteMergeableTextFragment(fragment, range, insertionPosition); } } // Helper for PasteTextFragment private static void PasteNonMergeableTextFragment(TextElement fragment, TextRange range) { // We cannot split Hyperlink or other non-splittable inline ancestor. // Paste text content of fragment in such case. // Get text content to be pasted. string fragmentText = TextRangeBase.GetTextInternal(fragment.ElementStart, fragment.ElementEnd); // Paste text into our empty target range. // range.Text = fragmentText; // Select pasted content range.Select(range.Start, range.End); } // Helper for PasteTextFragment private static void PasteMergeableTextFragment(TextElement fragment, TextRange range, TextPointer insertionPosition) { TextPointer fragmentStart; TextPointer fragmentEnd; if (fragment is Span) { // Split structure at insertion point in target insertionPosition = TextRangeEdit.SplitFormattingElements(insertionPosition, /*keepEmptyFormatting:*/false); Invariant.Assert(insertionPosition.Parent is Paragraph, "insertionPosition must be in a scope of a Paragraph after splitting formatting elements"); // Move the whole Span into the insertion point fragment.RepositionWithContent(insertionPosition); // Store edge positions of inserted content fragmentStart = fragment.ElementStart; fragmentEnd = fragment.ElementEnd; // Remove wrapper from a tree fragment.Reposition(null, null); ValidateMergingPositions(typeof(Inline), fragmentStart, fragmentEnd); // Transfer inheritable contextual properties ApplyContextualProperties(fragmentStart, fragmentEnd, fragment); } else { // Correct leading nested List elements in the fragment CorrectLeadingNestedLists((Section)fragment); // Split a paragraph at insertion position bool needFirstParagraphMerging = SplitParagraphForPasting(ref insertionPosition); // Move the whole Section into the insertion point fragment.RepositionWithContent(insertionPosition); // Store edge positions of inserted content fragmentStart = fragment.ElementStart; fragmentEnd = fragment.ElementEnd.GetPositionAtOffset(0, LogicalDirection.Forward); // need forward orientation to stick with the following content during merge at fragmentStart position // And unwrap the root Section fragment.Reposition(null, null); ValidateMergingPositions(typeof(Block), fragmentStart, fragmentEnd); // Transfer inheritable contextual properties ApplyContextualProperties(fragmentStart, fragmentEnd, fragment); // Merge paragraphs on fragment boundaries if (needFirstParagraphMerging) { MergeParagraphsAtPosition(fragmentStart, /*mergingOnFragmentStart:*/true); } // Get an indication that we need to merge last paragraph if (!((Section)fragment).HasTrailingParagraphBreakOnPaste) { MergeParagraphsAtPosition(fragmentEnd, /*mergingOnFragmentStart:*/false); } } // // For paragraph pasting move range end to the following paragraph, because // it must include an ending paragraph break (in case of no-merging) if (fragment is Section && ((Section)fragment).HasTrailingParagraphBreakOnPaste) { fragmentEnd = fragmentEnd.GetInsertionPosition(LogicalDirection.Forward); } // Select pasted content range.Select(fragmentStart, fragmentEnd); } // Removes nested ListItems in the beginning of a fragment // to avoid multiple bulleting. // private static void CorrectLeadingNestedLists(Section fragment) { List list = fragment.Blocks.FirstBlock as List; while (list != null) { ListItem listItem = list.ListItems.FirstListItem; if (listItem == null) { return; } if (listItem.NextListItem != null) { return; } List nestedList = listItem.Blocks.FirstBlock as List; if (nestedList == null) { return; } // So we have nested list in the very beginning of the outer single-item list: // remove that outer list listItem.Reposition(null, null); list.Reposition(null, null); list = nestedList; } } // Decides whether we need to split a paragraph before pasting a fragment or not. // Splits the paragraph if needed, or simply moves the insertionPosition before its start. // Returns true if splitting happened and consequently merging is required after pasting. private static bool SplitParagraphForPasting(ref TextPointer insertionPosition) { bool needFirstParagraphMerging = true; // we need splitting unless the position os at the bery beginniong of a paragraph // When the insertion position is at the beginning of a paragraph we can avoid // splitting and then merging paragraphs at fragment start position. // This is not a pref consideration. We do not want an empty paragraph // would kill a formatting of a first pasted paragraphs (say, ListItem of a pasted List). TextPointer positionBeforeParagraph = insertionPosition; // Skip formatting tags while (positionBeforeParagraph.GetPointerContext(LogicalDirection.Backward) == TextPointerContext.ElementStart && TextSchema.IsFormattingType(positionBeforeParagraph.Parent.GetType())) { positionBeforeParagraph = positionBeforeParagraph.GetNextContextPosition(LogicalDirection.Backward); } while (positionBeforeParagraph.GetPointerContext(LogicalDirection.Backward) == TextPointerContext.ElementStart && TextSchema.AllowsParagraphMerging(positionBeforeParagraph.Parent.GetType())) { needFirstParagraphMerging = false; positionBeforeParagraph = positionBeforeParagraph.GetNextContextPosition(LogicalDirection.Backward); } if (!needFirstParagraphMerging) { // Insertion position was in the beginning of a paragraph. // No need in splitting/merging at fragment start insertionPosition = positionBeforeParagraph; } else { // split paragraph to create an insertion positionn at block level insertionPosition = TextRangeEdit.InsertParagraphBreak(insertionPosition, /*moveIntoSecondParagraph:*/false); } // When insertionPosition is inside a ListItem, then InsertParagraphBreak will // split not only a parent Paragraph but also a ListItem and return a position // between ListItems. This position is not good for inserting Block elements, // so we also need to split parent List element. // In a case when insertionPosition was at the beginning of a paragraph, // we still can end up being between ListItems, so again need to split a parent List. if (insertionPosition.Parent is List) { insertionPosition = TextRangeEdit.SplitElement(insertionPosition); } return needFirstParagraphMerging; } // Merges two paragraphs preceding and following the given position private static void MergeParagraphsAtPosition(TextPointer position, bool mergingOnFragmentStart) { TextPointer navigator = position; while (navigator != null && !(navigator.Parent is Paragraph)) { if (navigator.GetPointerContext(LogicalDirection.Backward) == TextPointerContext.ElementEnd) { navigator = navigator.GetNextContextPosition(LogicalDirection.Backward); } else { navigator = null; } } if (navigator != null) { Invariant.Assert(navigator.Parent is Paragraph, "We suppose have a first paragraph found"); Paragraph firstParagraph = (Paragraph)navigator.Parent; navigator = position; while (navigator != null && !(navigator.Parent is Paragraph)) { if (navigator.GetPointerContext(LogicalDirection.Forward) == TextPointerContext.ElementStart) { navigator = navigator.GetNextContextPosition(LogicalDirection.Forward); } else { navigator = null; } } if (navigator != null) { Invariant.Assert(navigator.Parent is Paragraph, "We suppose a second paragraph found"); Paragraph secondParagraph = (Paragraph)navigator.Parent; if (TextRangeEditLists.ParagraphsAreMergeable(firstParagraph, secondParagraph)) { TextRangeEditLists.MergeParagraphs(firstParagraph, secondParagraph); } else if (mergingOnFragmentStart && firstParagraph.TextRange.IsEmpty) { firstParagraph.RepositionWithContent(null); } else if (!mergingOnFragmentStart && secondParagraph.TextRange.IsEmpty) { secondParagraph.RepositionWithContent(null); } } } } // Validates that the sibling element at this position belong to expected itemType (Inline, Block, ListItem) private static void ValidateMergingPositions(Type itemType, TextPointer start, TextPointer end) { if (start.CompareTo(end) < 0) { // Verify inner part TextPointerContext forwardFromStart = start.GetPointerContext(LogicalDirection.Forward); TextPointerContext backwardFromEnd = end.GetPointerContext(LogicalDirection.Backward); Invariant.Assert(forwardFromStart == TextPointerContext.ElementStart, "Expecting first opening tag of pasted fragment"); Invariant.Assert(backwardFromEnd == TextPointerContext.ElementEnd, "Expecting last closing tag of pasted fragment"); Invariant.Assert(itemType.IsAssignableFrom(start.GetAdjacentElement(LogicalDirection.Forward).GetType()), "The first pasted fragment item is expected to be a " + itemType.Name); Invariant.Assert(itemType.IsAssignableFrom(end.GetAdjacentElement(LogicalDirection.Backward).GetType()), "The last pasted fragment item is expected to be a " + itemType.Name); // Veryfy outer part TextPointerContext backwardFromStart = start.GetPointerContext(LogicalDirection.Backward); TextPointerContext forwardFromEnd = end.GetPointerContext(LogicalDirection.Forward); Invariant.Assert(backwardFromStart == TextPointerContext.ElementStart || backwardFromStart == TextPointerContext.ElementEnd || backwardFromStart == TextPointerContext.None, "Bad context preceding a pasted fragment"); Invariant.Assert(!(backwardFromStart == TextPointerContext.ElementEnd) || itemType.IsAssignableFrom(start.GetAdjacentElement(LogicalDirection.Backward).GetType()), "An element preceding a pasted fragment is expected to be a " + itemType.Name); Invariant.Assert(forwardFromEnd == TextPointerContext.ElementStart || forwardFromEnd == TextPointerContext.ElementEnd || forwardFromEnd == TextPointerContext.None, "Bad context following a pasted fragment"); Invariant.Assert(!(forwardFromEnd == TextPointerContext.ElementStart) || itemType.IsAssignableFrom(end.GetAdjacentElement(LogicalDirection.Forward).GetType()), "An element following a pasted fragment is expected to be a " + itemType.Name); } } // Helper function used to set default value for an indicator requesting to merge last paragraph. private static void AdjustFragmentForTargetRange(TextElement fragment, TextRange range) { if (fragment is Section && ((Section)fragment).HasTrailingParagraphBreakOnPaste) { // Explicit indicator is missing, we need to set it by default. // In a case of TextRange.Xml property assignment we assume that // user expects to insert as many paragraphs new paragraphs as her pasted xaml contains. // The expection must be done to the case when the target range is // extended beyond the last paragraph - then we must merge last paragraph // to avoid extra paragraph creation at the end (one additional paragraph // will be created in this case by Pasting code before pasting). // The other case for exception is when target TextContainer is empty - // in this case we as well want to merge last paragraph with the following // one (which will be created as part of paragraph enforcement in pasting operation). // The both desired conditions - IsAfterLastParagraph and "in empty container" // can be identified by the following simple test - range.End is not at end-of-doc. ((Section)fragment).HasTrailingParagraphBreakOnPaste = range.End.GetPointerContext(LogicalDirection.Forward) != TextPointerContext.None; } } // Applies a whole property bag to a range from start to end to simulate inheritance of this property from source conntext private static void ApplyContextualProperties(TextPointer start, TextPointer end, TextElement propertyBag) { Invariant.Assert(propertyBag.IsEmpty && propertyBag.Parent == null, "propertyBag is supposed to be an empty element outside any tree"); LocalValueEnumerator contextualProperties = propertyBag.GetLocalValueEnumerator(); while (start.CompareTo(end) < 0 && contextualProperties.MoveNext()) { // Note: we repeatedly check for IsEmpty because the selection // may become empty as a result of normalization after formatting // (thai character sequence). LocalValueEntry propertyEntry = contextualProperties.Current; DependencyProperty property = propertyEntry.Property; if (TextSchema.IsCharacterProperty(property) && TextSchema.IsParagraphProperty(property)) { // In case a property is both an Inline and Paragraph property, // propertyBag element type (section or span) decides how it should be applied. if (TextSchema.IsBlock(propertyBag.GetType())) { ApplyContextualProperty(typeof(Block), start, end, property, propertyEntry.Value); } else { ApplyContextualProperty(typeof(Inline), start, end, property, propertyEntry.Value); } } else if (TextSchema.IsCharacterProperty(property)) { ApplyContextualProperty(typeof(Inline), start, end, property, propertyEntry.Value); } else if (TextSchema.IsParagraphProperty(property)) { ApplyContextualProperty(typeof(Block), start, end, property, propertyEntry.Value); } } // Merge formatting elements at end position TextRangeEdit.MergeFormattingInlines(start); TextRangeEdit.MergeFormattingInlines(end); } // Applies one property to a range from start to end to simulate inheritance of this property from source conntext private static void ApplyContextualProperty(Type targetType, TextPointer start, TextPointer end, DependencyProperty property, object value) { if (TextSchema.ValuesAreEqual(start.Parent.GetValue(property), value)) { return; // The property at insertion position is the same as it was in source context. Nothing to do. } // Advance start pointer to enter pasted fragment start = start.GetNextContextPosition(LogicalDirection.Forward); while (start != null && start.CompareTo(end) < 0) { TextPointerContext passedContext = start.GetPointerContext(LogicalDirection.Backward); if (passedContext == TextPointerContext.ElementStart) { TextElement element = (TextElement)start.Parent; // Check if this element affects the property in question if (element.ReadLocalValue(property) != DependencyProperty.UnsetValue || !TextSchema.ValuesAreEqual(element.GetValue(property), element.Parent.GetValue(property))) { // The element affects this property, so we can skip it start = element.ElementEnd; } else if (targetType.IsAssignableFrom(element.GetType())) { start = element.ElementEnd; if (targetType == typeof(Block) && start.CompareTo(end) > 0) { // Contextual properties should not apply to the last paragraph // when it is merged with the following content - // to avoid affecting the folowing visible content formatting. break; } // This is topmost-level inline element which inherits this property. // Set the value explicitly if (!TextSchema.ValuesAreEqual(value, element.GetValue(property))) { element.ClearValue(property); if (!TextSchema.ValuesAreEqual(value, element.GetValue(property))) { element.SetValue(property, value); } TextRangeEdit.MergeFormattingInlines(element.ElementStart); } } else { // Traverse down into a structured (non-innline) element start = start.GetNextContextPosition(LogicalDirection.Forward); } } else { // Traverse up from any element Invariant.Assert(passedContext != TextPointerContext.None, "TextPointerContext.None is not expected"); start = start.GetNextContextPosition(LogicalDirection.Forward); } } } // Returns a navigator scoped in the common ancestor for the start and end positions // The navigator is positioned in the beginning of the ancestor's content. // Modifies the common ancestor for hyperlink serialization heuristic - in case when the range is positioned at hyperlink boundaries. // Since we need to write a hyperlink wrapper in this case, navigator is positioned before hyperlink element start. private static ITextPointer FindSerializationCommonAncestor(ITextRange range) { // Create navigators for tree traversing looking for commonAncestor ITextPointer commonAncestor = range.Start.CreatePointer(); ITextPointer runningEnd = range.End.CreatePointer(); // Find nearest common ancestor while (!commonAncestor.HasEqualScope(runningEnd)) { // Run all way from end up the tree to check if start is ancestor runningEnd.MoveToPosition(range.End); while (typeof(TextElement).IsAssignableFrom(runningEnd.ParentType) && !runningEnd.HasEqualScope(commonAncestor)) { runningEnd.MoveToElementEdge(ElementEdge.AfterEnd); } if (runningEnd.HasEqualScope(commonAncestor)) { break; } // Move start one level up commonAncestor.MoveToElementEdge(ElementEdge.BeforeStart); } while (!IsAcceptableAncestor(commonAncestor, range)) { commonAncestor.MoveToElementEdge(ElementEdge.BeforeStart); } if (typeof(TextElement).IsAssignableFrom(commonAncestor.ParentType)) { commonAncestor.MoveToElementEdge(ElementEdge.AfterStart); // Check for special case, when range start and end are at Hyperlink boundaries. // Need to expand commonAncestor to Hyperlink element start, so that Hyperlink wrapper is written. ITextPointer hyperlinkStart = GetHyperlinkStart(range); if (hyperlinkStart != null) { commonAncestor = hyperlinkStart; } } else { commonAncestor.MoveToPosition(commonAncestor.TextContainer.Start); } return commonAncestor; } // Verify that a pointer is an acceptable ancestor. Some types can't be ancestors at all, while // non-typographic-only elements are unacceptable if the range being serialized does not include the // element's start and end (because we don't want to serialize properties on such an element). private static bool IsAcceptableAncestor(ITextPointer commonAncestor, ITextRange range) { if (typeof(TableRow).IsAssignableFrom(commonAncestor.ParentType) || typeof(TableRowGroup).IsAssignableFrom(commonAncestor.ParentType) || typeof(Table).IsAssignableFrom(commonAncestor.ParentType) || typeof(BlockUIContainer).IsAssignableFrom(commonAncestor.ParentType) || typeof(List).IsAssignableFrom(commonAncestor.ParentType) || typeof(Inline).IsAssignableFrom(commonAncestor.ParentType) && TextSchema.HasTextDecorations(commonAncestor.GetValue(Inline.TextDecorationsProperty))) { return false; } // We don't want to use any formatting from within a non-typographic-only element unless the entire // element is selected (in which case, the ancestor candidate will already be outside that element. // If there is such an element ANYWHERE in the ancestry, the only acceptable // ancestor is outside the outermost such element. ITextPointer navigator = commonAncestor.CreatePointer(); while (typeof(TextElement).IsAssignableFrom(navigator.ParentType)) { TextElementEditingBehaviorAttribute behaviorAttribute = (TextElementEditingBehaviorAttribute)Attribute.GetCustomAttribute(navigator.ParentType, typeof(TextElementEditingBehaviorAttribute)); if (behaviorAttribute != null && !behaviorAttribute.IsTypographicOnly) { return false; } navigator.MoveToElementEdge(ElementEdge.BeforeStart); } return true; } // Removes (inplace) any invalid surrogate chars from an array. // Returns the new array length, text.Length minus any stripped // chars. // // Unicode surrogates are 32 bit references to abstract chars. // A valid surrogate pair consists of a 16 bit code point (the // high surrogate) in the range u+d800 - u+dbff, followed by a // second 16 bit code point (the low surrogate) in the range // u+dc00 - u+dfff. // // An invalid surrogate is a high surrogate not followed by a // low surrogate, or a low surrogate not preceeded by a high // surrogate. // // Length specifies the number of characters in text to examine, // characters past length are ignored (as if text.Length == length). // // Also removes nul characters private static int StripInvalidSurrogateChars(char[] text, int length) { int count; Invariant.Assert(text.Length >= length, "Asserting that text.Length >= length"); int i; for (i = 0; i < length; i++) { char testChar = text[i]; if (Char.IsHighSurrogate(testChar) || Char.IsLowSurrogate(testChar) || IsBadCode(testChar)) { break; } } if (i == length) { // No surrogates, early out. count = length; } else { count = i; for (; i < length; i++) { if (Char.IsHighSurrogate(text[i])) { if (i + 1 < length && Char.IsLowSurrogate(text[i + 1])) { // Valid surrogate encountered. text[count] = text[i]; text[count + 1] = text[i + 1]; count += 2; i++; // Skip over low surrogate. } else { // Bogus high surrogte encountered -- remove it. // Simply don't update destinationIndex or count. } } else if (Char.IsLowSurrogate(text[i])) { // Bogus low surrogate encountered -- remove it. // Simply don't update destinationIndex or count. } else if (IsBadCode(text[i])) { // nul character enountered - remove it. // Simply don't update destinationIndex or count. // } else { // Non-surrogate encountered. text[count] = text[i]; count += 1; } } } return count; } private static bool IsBadCode(char code) { return (code < ' ' && code != '\x0009' && code != '\x000A' && code != '\x000D'); } ////// Return true if rangeEnd is not at the end of an element. /// /// textReader must already be at the start of the element. /// private static bool IsPartialNonTypographic(ITextPointer textReader, ITextPointer rangeEnd) { bool isPartial = false; Invariant.Assert(textReader.GetPointerContext(LogicalDirection.Forward) == TextPointerContext.ElementStart); ITextPointer elementNavigation = textReader.CreatePointer(); ITextPointer elementEnd = textReader.CreatePointer(); elementEnd.MoveToNextContextPosition(LogicalDirection.Forward); // Find the end position elementEnd.MoveToElementEdge(ElementEdge.AfterEnd); if (elementEnd.CompareTo(rangeEnd) > 0) { isPartial = true; } return isPartial; } ////// Return true if Hyperlink range is invalid. /// Hyperlink is invalid if it include a UiElement except Image or the range end position /// is stated before the end position of hyperlink. /// This must be called before Hyperlink start element position. /// private static bool IsHyperlinkInvalid(ITextPointer textReader, ITextPointer rangeEnd) { // TextRead must be on the position before the element start position of Hyperlink Invariant.Assert(textReader.GetPointerContext(LogicalDirection.Forward) == TextPointerContext.ElementStart); Invariant.Assert(typeof(Hyperlink).IsAssignableFrom(textReader.GetElementType(LogicalDirection.Forward))); bool hyperlinkInvalid = false; // Get the forward adjacent element and cast Hyperlink hardly since it must be Hyperlink Hyperlink hyperlink = (Hyperlink)textReader.GetAdjacentElement(LogicalDirection.Forward); ITextPointer hyperlinkNavigation = textReader.CreatePointer(); ITextPointer hyperlinkEnd = textReader.CreatePointer(); hyperlinkEnd.MoveToNextContextPosition(LogicalDirection.Forward); // Find the hyperlink end position hyperlinkEnd.MoveToElementEdge(ElementEdge.AfterEnd); // Hyperlink end position is stated after the range end position. if (hyperlinkEnd.CompareTo(rangeEnd) > 0) { hyperlinkInvalid = true; } else { // Check whether the hyperlink having a UiElement except Image until hyperlink end position while (hyperlinkNavigation.CompareTo(hyperlinkEnd) < 0) { InlineUIContainer inlineUIContainer = hyperlinkNavigation.GetAdjacentElement(LogicalDirection.Forward) as InlineUIContainer; if (inlineUIContainer != null && !(inlineUIContainer.Child is Image)) { hyperlinkInvalid = true; break; } hyperlinkNavigation.MoveToNextContextPosition(LogicalDirection.Forward); } } return hyperlinkInvalid; } // Returns a position before hyperlink element start if passed range start and end are at hyperlink boundaries. Otherwise, null. private static ITextPointer GetHyperlinkStart(ITextRange range) { ITextPointer hyperlinkStart = null; if (TextPointerBase.IsAtNonMergeableInlineStart(range.Start) && TextPointerBase.IsAtNonMergeableInlineEnd(range.End)) { // Find a position at hyperlink start. hyperlinkStart = range.Start.CreatePointer(LogicalDirection.Forward); while (hyperlinkStart.GetPointerContext(LogicalDirection.Backward) == TextPointerContext.ElementStart && !typeof(Hyperlink).IsAssignableFrom(hyperlinkStart.ParentType)) { hyperlinkStart.MoveToElementEdge(ElementEdge.BeforeStart); } hyperlinkStart.MoveToElementEdge(ElementEdge.BeforeStart); hyperlinkStart.Freeze(); } return hyperlinkStart; } // private static string FilterNaNStringValueForDoublePropertyType(string stringValue, Type propertyType) { if (propertyType == typeof(double) && String.Compare(stringValue, "NaN", StringComparison.OrdinalIgnoreCase) == 0) { return "Auto"; // convert NaN to Auto, to keep parser happy } return stringValue; } #endregion Private Methods // -------------------------------------------------------------- // // Private Constants // // -------------------------------------------------------------- #region Private Constants // A structural depth of a empty FlowDocument fragment private const int EmptyDocumentDepth = 1; #endregion Private Constants } } // File provided for Reference Use Only by Microsoft Corporation (c) 2007. // Copyright (c) Microsoft Corporation. All rights reserved. //---------------------------------------------------------------------------- // // File: TextRangeSerialization.cs // // Copyright (C) Microsoft Corporation. All rights reserved. // // Description: Set of static methods implementing text range serialization // //--------------------------------------------------------------------------- namespace System.Windows.Documents { using MS.Internal; using System.Text; using System.Xml; using System.IO; using System.Windows.Markup; // TypeConvertContext, ParserContext using System.Windows.Controls; using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Globalization; using System.Security; ////// TextRangeSerialization is a static class containing /// an implementation for TextRange serialization functionality. /// It is only used from TextRange.GetXml/AppendXml methods. /// internal static class TextRangeSerialization { // ------------------------------------------------------------- // // Internal Methods // // ------------------------------------------------------------- #region Internal Methods internal static void WriteXaml(XmlWriter xmlWriter, ITextRange range, bool useFlowDocumentAsRoot, WpfPayload wpfPayload) { WriteXaml(xmlWriter, range, useFlowDocumentAsRoot, wpfPayload, false); } ////// Writes a content of current range in form of valid xml. /// Places an artificial element xaml:FlowDocument as a root of output xml. /// /// /// XmlWriter to which the range will be serialized /// /// /// TextRange whose content is copied into XmlWriter xmlWriter. /// /// /// true means that we need to serialize the whole FlowDocument - used in FileSave scenario; /// false means that we are in copy-paste scenario and will use Section or Span as a root - depending on context. /// /// /// When this parameter is not null, images are serialized. When null, images are stripped out. /// /// /// When TRUE, TextElements are serialized as-is. When FALSE, they're upcast to their base type. /// internal static void WriteXaml(XmlWriter xmlWriter, ITextRange range, bool useFlowDocumentAsRoot, WpfPayload wpfPayload, bool preserveTextElements) { // Set unindented formatting to avoid inserting insignificant whitespaces as significant ones Formatting saveWriterFormatting = Formatting.None; if (xmlWriter is XmlTextWriter) { saveWriterFormatting = ((XmlTextWriter)xmlWriter).Formatting; ((XmlTextWriter)xmlWriter).Formatting = Formatting.None; } // Get the default xamlTypeMapper. XamlTypeMapper xamlTypeMapper = XmlParserDefaults.DefaultMapper; // Identify structural scope of selection - nearest common ancestor ITextPointer commonAncestor = FindSerializationCommonAncestor(range); // Decide whether we need last paragraph merging or not bool lastParagraphMustBeMerged = !TextPointerBase.IsAfterLastParagraph(range.End) && range.End.GetPointerContext(LogicalDirection.Backward) != TextPointerContext.ElementStart; // Write wrapping element with contextual properties WriteRootFlowDocument(range, commonAncestor, xmlWriter, xamlTypeMapper, lastParagraphMustBeMerged, useFlowDocumentAsRoot); // The ignoreWriteHyperlinkEnd flag will be set after call WriteOpeningTags. // If ignoreWriteHyperlinkEnd is true, WriteXamlTextSegment won't write Hyperlink end element // since Hyperlink writing opening tag is ignored by selecting the partial of Hyperlink. bool ignoreWriteHyperlinkEnd; ListignoreList = new List (); // Start counting tags needed to be closed. // EmptyDocumentDepth==1 - counts FlowDocument opened in WriteRootFlowDocument above. int elementLevel = EmptyDocumentDepth + WriteOpeningTags(range, range.Start, commonAncestor, xmlWriter, xamlTypeMapper, /*reduceElement:*/wpfPayload == null, out ignoreWriteHyperlinkEnd, ref ignoreList, preserveTextElements); if (range.IsTableCellRange) { WriteXamlTableCellRange(xmlWriter, range, xamlTypeMapper, ref elementLevel, wpfPayload, preserveTextElements); } else { WriteXamlTextSegment(xmlWriter, range.Start, range.End, xamlTypeMapper, ref elementLevel, wpfPayload, ignoreWriteHyperlinkEnd, ignoreList, preserveTextElements); } // Close all remaining tags - scoping its End position Invariant.Assert(elementLevel >= 0, "elementLevel cannot be negative"); while (elementLevel-- > 0) { xmlWriter.WriteFullEndElement(); } // Restore xmlWriter's Formatting property if (xmlWriter is XmlTextWriter) { ((XmlTextWriter)xmlWriter).Formatting = saveWriterFormatting; } } /// /// Reads a well-formed xml representing a serialized text range. /// It expects a root element xaml:FlowDocument and two range markers /// The result of reading is pasting this text into End position /// of text range. /// /// /// TextRange designating the target position for pasting. /// The existing content of a range will be deleted and new content /// will be inserted at the end. /// Resulting locations of Start/End positions depend on their gravities. /// Normally (when gravity=Backward/Forward respectively) the resulting /// range will embrace the inserted content. /// /// /// Represents a portion of xml to insert into the range. /// ////// We are expecting to be called with xmlReader on opening tag /// of root text range element - xaml:FlowDocument. /// Some insignificant stuff may occur before the root though. /// Otherwise exception will be thrown. /// internal static void PasteXml(TextRange range, TextElement fragment) { Invariant.Assert(fragment != null); // Check a special case for pasing a single embedded element if (PasteSingleEmbeddedElement(range, fragment)) { // All done. Return successfully. return; } // Set default value for an indicator of whether we need to merge last paragraph or not. // It depends on a state of a range, so do it before emptying the range. AdjustFragmentForTargetRange(fragment, range); // Delete current content of a range if (!range.IsEmpty) { range.Text = String.Empty; } Invariant.Assert(range.IsEmpty, "range must be empty in the beginning of pasting"); // Chek special case of empty pasted fragment if (((ITextPointer)fragment.ContentStart).CompareTo(fragment.ContentEnd) == 0) { // Pasted fragment is empty. Nothing to insert. return; } // Transfer the content from reader to writer and merge elements on both ends PasteTextFragment(fragment, range); } #endregion Internal Methods // -------------------------------------------------------------- // // Private Methods // // ------------------------------------------------------------- #region Private Methods // ............................................................. // // Serialization // // ............................................................. ////// This function serializes text segment formed by rangeStart and rangeEnd to valid xml using xmlWriter. /// ////// To mask the security exception from XamlWriter.Save in partial trust case, /// this function checks if the current call stack has the all clipboard permission. /// private static void WriteXamlTextSegment(XmlWriter xmlWriter, ITextPointer rangeStart, ITextPointer rangeEnd, XamlTypeMapper xamlTypeMapper, ref int elementLevel, WpfPayload wpfPayload, bool ignoreWriteHyperlinkEnd, ListignoreList, bool preserveTextElements) { // Special case for pure text selection - we need a Run wrapper for it. if (elementLevel == EmptyDocumentDepth && typeof(Run).IsAssignableFrom(rangeStart.ParentType)) { elementLevel++; xmlWriter.WriteStartElement(typeof(Run).Name); } // Create text navigator for reading the range's content ITextPointer textReader = rangeStart.CreatePointer(); // Exclude last opening tag from serialization - we don't need to create extra element // is cases when we have whole paragraphs/cells selected. // NOTE: We do this slightly differently than in TextRangeEdit.AdjustRangeEnd, where we use normalization for adjusted position. // In this case normalized position does not work, because we need to keep information about crossed paragraph boundary. while (rangeEnd.GetPointerContext(LogicalDirection.Backward) == TextPointerContext.ElementStart) { rangeEnd = rangeEnd.GetNextContextPosition(LogicalDirection.Backward); } // Write the range internal contents while (textReader.CompareTo(rangeEnd) < 0) { TextPointerContext runType = textReader.GetPointerContext(LogicalDirection.Forward); switch (runType) { case TextPointerContext.ElementStart: TextElement nextElement = (TextElement)textReader.GetAdjacentElement(LogicalDirection.Forward); if (nextElement is Hyperlink) { // Don't write Hyperlink start element if Hyperlink is invalid // in case of having a UiElement except Image or stated the range end // position before the end position of the Hyperlink. if (IsHyperlinkInvalid(textReader, rangeEnd)) { ignoreWriteHyperlinkEnd = true; textReader.MoveToNextContextPosition(LogicalDirection.Forward); continue; } } else if (nextElement != null) { // TextElementEditingBehaviorAttribute att = (TextElementEditingBehaviorAttribute)Attribute.GetCustomAttribute(nextElement.GetType(), typeof(TextElementEditingBehaviorAttribute)); if (att != null && !att.IsTypographicOnly) { if (IsPartialNonTypographic(textReader, rangeEnd)) { // Add pointer to ignore list ITextPointer ptr = textReader.CreatePointer(); ptr.MoveToElementEdge(ElementEdge.BeforeEnd); ignoreList.Add(ptr.Offset); textReader.MoveToNextContextPosition(LogicalDirection.Forward); continue; } } } elementLevel++; textReader.MoveToNextContextPosition(LogicalDirection.Forward); WriteStartXamlElement(/*range:*/null, textReader, xmlWriter, xamlTypeMapper, /*reduceElement:*/wpfPayload == null, preserveTextElements); break; case TextPointerContext.ElementEnd: // Don't write Hyperlink end element if Hyperlink include the invalid // in case of having a UiElement except Image or stated the range end // before the end position of the Hyperlink or Hyperlink opening tag is // skipped from WriteOpeningTags by selecting of the partial of Hyperlink. if (ignoreWriteHyperlinkEnd && (textReader.GetAdjacentElement(LogicalDirection.Forward) is Hyperlink)) { // Reset the flag to keep walk up the next Hyperlink tag ignoreWriteHyperlinkEnd = false; textReader.MoveToNextContextPosition(LogicalDirection.Forward); continue; } // Check the ignore list ITextPointer endPointer = textReader.CreatePointer(); endPointer.MoveToElementEdge(ElementEdge.BeforeEnd); // if (ignoreList.Contains(endPointer.Offset)) { ignoreList.Remove(endPointer.Offset); textReader.MoveToNextContextPosition(LogicalDirection.Forward); continue; } elementLevel--; if (TextSchema.IsBreak(textReader.ParentType)) { // For LineBreak, etc. use empty element syntax xmlWriter.WriteEndElement(); } else { // // For all other textelements use explicit closing tag. xmlWriter.WriteFullEndElement(); } textReader.MoveToNextContextPosition(LogicalDirection.Forward); break; case TextPointerContext.Text: int textLength = textReader.GetTextRunLength(LogicalDirection.Forward); char[] text = new Char[textLength]; textLength = TextPointerBase.GetTextWithLimit(textReader, LogicalDirection.Forward, text, 0, textLength, rangeEnd); // XmlWriter will throw an ArgumentException if text contains // any invalid surrogates, so strip them out now. textLength = StripInvalidSurrogateChars(text, textLength); xmlWriter.WriteChars(text, 0, textLength); textReader.MoveToNextContextPosition(LogicalDirection.Forward); break; case TextPointerContext.EmbeddedElement: object embeddedObject = textReader.GetAdjacentElement(LogicalDirection.Forward); textReader.MoveToNextContextPosition(LogicalDirection.Forward); WriteEmbeddedObject(embeddedObject, xmlWriter, wpfPayload); break; default: Invariant.Assert(false, "unexpected value of runType"); textReader.MoveToNextContextPosition(LogicalDirection.Forward); break; } } } /// /// Serializes a rectagular table range /// private static void WriteXamlTableCellRange(XmlWriter xmlWriter, ITextRange range, XamlTypeMapper xamlTypeMapper, ref int elementLevel, WpfPayload wpfPayload, bool preserveTextElements) { Invariant.Assert(range.IsTableCellRange, "range is expected to be in IsTableCellRange state"); ListtextSegments = range.TextSegments; int checkElementLevel = -1; // negative value as an indicator that it is not yet initialized // Set ignoreWriteHyperlinkEnd as false initially bool ignoreWriteHyperlinkEnd = false; List ignoreList = new List (); for (int i = 0; i < textSegments.Count; i++) { TextSegment textSegment = textSegments[i]; // Open a row for this segment (except for the very first one, for which we opened a row in a WriteOpeningTags method) if (i > 0) { ITextPointer pointer = textSegment.Start.CreatePointer(); while (!typeof(TableRow).IsAssignableFrom(pointer.ParentType)) { Invariant.Assert(typeof(TextElement).IsAssignableFrom(pointer.ParentType), "pointer must be still in a scope of TextElement"); pointer.MoveToElementEdge(ElementEdge.BeforeStart); } Invariant.Assert(typeof(TableRow).IsAssignableFrom(pointer.ParentType), "pointer must be in a scope of TableRow"); pointer.MoveToElementEdge(ElementEdge.BeforeStart); ITextRange textRange = new TextRange(textSegment.Start, textSegment.End); elementLevel += WriteOpeningTags(textRange, textSegment.Start, pointer, xmlWriter, xamlTypeMapper, /*reduceElement:*/wpfPayload == null, out ignoreWriteHyperlinkEnd, ref ignoreList, preserveTextElements); } // Output the cell segment for one row WriteXamlTextSegment(xmlWriter, textSegment.Start, textSegment.End, xamlTypeMapper, ref elementLevel, wpfPayload, ignoreWriteHyperlinkEnd, ignoreList, preserveTextElements); Invariant.Assert(elementLevel >= 4, "At the minimun we expected to stay within four elements: Section(wrapper),Table,TableRowGroup,TableRow"); if (checkElementLevel < 0) checkElementLevel = elementLevel; // initialize level checking variable Invariant.Assert(checkElementLevel == elementLevel, "elementLevel is supposed to be unchanged between segments of table cell range"); // Assuming that the element is TableRow - close it. // NOTE: Such assumption is valid because WriteXamlTextSegment moves end pointer out of all opening tags, // so it ends serialization immediately after the last cell's closing tag. // This means that we only need to close one level - for TableRow. // elementLevel--; xmlWriter.WriteFullEndElement(); } } /// /// Walks the tree up from current position and writes all scoping tags /// in their natural order - from root to leafs. /// /// /// Range identifying the whole selection. /// Needed for /// - table cell range case: proper column processing: to output only columns related to the selection /// - text segement case: hyperlink serialization heuristics /// /// /// ITextPointer identifying an element. /// /// /// A position identifying the scope which should be used for serialization. /// All tags outside of this scope will be ignored. /// /// /// XmlWriter to write element tags. /// /// /// ////// /// /// /// /// /// /// Number of opening tags written into XmlWriter. /// This number should be used afterwards to close all opened tags. /// private static int WriteOpeningTags(ITextRange range, ITextPointer thisElement, ITextPointer scope, XmlWriter xmlWriter, XamlTypeMapper xamlTypeMapper, bool reduceElement, out bool ignoreWriteHyperlinkEnd, ref ListignoreList, bool preserveTextElements) { ignoreWriteHyperlinkEnd = false; // Recursion ends when we reach the scope level. We will write tags on returing path from the recursion if (thisElement.HasEqualScope(scope)) { return 0; // no elements have opened at this level. Return elementCount==0. } Invariant.Assert(typeof(TextElement).IsAssignableFrom(thisElement.ParentType), "thisElement is expected to be a TextElement"); ITextPointer previousLevel = thisElement.CreatePointer(); previousLevel.MoveToElementEdge(ElementEdge.BeforeStart); // Recurse into the parent element int elementLevel = WriteOpeningTags(range, previousLevel, scope, xmlWriter, xamlTypeMapper, reduceElement, out ignoreWriteHyperlinkEnd, ref ignoreList, preserveTextElements); // After returning from the recursion - when all parent tags have been written, // write the opening tag for this element // Hyperlink open tag will be skipped since the range selection of Hyperlink is the partial // of Hyperlink range or Hyperlink include invalid UIElement except Image. bool ignoreHyperlink = false; bool isPartialNonTypographic = false; if (thisElement.ParentType == typeof(Hyperlink)) { if (TextPointerBase.IsAtNonMergeableInlineStart(range.Start)) { ITextPointer position = thisElement.CreatePointer(); position.MoveToElementEdge(ElementEdge.BeforeStart); ignoreHyperlink = IsHyperlinkInvalid(position, range.End); } else { ignoreHyperlink = true; } } else { // TextElementEditingBehaviorAttribute att = (TextElementEditingBehaviorAttribute)Attribute.GetCustomAttribute(thisElement.ParentType, typeof(TextElementEditingBehaviorAttribute)); if (att != null && !att.IsTypographicOnly) { if (TextPointerBase.IsAtNonMergeableInlineStart(range.Start)) { ITextPointer position = thisElement.CreatePointer(); position.MoveToElementEdge(ElementEdge.BeforeStart); isPartialNonTypographic = IsPartialNonTypographic(position, range.End); } else { isPartialNonTypographic = true; } } } int count; if (ignoreHyperlink) { // Ignore writing Hyperlink opening tag ignoreWriteHyperlinkEnd = true; // Set elementLevel without adding it count = elementLevel; } else if (isPartialNonTypographic) { // Add the end pointer to the list ITextPointer position = thisElement.CreatePointer(); position.MoveToElementEdge(ElementEdge.BeforeEnd); ignoreList.Add(position.Offset); // Set elementLevel without adding to it count = elementLevel; } else { // Write the opening tag WriteStartXamlElement(range, thisElement, xmlWriter, xamlTypeMapper, reduceElement, preserveTextElements); // Each opening tag adds one to the level count count = elementLevel + 1; } // Return the opening tag count return count; } /// /// Writes an opening tag of an element together with all attributes /// representing Avalon properties. /// /// /// Parameter used for top-level Table element - to decide what columns to output. /// For all other elements it's ignored. /// /// /// TextPointer positioned in the scope of element whose /// start tag is going to be written. /// /// /// XmlWriter to output element opening tag. /// /// /// /// True value of this parameter indicates that /// serialization goes into XamlPackage, so all elements /// can be preserved as is; otherwise some of them must be /// reduced into simpler representations (such as InlineUIContainer -> Run /// and BlockUIContainer -> Paragraph). /// /// /// If TRUE, TextElements are serialized as-is. If FALSE, they're upcast /// to their base types. /// private static void WriteStartXamlElement(ITextRange range, ITextPointer textReader, XmlWriter xmlWriter, XamlTypeMapper xamlTypeMapper, bool reduceElement, bool preserveTextElements) { Type elementType = textReader.ParentType; Type elementTypeStandardized = TextSchema.GetStandardElementType(elementType, reduceElement); // Get rid f UIContainers when their child is not an image if (elementTypeStandardized == typeof(InlineUIContainer) || elementTypeStandardized == typeof(BlockUIContainer)) { Invariant.Assert(!reduceElement); InlineUIContainer inlineUIContainer = textReader.GetAdjacentElement(LogicalDirection.Backward) as InlineUIContainer; BlockUIContainer blockUIContainer = textReader.GetAdjacentElement(LogicalDirection.Backward) as BlockUIContainer; if ((inlineUIContainer == null || !(inlineUIContainer.Child is Image)) && (blockUIContainer == null || !(blockUIContainer.Child is Image))) { // Even when we serialize for DataFormats.XamlPackage we strip out UIElement // different from Images. // Note that this condition is consistent with the one in WriteEmbeddedObject - // so that when we reduce the element type fromm UIContainer to Run/Paragraph // we also output just a space instead of the embedded object conntained in it. elementTypeStandardized = TextSchema.GetStandardElementType(elementType, /*reduceElement:*/true); } } else if (preserveTextElements) { elementTypeStandardized = elementType; } bool customTextElement = preserveTextElements && !TextSchema.IsKnownType(elementType); if (customTextElement) { // If the element is not from PresentationFramework, we'll need to serialize a namespace // int index = elementTypeStandardized.Module.Name.LastIndexOf('.'); string assembly = (index == -1 ? elementTypeStandardized.Module.Name : elementTypeStandardized.Module.Name.Substring(0, index)); string nameSpace = "clr-namespace:" + elementTypeStandardized.Namespace + ";" + "assembly=" + assembly; string prefix = elementTypeStandardized.Namespace; xmlWriter.WriteStartElement(prefix, elementTypeStandardized.Name, nameSpace); } else { xmlWriter.WriteStartElement(elementTypeStandardized.Name); } // Write properties DependencyObject complexProperties = new DependencyObject(); WriteInheritableProperties(elementTypeStandardized, textReader, xmlWriter, /*onlyAffected:*/true, complexProperties); WriteNoninheritableProperties(elementTypeStandardized, textReader, xmlWriter, /*onlyAffected:*/true, complexProperties); if (customTextElement) { WriteLocallySetProperties(elementTypeStandardized, textReader, xmlWriter, complexProperties); } WriteComplexProperties(xmlWriter, complexProperties, elementTypeStandardized); // Special case for Table element serialization if (elementTypeStandardized == typeof(Table) && textReader is TextPointer) { // Write the columns text. WriteTableColumnsInformation(range, (Table)((TextPointer)textReader).Parent, xmlWriter, xamlTypeMapper); } } // Write columns related to the given table cell range. private static void WriteTableColumnsInformation(ITextRange range, Table table, XmlWriter xmlWriter, XamlTypeMapper xamlTypeMapper) { TableColumnCollection columns = table.Columns; int startColumn; int endColumn; if (!TextRangeEditTables.GetColumnRange(range, table, out startColumn, out endColumn)) { startColumn = 0; endColumn = columns.Count - 1; } Invariant.Assert(startColumn >= 0, "startColumn index is supposed to be non-negative"); if(columns.Count > 0) { // Build an appropriate name for the complex property string complexPropertyName = table.GetType().Name + ".Columns"; // Write the start element for the complex property. xmlWriter.WriteStartElement(complexPropertyName); for (int i = startColumn; i <= endColumn && i < columns.Count; i++) { WriteXamlAtomicElement(columns[i], xmlWriter, /*reduceElement:*/false); } // Close the element for the complex property xmlWriter.WriteEndElement(); } } ////// Creates a FlowDocument element wrapping copied content and storing its contextual properties. /// /// /// /// /// /// /// /// true means that we need to serialize the whole FlowDocument - used in FileSave scenario; /// false means that we are in copy-paste scenario and will use Section or Span as a root - depending on context. /// private static void WriteRootFlowDocument(ITextRange range, ITextPointer context, XmlWriter xmlWriter, XamlTypeMapper xamlTypeMapper, bool lastParagraphMustBeMerged, bool useFlowDocumentAsRoot) { Type rootType; const string xmlNamespace = "http://schemas.microsoft.com/winfx/2006/xaml/presentation"; const string xmlns = "xmlns"; // Decide what root element to use if (useFlowDocumentAsRoot) { rootType = typeof(FlowDocument); } else { Type contextType = context.ParentType; if (contextType == null || typeof(Paragraph).IsAssignableFrom(contextType) || typeof(Inline).IsAssignableFrom(contextType) && !typeof(AnchoredBlock).IsAssignableFrom(contextType)) { rootType = typeof(Span); } else { rootType = typeof(Section); } } // Create a root element FlowDocument xmlWriter.WriteStartElement(rootType.Name, xmlNamespace); // Define default namespace as Avalon namespace xmlWriter.WriteAttributeString(xmlns, xmlNamespace); // Set the value of xml:space to "preserve" to consider all spaces as significant // Note that Xml treats whitespaces as significant if they belong to some nonempty line // (neighbored by non-whitespace characters at least from one side) // That's why we only loose whitespaces if they occupy the whole textrun in xml. // So alternative solution for whitespace preservation could be setting xml:space="preserve" // attribute to only empty runs - this would make our whitespace preservation more // narrowed... // xmlWriter.WriteAttributeString("xml:space", "preserve"); // Write all contextual properties as attributes of root fragment DependencyObject complexProperties = new DependencyObject(); if (useFlowDocumentAsRoot) { WriteInheritablePropertiesForFlowDocument((DependencyObject)((TextPointer)context).Parent, xmlWriter, complexProperties); } else { WriteInheritableProperties(rootType, context, xmlWriter, /*onlyAffected:*/false, complexProperties); } if (rootType == typeof(Span)) { // Root element is not real element to paste. It is just a property bag for contextual properties. // So we collect non-inheritable properties only for inline content; not needing it for block one. WriteNoninheritableProperties(typeof(Span), context, xmlWriter, /*onlyAffected:*/false, complexProperties); } // Write an indicator that last paragraph must be merged on paste if (rootType == typeof(Section) && lastParagraphMustBeMerged) { xmlWriter.WriteAttributeString(Section.HasTrailingParagraphBreakOnPastePropertyName, "False"); } // Note that we are skipping background property, because we only want to transfer it for the whole document. // WriteComplexProperties(xmlWriter, complexProperties, rootType); } private static void WriteInheritablePropertiesForFlowDocument(DependencyObject context, XmlWriter xmlWriter, DependencyObject complexProperties) { DependencyProperty[] inheritableProperties = TextSchema.GetInheritableProperties(typeof(FlowDocument)); for (int i = 0; i < inheritableProperties.Length; i++) { DependencyProperty property = inheritableProperties[i]; object value = context.ReadLocalValue(property); if (value != DependencyProperty.UnsetValue) { string stringValue = DPTypeDescriptorContext.GetStringValue(property, value); if (stringValue != null) { stringValue = FilterNaNStringValueForDoublePropertyType(stringValue, property.PropertyType); string propertyName; if (property == FrameworkContentElement.LanguageProperty) { // Special case for CultureInfo property that must be represented in xaml as xml:lang attribute propertyName = "xml:lang"; } else { // Regular case using own property name propertyName = property.OwnerType == typeof(Typography) ? "Typography." + property.Name : property.Name; } xmlWriter.WriteAttributeString(propertyName, stringValue); } else { complexProperties.SetValue(property, value); } } } } // Writes a collection of attributes representing inheritable properties // whose values has been affected by this element. // Parameter onlyAffected=true means that we serialize only properties affected by // the current element; otherwise we output all known inheritable properties. private static void WriteInheritableProperties(Type elementTypeStandardized, ITextPointer context, XmlWriter xmlWriter, bool onlyAffected, DependencyObject complexProperties) { // Create a pointer positioned immediately outside the element ITextPointer outerContext = null; if (onlyAffected) { outerContext = context.CreatePointer(); outerContext.MoveToElementEdge(ElementEdge.BeforeStart); } DependencyProperty[] inheritableProperties = TextSchema.GetInheritableProperties(elementTypeStandardized); for (int i = 0; i < inheritableProperties.Length; i++) { DependencyProperty property = inheritableProperties[i]; object innerValue = context.GetValue(property); if (innerValue == null) { // Some properties like Foreground may have null as default value. // Skip them. continue; } object outerValue = null; if (onlyAffected) { outerValue = outerContext.GetValue(property); } // The property must appear in markup if the element if (!onlyAffected || // all properties requested for saving context on root !TextSchema.ValuesAreEqual(innerValue, outerValue)) // or the element really affects the property { string stringValue = DPTypeDescriptorContext.GetStringValue(property, innerValue); if (stringValue != null) { stringValue = FilterNaNStringValueForDoublePropertyType(stringValue, property.PropertyType); string propertyName; if (property == FrameworkContentElement.LanguageProperty) { // Special case for CultureInfo property that must be represented in xaml as xml:lang attribute propertyName = "xml:lang"; } else { // Regular case: serialize a property with its own name propertyName = GetPropertyNameForElement(property, elementTypeStandardized, /*forceComplexName:*/false); } xmlWriter.WriteAttributeString(propertyName, stringValue); } else { complexProperties.SetValue(property, innerValue); } } } } // Writes a collection of attributes representing non-inheritable properties // whose values are set inline on the given element instance. // When we read properties fromContext we want all values including defaults; from text elements we only want only affected private static void WriteNoninheritableProperties(Type elementTypeStandardized, ITextPointer context, XmlWriter xmlWriter, bool onlyAffected, DependencyObject complexProperties) { DependencyProperty[] elementProperties = TextSchema.GetNoninheritableProperties(elementTypeStandardized); // We'll need a pointer to walk the tree up when onlyAffected=false ITextPointer parentContext = onlyAffected ? null : context.CreatePointer(); for (int i = 0; i < elementProperties.Length; i++) { DependencyProperty property = elementProperties[i]; Type propertyOwnerType = context.ParentType; object propertyValue; if (onlyAffected) { // propertyValue = context.GetValue(property); } else { // This is request for contextual properties - use "manual" inheritance to collect values Invariant.Assert(elementTypeStandardized == typeof(Span), "Request for contextual properties is expected for Span wrapper only"); // Get property value from this element or from one of its ancestors (the latter in case of !onlyAffeted) propertyValue = context.GetValue(property); // Get property value from its ancestors if the property is not set. // TextDecorationCollection is special-cased as its default is empty collection, // and its value source cannot be distinguished from ITextPointer. if (propertyValue == null || TextDecorationCollection.Empty.ValueEquals(propertyValue as TextDecorationCollection)) { if (property == Inline.BaselineAlignmentProperty || property == TextElement.TextEffectsProperty) { // These properties do not make sense as contextual; do not include them into context. continue; } parentContext.MoveToPosition(context); while ((propertyValue == null || TextDecorationCollection.Empty.ValueEquals(propertyValue as TextDecorationCollection)) && typeof(Inline).IsAssignableFrom(parentContext.ParentType)) { parentContext.MoveToElementEdge(ElementEdge.BeforeStart); propertyValue = parentContext.GetValue(property); propertyOwnerType = parentContext.ParentType; } } } // if ((property == Block.MarginProperty && (typeof(Paragraph).IsAssignableFrom(propertyOwnerType) || typeof(List).IsAssignableFrom(propertyOwnerType))) || (property == Block.PaddingProperty) && typeof(List).IsAssignableFrom(propertyOwnerType)) { Thickness thickness = (Thickness)propertyValue; if (Paragraph.IsMarginAuto(thickness)) { continue; } } // Write the property as attribute string or add it to a list of complex properties. WriteNoninheritableProperty(xmlWriter, property, propertyValue, propertyOwnerType, onlyAffected, complexProperties, context.ReadLocalValue(property)); } } // Writes a value of an individual non-inheritable property in form of attribute string. // If the value cannot be serialized as a string, adds the property to a collection of complexProperties. // To minimize the amount of xaml produced, the property is skipped if its value is equal to its default value // for the given element type - the propertyOwnerType. // The flag onlyAffected=false means that we want to output all properties independently on // if they are equal to their default values or not. private static void WriteNoninheritableProperty(XmlWriter xmlWriter, DependencyProperty property, object propertyValue, Type propertyOwnerType, bool onlyAffected, DependencyObject complexProperties, object localValue) { bool write = false; if (propertyValue != null && propertyValue != DependencyProperty.UnsetValue) { if (!onlyAffected) { write = true; } else { PropertyMetadata metadata = property.GetMetadata(propertyOwnerType); write = (metadata == null) || !(TextSchema.ValuesAreEqual(propertyValue, /*defaultValue*/metadata.DefaultValue) && localValue == DependencyProperty.UnsetValue); } } if (write) { string stringValue = DPTypeDescriptorContext.GetStringValue(property, propertyValue); if (stringValue != null) { stringValue = FilterNaNStringValueForDoublePropertyType(stringValue, property.PropertyType); // For the property name in this case we safe to use simple name only; // as noninheritable property would never require TypeName.PropertyName notation // for attribute syntax. xmlWriter.WriteAttributeString(property.Name, stringValue); } else { complexProperties.SetValue(property, propertyValue); } } } // Writes a collection of attributes representing properties with local values set on them. // If the value cannot be serialized as a string, adds the property to a collection of complexProperties. private static void WriteLocallySetProperties(Type elementTypeStandardized, ITextPointer context, XmlWriter xmlWriter, DependencyObject complexProperties) { TextPointer textPointer = context as TextPointer; if (textPointer == null) { // We can't have custom properties if we're not a TextPointer return; } LocalValueEnumerator locallySetProperties = context.GetLocalValueEnumerator(); DependencyProperty[] inheritableProperties = TextSchema.GetInheritableProperties(elementTypeStandardized); DependencyProperty[] nonInheritableProperties = TextSchema.GetNoninheritableProperties(elementTypeStandardized); while (locallySetProperties.MoveNext()) { DependencyProperty locallySetProperty = (DependencyProperty)locallySetProperties.Current.Property; // Don't serialize read-only properties, or any properties registered or owned by a // a class in the framework (we only want to serialize custom properties), to be // consistent with our behavior for non-custom inlines. // if (!locallySetProperty.ReadOnly && !IsPropertyKnown(locallySetProperty, inheritableProperties, nonInheritableProperties) && !TextSchema.IsKnownType(locallySetProperty.OwnerType)) { object propertyValue = context.ReadLocalValue(locallySetProperty); string stringValue = DPTypeDescriptorContext.GetStringValue(locallySetProperty, propertyValue); if (stringValue != null) { stringValue = FilterNaNStringValueForDoublePropertyType(stringValue, locallySetProperty.PropertyType); string propertyName = GetPropertyNameForElement(locallySetProperty, elementTypeStandardized, /*forceComplexName:*/false); xmlWriter.WriteAttributeString(propertyName, stringValue); } else { complexProperties.SetValue(locallySetProperty, propertyValue); } } } // *** WE NEED TO BETTER UNDERSTAND THE IMPLICATIONS OF SERIALIZING NON-DP CLR PROPERTIES, SO THE REST OF // *** THIS METHOD IS DISABLED UNTIL WE DECIDE THE BEST WAY TO HANDLE THEM. // *** CLRTypeDescriptorContext is essentially the same as DPTypeDescriptorContext. #if false // Check all CLR properties // Note that this is partially redundant. TypeDescriptor.GetProperties, when called on a // DependencyObject, will return all properties that are set-- including all those already // serialized as Inheritable, NonInheritable, or LocallySet properties. A potential // optimization, therefore, is to remove those serialization methods and simply use this one // for everything when we've opted into custom element serialization. PropertyDescriptorCollection descriptorCollection = TypeDescriptor.GetProperties(textPointer.Parent); IEnumerator descriptors = descriptorCollection.GetEnumerator(); while (descriptors.MoveNext()) { PropertyDescriptor current = (PropertyDescriptor)descriptors.Current; // ShouldSerializeValue() will return true for readonly properties that have explicitly // been told to serialize, such as Span.Inlines. If we serialize a read-only property, // however, the parser will throw an exception when we try to deserialize. So we // explicitly skip all read-only properties, and all DPs. if (!current.ShouldSerializeValue(textPointer.Parent) || current.IsReadOnly || current is MS.Internal.ComponentModel.DependencyObjectPropertyDescriptor) { continue; } // Serialize the property object propertyValue = current.GetValue(textPointer.Parent); if (propertyValue != null) { string stringValue = CLRTypeDescriptorContext.GetStringValue(current, propertyValue); if (stringValue != null) { stringValue = FilterNaNStringValueForDoublePropertyType(stringValue, current.PropertyType); xmlWriter.WriteAttributeString(current.Name, stringValue); } else { // } } } #endif } private static bool IsPropertyKnown(DependencyProperty propertyToTest, DependencyProperty[] inheritableProperties, DependencyProperty[] nonInheritableProperties) { for (int i = 0; i < inheritableProperties.Length; i++) { DependencyProperty property = inheritableProperties[i]; if (property == propertyToTest) { return true; } } for (int i = 0; i < nonInheritableProperties.Length; i++) { DependencyProperty property = nonInheritableProperties[i]; if (property == propertyToTest) { return true; } } return false; } ////// Writes complex properties in form of child elements with compound names /// ////// To mask the security exception from XamlWriter.Save in partial trust case, /// this function checks if the current call stack has unmanaged code permission. /// private static void WriteComplexProperties(XmlWriter xmlWriter, DependencyObject complexProperties, Type elementType) { if (!SecurityHelper.CheckUnmanagedCodePermission()) { // In partial trust, we cannot serialize any complex properties because // XamlWriter.Save demands UnmanagedCodePermission. // // If we're in PT, drop the properties. // // If you're here debugging a lost complex property, consider adding // code to DPTypeDescriptorContext to convert the complex property // into a non-complex property, or consider modifying XamlWriter.Save. return; } LocalValueEnumerator properties = complexProperties.GetLocalValueEnumerator(); properties.Reset(); while (properties.MoveNext()) { LocalValueEntry propertyEntry = properties.Current; // Build an appropriate name for the complex property string complexPropertyName = GetPropertyNameForElement(propertyEntry.Property, elementType, /*forceComplexName:*/true); // Write the start element for the complex property. xmlWriter.WriteStartElement(complexPropertyName); // Serialize the complex property value from SaveAsXml(). string complexPropertyXml = System.Windows.Markup.XamlWriter.Save(propertyEntry.Value); // Write the serialized complext property value as Xml. xmlWriter.WriteRaw(complexPropertyXml); // Close the element for the complex property xmlWriter.WriteEndElement(); } } // Creates a name for the property which is consistent with xaml parser logic // When forceComplexName=true produces the TypeName.PropertyName notation unconditionally, // otherwise such complex name is produced only when the TypeName is different from elementType.Name. private static string GetPropertyNameForElement(DependencyProperty property, Type elementType, bool forceComplexName) { string propertyName; if (DependencyProperty.FromName(property.Name, elementType) == property) { // The elementType is an owner of this property, so we can use its name if (forceComplexName) { propertyName = elementType.Name + "." + property.Name; } else { propertyName = property.Name; } } else { // The elementType does not own this property, so we use the property's registered owner type name. propertyName = property.OwnerType.Name + "." + property.Name; } return propertyName; } // Serializes an element assuming that it does not have any children. Used for TableColumn // private static void WriteXamlAtomicElement(DependencyObject element, XmlWriter xmlWriter, bool reduceElement) { Type elementTypeStandardized = TextSchema.GetStandardElementType(element.GetType(), reduceElement); DependencyProperty[] elementProperties = TextSchema.GetNoninheritableProperties(elementTypeStandardized); xmlWriter.WriteStartElement(elementTypeStandardized.Name); for (int i = 0; i < elementProperties.Length; i++) { DependencyProperty property = elementProperties[i]; object propertyValue = element.ReadLocalValue(property); if (propertyValue != null && propertyValue != DependencyProperty.UnsetValue) { System.ComponentModel.TypeConverter typeConverter = System.ComponentModel.TypeDescriptor.GetConverter(property.PropertyType); Invariant.Assert(typeConverter != null, "typeConverter==null: is not expected for atomic elements"); Invariant.Assert(typeConverter.CanConvertTo(typeof(string)), "type is expected to be convertable into string type"); string stringValue = (string)typeConverter.ConvertTo(/*ITypeDescriptorContext:*/null, CultureInfo.InvariantCulture, propertyValue, typeof(string)); Invariant.Assert(stringValue != null, "expecting non-null stringValue"); xmlWriter.WriteAttributeString(property.Name, stringValue); } } xmlWriter.WriteEndElement(); } ////// Writes embeded object tag. /// /// /// /// /// XmlWriter to output element opening tag. /// /// /// private static void WriteEmbeddedObject(object embeddedObject, XmlWriter xmlWriter, WpfPayload wpfPayload) { if (wpfPayload != null && embeddedObject is Image) { // Writing in WPF mode: need to create an image with a Source referring into a package Image image = (Image)embeddedObject; if (image.Source != null && !string.IsNullOrEmpty(image.Source.ToString())) { // Add the image to the Image collection in the package // and define the reference to image into the package string imageSource = wpfPayload.AddImage(image); if (imageSource != null) { Type elementTypeStandardized = typeof(Image); // Write opening tag for the element xmlWriter.WriteStartElement(elementTypeStandardized.Name); // Write all properties except for Source DependencyProperty[] imageProperties = TextSchema.ImageProperties; DependencyObject complexProperties = new DependencyObject(); for (int i = 0; i < imageProperties.Length; i++) { DependencyProperty property = imageProperties[i]; if (property != Image.SourceProperty) { object value = image.GetValue(property); // Write the property as attribute string or add it to a list of complex properties. WriteNoninheritableProperty(xmlWriter, property, value, elementTypeStandardized, /*onlyAffected:*/true, complexProperties, image.ReadLocalValue(property)); } } // Write Source property - as a local reference into the package container // Write Source property as the complex property to specify the BitmapImage // cache option as "OnLoad" instead of the default "OnDeman". Otherwise, // we couldn't load the image by disposing WpfPayload package. xmlWriter.WriteStartElement(typeof(Image).Name + "." + Image.SourceProperty.Name); xmlWriter.WriteStartElement(typeof(System.Windows.Media.Imaging.BitmapImage).Name); xmlWriter.WriteAttributeString(System.Windows.Media.Imaging.BitmapImage.UriSourceProperty.Name, imageSource); xmlWriter.WriteAttributeString(System.Windows.Media.Imaging.BitmapImage.CacheOptionProperty.Name, "OnLoad"); xmlWriter.WriteEndElement(); xmlWriter.WriteEndElement(); // Write remaining complex properties WriteComplexProperties(xmlWriter, complexProperties, elementTypeStandardized); // Close the element xmlWriter.WriteEndElement(); } } } else { // In non-package mode we ignore all UIElements. // Output a space replacing this embedded element. // Note that in this mode (DataFormats.Xaml) InlineUIContainer was // replaced by Run and BlockUIContainer - by Paragraph, // so the space output here will be significant. xmlWriter.WriteString(" "); } } // ............................................................. // // Pasting // // ............................................................. // Handles a special case for pasting a single embedded element - // needs to choose between BlockUIContainer and InlineUIContainer. private static bool PasteSingleEmbeddedElement(TextRange range, TextElement fragment) { if (fragment.ContentStart.GetOffsetToPosition(fragment.ContentEnd) == 3) { TextElement uiContainer = fragment.ContentStart.GetAdjacentElement(LogicalDirection.Forward) as TextElement; FrameworkElement embeddedElement = null; if (uiContainer is BlockUIContainer) { embeddedElement = ((BlockUIContainer)uiContainer).Child as FrameworkElement; if (embeddedElement != null) { ((BlockUIContainer)uiContainer).Child = null; } } else if (uiContainer is InlineUIContainer) { embeddedElement = ((InlineUIContainer)uiContainer).Child as FrameworkElement; if (embeddedElement != null) { ((InlineUIContainer)uiContainer).Child = null; } } if (embeddedElement != null) { range.InsertEmbeddedUIElement(embeddedElement); return true; } } return false; } private static void PasteTextFragment(TextElement fragment, TextRange range) { Invariant.Assert(range.IsEmpty, "range must be empty at this point - emptied by a caller"); Invariant.Assert(fragment is Section || fragment is Span, "The wrapper element must be a Section or Span"); // Define insertion position. TextPointer insertionPosition = TextRangeEditTables.EnsureInsertionPosition(range.End); // Check if our insertion position has a non-splittable Inline ancestor such as Hyperlink element. // Since we cannot split such Inline, we must switch to Text mode for pasting. // Note that this also has the side effect of converting paragraph breaks to space characters. if (insertionPosition.HasNonMergeableInlineAncestor) { PasteNonMergeableTextFragment(fragment, range); } else { PasteMergeableTextFragment(fragment, range, insertionPosition); } } // Helper for PasteTextFragment private static void PasteNonMergeableTextFragment(TextElement fragment, TextRange range) { // We cannot split Hyperlink or other non-splittable inline ancestor. // Paste text content of fragment in such case. // Get text content to be pasted. string fragmentText = TextRangeBase.GetTextInternal(fragment.ElementStart, fragment.ElementEnd); // Paste text into our empty target range. // range.Text = fragmentText; // Select pasted content range.Select(range.Start, range.End); } // Helper for PasteTextFragment private static void PasteMergeableTextFragment(TextElement fragment, TextRange range, TextPointer insertionPosition) { TextPointer fragmentStart; TextPointer fragmentEnd; if (fragment is Span) { // Split structure at insertion point in target insertionPosition = TextRangeEdit.SplitFormattingElements(insertionPosition, /*keepEmptyFormatting:*/false); Invariant.Assert(insertionPosition.Parent is Paragraph, "insertionPosition must be in a scope of a Paragraph after splitting formatting elements"); // Move the whole Span into the insertion point fragment.RepositionWithContent(insertionPosition); // Store edge positions of inserted content fragmentStart = fragment.ElementStart; fragmentEnd = fragment.ElementEnd; // Remove wrapper from a tree fragment.Reposition(null, null); ValidateMergingPositions(typeof(Inline), fragmentStart, fragmentEnd); // Transfer inheritable contextual properties ApplyContextualProperties(fragmentStart, fragmentEnd, fragment); } else { // Correct leading nested List elements in the fragment CorrectLeadingNestedLists((Section)fragment); // Split a paragraph at insertion position bool needFirstParagraphMerging = SplitParagraphForPasting(ref insertionPosition); // Move the whole Section into the insertion point fragment.RepositionWithContent(insertionPosition); // Store edge positions of inserted content fragmentStart = fragment.ElementStart; fragmentEnd = fragment.ElementEnd.GetPositionAtOffset(0, LogicalDirection.Forward); // need forward orientation to stick with the following content during merge at fragmentStart position // And unwrap the root Section fragment.Reposition(null, null); ValidateMergingPositions(typeof(Block), fragmentStart, fragmentEnd); // Transfer inheritable contextual properties ApplyContextualProperties(fragmentStart, fragmentEnd, fragment); // Merge paragraphs on fragment boundaries if (needFirstParagraphMerging) { MergeParagraphsAtPosition(fragmentStart, /*mergingOnFragmentStart:*/true); } // Get an indication that we need to merge last paragraph if (!((Section)fragment).HasTrailingParagraphBreakOnPaste) { MergeParagraphsAtPosition(fragmentEnd, /*mergingOnFragmentStart:*/false); } } // // For paragraph pasting move range end to the following paragraph, because // it must include an ending paragraph break (in case of no-merging) if (fragment is Section && ((Section)fragment).HasTrailingParagraphBreakOnPaste) { fragmentEnd = fragmentEnd.GetInsertionPosition(LogicalDirection.Forward); } // Select pasted content range.Select(fragmentStart, fragmentEnd); } // Removes nested ListItems in the beginning of a fragment // to avoid multiple bulleting. // private static void CorrectLeadingNestedLists(Section fragment) { List list = fragment.Blocks.FirstBlock as List; while (list != null) { ListItem listItem = list.ListItems.FirstListItem; if (listItem == null) { return; } if (listItem.NextListItem != null) { return; } List nestedList = listItem.Blocks.FirstBlock as List; if (nestedList == null) { return; } // So we have nested list in the very beginning of the outer single-item list: // remove that outer list listItem.Reposition(null, null); list.Reposition(null, null); list = nestedList; } } // Decides whether we need to split a paragraph before pasting a fragment or not. // Splits the paragraph if needed, or simply moves the insertionPosition before its start. // Returns true if splitting happened and consequently merging is required after pasting. private static bool SplitParagraphForPasting(ref TextPointer insertionPosition) { bool needFirstParagraphMerging = true; // we need splitting unless the position os at the bery beginniong of a paragraph // When the insertion position is at the beginning of a paragraph we can avoid // splitting and then merging paragraphs at fragment start position. // This is not a pref consideration. We do not want an empty paragraph // would kill a formatting of a first pasted paragraphs (say, ListItem of a pasted List). TextPointer positionBeforeParagraph = insertionPosition; // Skip formatting tags while (positionBeforeParagraph.GetPointerContext(LogicalDirection.Backward) == TextPointerContext.ElementStart && TextSchema.IsFormattingType(positionBeforeParagraph.Parent.GetType())) { positionBeforeParagraph = positionBeforeParagraph.GetNextContextPosition(LogicalDirection.Backward); } while (positionBeforeParagraph.GetPointerContext(LogicalDirection.Backward) == TextPointerContext.ElementStart && TextSchema.AllowsParagraphMerging(positionBeforeParagraph.Parent.GetType())) { needFirstParagraphMerging = false; positionBeforeParagraph = positionBeforeParagraph.GetNextContextPosition(LogicalDirection.Backward); } if (!needFirstParagraphMerging) { // Insertion position was in the beginning of a paragraph. // No need in splitting/merging at fragment start insertionPosition = positionBeforeParagraph; } else { // split paragraph to create an insertion positionn at block level insertionPosition = TextRangeEdit.InsertParagraphBreak(insertionPosition, /*moveIntoSecondParagraph:*/false); } // When insertionPosition is inside a ListItem, then InsertParagraphBreak will // split not only a parent Paragraph but also a ListItem and return a position // between ListItems. This position is not good for inserting Block elements, // so we also need to split parent List element. // In a case when insertionPosition was at the beginning of a paragraph, // we still can end up being between ListItems, so again need to split a parent List. if (insertionPosition.Parent is List) { insertionPosition = TextRangeEdit.SplitElement(insertionPosition); } return needFirstParagraphMerging; } // Merges two paragraphs preceding and following the given position private static void MergeParagraphsAtPosition(TextPointer position, bool mergingOnFragmentStart) { TextPointer navigator = position; while (navigator != null && !(navigator.Parent is Paragraph)) { if (navigator.GetPointerContext(LogicalDirection.Backward) == TextPointerContext.ElementEnd) { navigator = navigator.GetNextContextPosition(LogicalDirection.Backward); } else { navigator = null; } } if (navigator != null) { Invariant.Assert(navigator.Parent is Paragraph, "We suppose have a first paragraph found"); Paragraph firstParagraph = (Paragraph)navigator.Parent; navigator = position; while (navigator != null && !(navigator.Parent is Paragraph)) { if (navigator.GetPointerContext(LogicalDirection.Forward) == TextPointerContext.ElementStart) { navigator = navigator.GetNextContextPosition(LogicalDirection.Forward); } else { navigator = null; } } if (navigator != null) { Invariant.Assert(navigator.Parent is Paragraph, "We suppose a second paragraph found"); Paragraph secondParagraph = (Paragraph)navigator.Parent; if (TextRangeEditLists.ParagraphsAreMergeable(firstParagraph, secondParagraph)) { TextRangeEditLists.MergeParagraphs(firstParagraph, secondParagraph); } else if (mergingOnFragmentStart && firstParagraph.TextRange.IsEmpty) { firstParagraph.RepositionWithContent(null); } else if (!mergingOnFragmentStart && secondParagraph.TextRange.IsEmpty) { secondParagraph.RepositionWithContent(null); } } } } // Validates that the sibling element at this position belong to expected itemType (Inline, Block, ListItem) private static void ValidateMergingPositions(Type itemType, TextPointer start, TextPointer end) { if (start.CompareTo(end) < 0) { // Verify inner part TextPointerContext forwardFromStart = start.GetPointerContext(LogicalDirection.Forward); TextPointerContext backwardFromEnd = end.GetPointerContext(LogicalDirection.Backward); Invariant.Assert(forwardFromStart == TextPointerContext.ElementStart, "Expecting first opening tag of pasted fragment"); Invariant.Assert(backwardFromEnd == TextPointerContext.ElementEnd, "Expecting last closing tag of pasted fragment"); Invariant.Assert(itemType.IsAssignableFrom(start.GetAdjacentElement(LogicalDirection.Forward).GetType()), "The first pasted fragment item is expected to be a " + itemType.Name); Invariant.Assert(itemType.IsAssignableFrom(end.GetAdjacentElement(LogicalDirection.Backward).GetType()), "The last pasted fragment item is expected to be a " + itemType.Name); // Veryfy outer part TextPointerContext backwardFromStart = start.GetPointerContext(LogicalDirection.Backward); TextPointerContext forwardFromEnd = end.GetPointerContext(LogicalDirection.Forward); Invariant.Assert(backwardFromStart == TextPointerContext.ElementStart || backwardFromStart == TextPointerContext.ElementEnd || backwardFromStart == TextPointerContext.None, "Bad context preceding a pasted fragment"); Invariant.Assert(!(backwardFromStart == TextPointerContext.ElementEnd) || itemType.IsAssignableFrom(start.GetAdjacentElement(LogicalDirection.Backward).GetType()), "An element preceding a pasted fragment is expected to be a " + itemType.Name); Invariant.Assert(forwardFromEnd == TextPointerContext.ElementStart || forwardFromEnd == TextPointerContext.ElementEnd || forwardFromEnd == TextPointerContext.None, "Bad context following a pasted fragment"); Invariant.Assert(!(forwardFromEnd == TextPointerContext.ElementStart) || itemType.IsAssignableFrom(end.GetAdjacentElement(LogicalDirection.Forward).GetType()), "An element following a pasted fragment is expected to be a " + itemType.Name); } } // Helper function used to set default value for an indicator requesting to merge last paragraph. private static void AdjustFragmentForTargetRange(TextElement fragment, TextRange range) { if (fragment is Section && ((Section)fragment).HasTrailingParagraphBreakOnPaste) { // Explicit indicator is missing, we need to set it by default. // In a case of TextRange.Xml property assignment we assume that // user expects to insert as many paragraphs new paragraphs as her pasted xaml contains. // The expection must be done to the case when the target range is // extended beyond the last paragraph - then we must merge last paragraph // to avoid extra paragraph creation at the end (one additional paragraph // will be created in this case by Pasting code before pasting). // The other case for exception is when target TextContainer is empty - // in this case we as well want to merge last paragraph with the following // one (which will be created as part of paragraph enforcement in pasting operation). // The both desired conditions - IsAfterLastParagraph and "in empty container" // can be identified by the following simple test - range.End is not at end-of-doc. ((Section)fragment).HasTrailingParagraphBreakOnPaste = range.End.GetPointerContext(LogicalDirection.Forward) != TextPointerContext.None; } } // Applies a whole property bag to a range from start to end to simulate inheritance of this property from source conntext private static void ApplyContextualProperties(TextPointer start, TextPointer end, TextElement propertyBag) { Invariant.Assert(propertyBag.IsEmpty && propertyBag.Parent == null, "propertyBag is supposed to be an empty element outside any tree"); LocalValueEnumerator contextualProperties = propertyBag.GetLocalValueEnumerator(); while (start.CompareTo(end) < 0 && contextualProperties.MoveNext()) { // Note: we repeatedly check for IsEmpty because the selection // may become empty as a result of normalization after formatting // (thai character sequence). LocalValueEntry propertyEntry = contextualProperties.Current; DependencyProperty property = propertyEntry.Property; if (TextSchema.IsCharacterProperty(property) && TextSchema.IsParagraphProperty(property)) { // In case a property is both an Inline and Paragraph property, // propertyBag element type (section or span) decides how it should be applied. if (TextSchema.IsBlock(propertyBag.GetType())) { ApplyContextualProperty(typeof(Block), start, end, property, propertyEntry.Value); } else { ApplyContextualProperty(typeof(Inline), start, end, property, propertyEntry.Value); } } else if (TextSchema.IsCharacterProperty(property)) { ApplyContextualProperty(typeof(Inline), start, end, property, propertyEntry.Value); } else if (TextSchema.IsParagraphProperty(property)) { ApplyContextualProperty(typeof(Block), start, end, property, propertyEntry.Value); } } // Merge formatting elements at end position TextRangeEdit.MergeFormattingInlines(start); TextRangeEdit.MergeFormattingInlines(end); } // Applies one property to a range from start to end to simulate inheritance of this property from source conntext private static void ApplyContextualProperty(Type targetType, TextPointer start, TextPointer end, DependencyProperty property, object value) { if (TextSchema.ValuesAreEqual(start.Parent.GetValue(property), value)) { return; // The property at insertion position is the same as it was in source context. Nothing to do. } // Advance start pointer to enter pasted fragment start = start.GetNextContextPosition(LogicalDirection.Forward); while (start != null && start.CompareTo(end) < 0) { TextPointerContext passedContext = start.GetPointerContext(LogicalDirection.Backward); if (passedContext == TextPointerContext.ElementStart) { TextElement element = (TextElement)start.Parent; // Check if this element affects the property in question if (element.ReadLocalValue(property) != DependencyProperty.UnsetValue || !TextSchema.ValuesAreEqual(element.GetValue(property), element.Parent.GetValue(property))) { // The element affects this property, so we can skip it start = element.ElementEnd; } else if (targetType.IsAssignableFrom(element.GetType())) { start = element.ElementEnd; if (targetType == typeof(Block) && start.CompareTo(end) > 0) { // Contextual properties should not apply to the last paragraph // when it is merged with the following content - // to avoid affecting the folowing visible content formatting. break; } // This is topmost-level inline element which inherits this property. // Set the value explicitly if (!TextSchema.ValuesAreEqual(value, element.GetValue(property))) { element.ClearValue(property); if (!TextSchema.ValuesAreEqual(value, element.GetValue(property))) { element.SetValue(property, value); } TextRangeEdit.MergeFormattingInlines(element.ElementStart); } } else { // Traverse down into a structured (non-innline) element start = start.GetNextContextPosition(LogicalDirection.Forward); } } else { // Traverse up from any element Invariant.Assert(passedContext != TextPointerContext.None, "TextPointerContext.None is not expected"); start = start.GetNextContextPosition(LogicalDirection.Forward); } } } // Returns a navigator scoped in the common ancestor for the start and end positions // The navigator is positioned in the beginning of the ancestor's content. // Modifies the common ancestor for hyperlink serialization heuristic - in case when the range is positioned at hyperlink boundaries. // Since we need to write a hyperlink wrapper in this case, navigator is positioned before hyperlink element start. private static ITextPointer FindSerializationCommonAncestor(ITextRange range) { // Create navigators for tree traversing looking for commonAncestor ITextPointer commonAncestor = range.Start.CreatePointer(); ITextPointer runningEnd = range.End.CreatePointer(); // Find nearest common ancestor while (!commonAncestor.HasEqualScope(runningEnd)) { // Run all way from end up the tree to check if start is ancestor runningEnd.MoveToPosition(range.End); while (typeof(TextElement).IsAssignableFrom(runningEnd.ParentType) && !runningEnd.HasEqualScope(commonAncestor)) { runningEnd.MoveToElementEdge(ElementEdge.AfterEnd); } if (runningEnd.HasEqualScope(commonAncestor)) { break; } // Move start one level up commonAncestor.MoveToElementEdge(ElementEdge.BeforeStart); } while (!IsAcceptableAncestor(commonAncestor, range)) { commonAncestor.MoveToElementEdge(ElementEdge.BeforeStart); } if (typeof(TextElement).IsAssignableFrom(commonAncestor.ParentType)) { commonAncestor.MoveToElementEdge(ElementEdge.AfterStart); // Check for special case, when range start and end are at Hyperlink boundaries. // Need to expand commonAncestor to Hyperlink element start, so that Hyperlink wrapper is written. ITextPointer hyperlinkStart = GetHyperlinkStart(range); if (hyperlinkStart != null) { commonAncestor = hyperlinkStart; } } else { commonAncestor.MoveToPosition(commonAncestor.TextContainer.Start); } return commonAncestor; } // Verify that a pointer is an acceptable ancestor. Some types can't be ancestors at all, while // non-typographic-only elements are unacceptable if the range being serialized does not include the // element's start and end (because we don't want to serialize properties on such an element). private static bool IsAcceptableAncestor(ITextPointer commonAncestor, ITextRange range) { if (typeof(TableRow).IsAssignableFrom(commonAncestor.ParentType) || typeof(TableRowGroup).IsAssignableFrom(commonAncestor.ParentType) || typeof(Table).IsAssignableFrom(commonAncestor.ParentType) || typeof(BlockUIContainer).IsAssignableFrom(commonAncestor.ParentType) || typeof(List).IsAssignableFrom(commonAncestor.ParentType) || typeof(Inline).IsAssignableFrom(commonAncestor.ParentType) && TextSchema.HasTextDecorations(commonAncestor.GetValue(Inline.TextDecorationsProperty))) { return false; } // We don't want to use any formatting from within a non-typographic-only element unless the entire // element is selected (in which case, the ancestor candidate will already be outside that element. // If there is such an element ANYWHERE in the ancestry, the only acceptable // ancestor is outside the outermost such element. ITextPointer navigator = commonAncestor.CreatePointer(); while (typeof(TextElement).IsAssignableFrom(navigator.ParentType)) { TextElementEditingBehaviorAttribute behaviorAttribute = (TextElementEditingBehaviorAttribute)Attribute.GetCustomAttribute(navigator.ParentType, typeof(TextElementEditingBehaviorAttribute)); if (behaviorAttribute != null && !behaviorAttribute.IsTypographicOnly) { return false; } navigator.MoveToElementEdge(ElementEdge.BeforeStart); } return true; } // Removes (inplace) any invalid surrogate chars from an array. // Returns the new array length, text.Length minus any stripped // chars. // // Unicode surrogates are 32 bit references to abstract chars. // A valid surrogate pair consists of a 16 bit code point (the // high surrogate) in the range u+d800 - u+dbff, followed by a // second 16 bit code point (the low surrogate) in the range // u+dc00 - u+dfff. // // An invalid surrogate is a high surrogate not followed by a // low surrogate, or a low surrogate not preceeded by a high // surrogate. // // Length specifies the number of characters in text to examine, // characters past length are ignored (as if text.Length == length). // // Also removes nul characters private static int StripInvalidSurrogateChars(char[] text, int length) { int count; Invariant.Assert(text.Length >= length, "Asserting that text.Length >= length"); int i; for (i = 0; i < length; i++) { char testChar = text[i]; if (Char.IsHighSurrogate(testChar) || Char.IsLowSurrogate(testChar) || IsBadCode(testChar)) { break; } } if (i == length) { // No surrogates, early out. count = length; } else { count = i; for (; i < length; i++) { if (Char.IsHighSurrogate(text[i])) { if (i + 1 < length && Char.IsLowSurrogate(text[i + 1])) { // Valid surrogate encountered. text[count] = text[i]; text[count + 1] = text[i + 1]; count += 2; i++; // Skip over low surrogate. } else { // Bogus high surrogte encountered -- remove it. // Simply don't update destinationIndex or count. } } else if (Char.IsLowSurrogate(text[i])) { // Bogus low surrogate encountered -- remove it. // Simply don't update destinationIndex or count. } else if (IsBadCode(text[i])) { // nul character enountered - remove it. // Simply don't update destinationIndex or count. // } else { // Non-surrogate encountered. text[count] = text[i]; count += 1; } } } return count; } private static bool IsBadCode(char code) { return (code < ' ' && code != '\x0009' && code != '\x000A' && code != '\x000D'); } ////// Return true if rangeEnd is not at the end of an element. /// /// textReader must already be at the start of the element. /// private static bool IsPartialNonTypographic(ITextPointer textReader, ITextPointer rangeEnd) { bool isPartial = false; Invariant.Assert(textReader.GetPointerContext(LogicalDirection.Forward) == TextPointerContext.ElementStart); ITextPointer elementNavigation = textReader.CreatePointer(); ITextPointer elementEnd = textReader.CreatePointer(); elementEnd.MoveToNextContextPosition(LogicalDirection.Forward); // Find the end position elementEnd.MoveToElementEdge(ElementEdge.AfterEnd); if (elementEnd.CompareTo(rangeEnd) > 0) { isPartial = true; } return isPartial; } ////// Return true if Hyperlink range is invalid. /// Hyperlink is invalid if it include a UiElement except Image or the range end position /// is stated before the end position of hyperlink. /// This must be called before Hyperlink start element position. /// private static bool IsHyperlinkInvalid(ITextPointer textReader, ITextPointer rangeEnd) { // TextRead must be on the position before the element start position of Hyperlink Invariant.Assert(textReader.GetPointerContext(LogicalDirection.Forward) == TextPointerContext.ElementStart); Invariant.Assert(typeof(Hyperlink).IsAssignableFrom(textReader.GetElementType(LogicalDirection.Forward))); bool hyperlinkInvalid = false; // Get the forward adjacent element and cast Hyperlink hardly since it must be Hyperlink Hyperlink hyperlink = (Hyperlink)textReader.GetAdjacentElement(LogicalDirection.Forward); ITextPointer hyperlinkNavigation = textReader.CreatePointer(); ITextPointer hyperlinkEnd = textReader.CreatePointer(); hyperlinkEnd.MoveToNextContextPosition(LogicalDirection.Forward); // Find the hyperlink end position hyperlinkEnd.MoveToElementEdge(ElementEdge.AfterEnd); // Hyperlink end position is stated after the range end position. if (hyperlinkEnd.CompareTo(rangeEnd) > 0) { hyperlinkInvalid = true; } else { // Check whether the hyperlink having a UiElement except Image until hyperlink end position while (hyperlinkNavigation.CompareTo(hyperlinkEnd) < 0) { InlineUIContainer inlineUIContainer = hyperlinkNavigation.GetAdjacentElement(LogicalDirection.Forward) as InlineUIContainer; if (inlineUIContainer != null && !(inlineUIContainer.Child is Image)) { hyperlinkInvalid = true; break; } hyperlinkNavigation.MoveToNextContextPosition(LogicalDirection.Forward); } } return hyperlinkInvalid; } // Returns a position before hyperlink element start if passed range start and end are at hyperlink boundaries. Otherwise, null. private static ITextPointer GetHyperlinkStart(ITextRange range) { ITextPointer hyperlinkStart = null; if (TextPointerBase.IsAtNonMergeableInlineStart(range.Start) && TextPointerBase.IsAtNonMergeableInlineEnd(range.End)) { // Find a position at hyperlink start. hyperlinkStart = range.Start.CreatePointer(LogicalDirection.Forward); while (hyperlinkStart.GetPointerContext(LogicalDirection.Backward) == TextPointerContext.ElementStart && !typeof(Hyperlink).IsAssignableFrom(hyperlinkStart.ParentType)) { hyperlinkStart.MoveToElementEdge(ElementEdge.BeforeStart); } hyperlinkStart.MoveToElementEdge(ElementEdge.BeforeStart); hyperlinkStart.Freeze(); } return hyperlinkStart; } // private static string FilterNaNStringValueForDoublePropertyType(string stringValue, Type propertyType) { if (propertyType == typeof(double) && String.Compare(stringValue, "NaN", StringComparison.OrdinalIgnoreCase) == 0) { return "Auto"; // convert NaN to Auto, to keep parser happy } return stringValue; } #endregion Private Methods // -------------------------------------------------------------- // // Private Constants // // -------------------------------------------------------------- #region Private Constants // A structural depth of a empty FlowDocument fragment private const int EmptyDocumentDepth = 1; #endregion Private Constants } } // File provided for Reference Use Only by Microsoft Corporation (c) 2007. // Copyright (c) Microsoft Corporation. All rights reserved.
Link Menu

This book is available now!
Buy at Amazon US or
Buy at Amazon UK
- _ChunkParse.cs
- LeaseManager.cs
- RegistryKey.cs
- XmlBinaryWriter.cs
- XmlSchemaRedefine.cs
- WinFormsSpinner.cs
- CounterSet.cs
- CultureInfo.cs
- DataContract.cs
- InitiatorSessionSymmetricMessageSecurityProtocol.cs
- CngKeyBlobFormat.cs
- ThreadWorkerController.cs
- DeferredElementTreeState.cs
- NamespaceEmitter.cs
- FontDriver.cs
- PngBitmapEncoder.cs
- Tracer.cs
- AsynchronousChannelMergeEnumerator.cs
- DynamicMethod.cs
- Window.cs
- ClientProxyGenerator.cs
- DocumentOrderQuery.cs
- Polygon.cs
- WorkflowServiceHostFactory.cs
- HtmlInputReset.cs
- WebPartVerbsEventArgs.cs
- EntityCodeGenerator.cs
- RoleService.cs
- FontStyleConverter.cs
- FontCollection.cs
- CheckBox.cs
- DropShadowEffect.cs
- SmiEventSink.cs
- _NativeSSPI.cs
- TextTreeRootTextBlock.cs
- LinearQuaternionKeyFrame.cs
- XsdValidatingReader.cs
- TypeDescriptionProviderAttribute.cs
- DataServiceClientException.cs
- Object.cs
- RC2.cs
- RegexWriter.cs
- DataPagerFieldCollection.cs
- BaseCodePageEncoding.cs
- XmlName.cs
- XamlBrushSerializer.cs
- BindingListCollectionView.cs
- TextBox.cs
- AttachedPropertyBrowsableAttribute.cs
- WindowsNonControl.cs
- EnumConverter.cs
- Int32RectConverter.cs
- TdsParserSafeHandles.cs
- ContentPosition.cs
- EntityCommand.cs
- VisualBrush.cs
- AppDomainProtocolHandler.cs
- X509AudioLogo.cs
- storepermission.cs
- AbsoluteQuery.cs
- JoinGraph.cs
- TreeNodeClickEventArgs.cs
- OracleEncoding.cs
- LoginView.cs
- AmbientValueAttribute.cs
- BitmapSource.cs
- SafeSystemMetrics.cs
- StyleBamlRecordReader.cs
- Separator.cs
- DefaultMemberAttribute.cs
- FontStyles.cs
- StylusDownEventArgs.cs
- XsltSettings.cs
- BrowserDefinition.cs
- HtmlInputImage.cs
- ApplySecurityAndSendAsyncResult.cs
- SelectorAutomationPeer.cs
- RecognitionEventArgs.cs
- MenuCommands.cs
- TreeViewDesigner.cs
- SecurityCriticalDataForSet.cs
- HttpListenerContext.cs
- BindingMAnagerBase.cs
- LogReservationCollection.cs
- Quaternion.cs
- securitycriticaldataClass.cs
- ZoneMembershipCondition.cs
- AssemblyNameProxy.cs
- DeviceFilterEditorDialog.cs
- ExtenderControl.cs
- CounterCreationData.cs
- DataGridViewCellConverter.cs
- DataBindingCollection.cs
- ProcessThreadDesigner.cs
- ServiceOperationParameter.cs
- WorkerRequest.cs
- FontWeightConverter.cs
- XslAstAnalyzer.cs
- DataListCommandEventArgs.cs
- StylusSystemGestureEventArgs.cs