Insert contents from the one FlowDocument into another when using XamlReader and XamlWriter
Asked Answered
M

2

8

I use FlowDocument with BlockUIContainer and InlineUIContainer elements containing (or as base classes) some custom blocks - SVG, math formulas etc. Because of that using Selection.Load(stream, DataFormats.XamlPackage) wont work as the serialization will drop the contents of *UIContainers except if the Child property is an image as available in Microsoft reference source:

private static void WriteStartXamlElement(...)
{
    ...
    if ((inlineUIContainer == null || !(inlineUIContainer.Child is Image)) &&
                (blockUIContainer == null || !(blockUIContainer.Child is Image)))
    {
        ...
        elementTypeStandardized = TextSchema.GetStandardElementType(elementType, /*reduceElement:*/true);
    }
    ...
}

The only option in this case is to use is to use XamlWriter.Save and XamlReader.Load which are working flawlessly, serialize and deserialize all required properties and objects of a FlowDocument yet the Copy+Paste must be implemented manually as default implementation of Copy+Paste uses Selection.Load/Save.

Copy/Paste is critical as it is also used to handle dragging of elements in or between RichTextBox controls - the only way objects can be manipulated without custom dragging code.

This is why I am looking to implement copy/paste using a FlowDocument serialization but unfortunately there are some issues with it:

  1. In current solution a whole FlowDocument object needs to be serialized/deserialized. Performance-wise it should not be a problem but I need to store information what selection range needs to be pasted from it (CustomRichTextBoxTag class).
  2. Apparently objects cannot be removed from one document and added to another (a dead-end I discovered recently): 'InlineCollection' element cannot be inserted in a tree because it is already a child of a tree.

    [TextElementCollection.cs]
    public void InsertAfter(TextElementType previousSibling, TextElementType newItem)
    {
        ...
        if (previousSibling.Parent != this.Parent)
            throw new InvalidOperationException(System.Windows.SR.Get("TextElementCollection_PreviousSiblingDoesNotBelongToThisCollection", new object[1]
            {
                (object) previousSibling.GetType().Name
            }));
        ...
    }
    

    I think about setting FrameworkContentElement._parent using reflection in all elements which need to be moved to another document but that's a last resort hackish and dirty solution:

  3. In theory I can copy only required objects: (optional) partial run with text at the beginning of selection, all paragraphs and inlines in between and and (possibly) partial run at the end, encapsulate these in a custom class and serialize/deserialize using XamlReader/XamlWriter.

  4. Another solution I didn't think about.

Here is the custom RichTextBox control implementation with partially working custom Copy/Paste code:

using System.IO;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Markup;

namespace FlowMathTest
{
    public class CustomRichTextBoxTag: DependencyObject
    {
        public static readonly DependencyProperty SelectionStartProperty = DependencyProperty.Register(
            "SelectionStart",
            typeof(int),
            typeof(CustomRichTextBoxTag));

        public int SelectionStart
        {
            get { return (int)GetValue(SelectionStartProperty); }
            set { SetValue(SelectionStartProperty, value); }
        }

        public static readonly DependencyProperty SelectionEndProperty = DependencyProperty.Register(
            "SelectionEnd",
            typeof(int),
            typeof(CustomRichTextBoxTag));

        public int SelectionEnd
        {
            get { return (int)GetValue(SelectionEndProperty); }
            set { SetValue(SelectionEndProperty, value); }
        }
    }

    public class CustomRichTextBox: RichTextBox
    {
        public CustomRichTextBox()
        {
            DataObject.AddCopyingHandler(this, OnCopy);
            DataObject.AddPastingHandler(this, OnPaste);
        }

        protected override void OnSelectionChanged(RoutedEventArgs e)
        {
            base.OnSelectionChanged(e);
            var tag = Document.Tag as CustomRichTextBoxTag;
            if(tag == null)
            {
                tag = new CustomRichTextBoxTag();
                Document.Tag = tag;
            }
            tag.SelectionStart = Document.ContentStart.GetOffsetToPosition(Selection.Start);
            tag.SelectionEnd = Document.ContentStart.GetOffsetToPosition(Selection.End);
        }

        private void OnCopy(object sender, DataObjectCopyingEventArgs e)
        {
            if(e.DataObject != null)
            {
                e.Handled = true;
                var ms = new MemoryStream();
                XamlWriter.Save(Document, ms);
                e.DataObject.SetData(DataFormats.Xaml, ms);
            }
        }

        private void OnPaste(object sender, DataObjectPastingEventArgs e)
        {
            var xamlData = e.DataObject.GetData(DataFormats.Xaml) as MemoryStream;
            if(xamlData != null)
            {
                xamlData.Position = 0;
                var fd = XamlReader.Load(xamlData) as FlowDocument;
                if(fd != null)
                {
                    var tag = fd.Tag as CustomRichTextBoxTag;
                    if(tag != null)
                    {
                        InsertAt(Document, Selection.Start, Selection.End, fd, fd.ContentStart.GetPositionAtOffset(tag.SelectionStart), fd.ContentStart.GetPositionAtOffset(tag.SelectionEnd));
                        e.Handled = true;
                    }
                }
            }
        }

        public static void InsertAt(FlowDocument destDocument, TextPointer destStart, TextPointer destEnd, FlowDocument sourceDocument, TextPointer sourceStart, TextPointer sourceEnd)
        {
            var destRange = new TextRange(destStart, destEnd);
            destRange.Text = string.Empty;

            // insert partial text of the first run in the selection
            if(sourceStart.GetPointerContext(LogicalDirection.Forward) == TextPointerContext.Text)
            {
                var sourceRange = new TextRange(sourceStart, sourceStart.GetNextContextPosition(LogicalDirection.Forward));
                destStart.InsertTextInRun(sourceRange.Text);
                sourceStart = sourceStart.GetNextContextPosition(LogicalDirection.Forward);
                destStart = destStart.GetNextContextPosition(LogicalDirection.Forward);
            }

            var field = typeof(FrameworkContentElement).GetField("_parent", BindingFlags.NonPublic | BindingFlags.Instance);
            while(sourceStart != null && sourceStart.CompareTo(sourceEnd) <= 0 && sourceStart.Paragraph != null)
            {
                var sourceInline = sourceStart.Parent as Inline;
                if(sourceInline != null)
                {
                    sourceStart.Paragraph.Inlines.Remove(sourceInline);
                    if(destStart.Parent is Inline)
                    {
                        field.SetValue(sourceInline, null);
                        destStart.Paragraph.Inlines.InsertAfter(destStart.Parent as Inline, sourceInline);
                    }
                    else
                    {
                        var p = new Paragraph();
                        destDocument.Blocks.InsertAfter(destStart.Paragraph, p);
                        p.Inlines.Add(sourceInline);
                    }
                    sourceStart = sourceStart.GetNextContextPosition(LogicalDirection.Forward);
                }
                else
                {
                    var sourceBlock = sourceStart.Parent as Block;
                    field.SetValue(sourceBlock, null);
                    destDocument.Blocks.InsertAfter(destStart.Paragraph, sourceBlock);
                    sourceStart = sourceStart.GetNextContextPosition(LogicalDirection.Forward);
                }
            }
        }
    }
}

And the question - is there an existing solution for custom Copy+Paste code for FlowDocument using XamlReader and XamlWriter? How to fix the code above so it won't complain about different FlowDocument object or work around this limitation?

EDIT: As an experiment I implemented 2) so that objects can be moved from one FlowDocument to another. The code above is updated - all references to the "field" variable.

Malemute answered 4/9, 2014 at 9:52 Comment(8)
if I can understand you want to copy content of a flow document ot another and you've tried xaml save/load, serialize/deserialize, copy/paste and drag. have you also tried/though of re-creating the document?Gilmore
@pushpraj: if by recreating you mean inserting a part of FlowDocument deserialized after pasting into another FlowDocument by copying the structure and all properties using a visitor and a clone method than no, I have not considered it as I don't know about a straightforward and reliable method of cloning FlowDocument objects structure (tables, custom objects, paragraphs and runs with all their properties) without serailizing/deserializing a whole lot.Balliett
visitor is a good choice if you can predict the expected elements. rest is just about blocks & inlines.Gilmore
It is though just a part of solution and cloning each visited object is a problem - these would need to be serialized and deserialized again or a whole structure serialized into a custom tree-like structure when copying. During the process code would need to consider all the issues with partial selections of text Runs. I think it's doable but pretty complex as it would basically mean re-implementing Microsoft's FlowDocument positioning code.Balliett
Now I'm experimenting how to get around TextRange limitations: inherit from InlineUIContainer and BlockUIContainer and make these classes serialize their respective child UIElement but I'm not sure how if this is not a dead-end.Balliett
What's the problem again? You want to implement a copy paste based on a selection that'll be moved to another FlowDocument. Is that correct?Credulous
@lll: That's correct, It would need to be Copy + paste within the same RichTextBox (dragging), different RichTextBoxes while keeping intact (cloned) all UIElement objects within the copied/pasted text (contained in BlockUIContainer and InlineUIContainer) i.e. Button or custom controlsBalliett
@too And you want it to paste wherever the caret is on the second rtb's selection, correct? If that's so give me time I'll give you an answerCredulous
M
4

It seems the bounty period is about to expire and I made a breakthrough how to implement the above problem so I will share it here.

First of all TextRange.Save has a "preserveTextElements" argument which can be used to serialize InlineUIContainer and BlockUIContainer elements. Also, both these controls are not sealed so can be used as base classes for a custom TextElement implementation.

With the above in mind:

  1. I created an InlineMedia element inherited from InlineUIContainer which serializes it's Child "manually" into a "ChildSource" dependency property using XamlReader and XamlWriter and hides the original "Child" from default serializer

  2. I changed the above implementation of CustomRichTextBox to copy selection using range.Save(ms, DataFormats.Xaml, true).

As you can notice, no special Paste handling is necessary as Xaml is nicely deserialized after swapping original Xaml in the clipboard and this means dragging works as copy from all CustomRichtextBox controls and Paste works even into normal RichTextBox.

The only limitation is that for all InlineMedia controls the ChildSource property need to be updated by serializing it's Child before serializing a whole document and I found no way to do it automatically (hook into TextRange.Save before element is saved).

I can live with that but a nicer solution without this problem will still get a bounty!

InlineMedia element code:

public class InlineMedia: InlineUIContainer
{
    public InlineMedia()
    {
    }

    public InlineMedia(UIElement childUIElement) : base(childUIElement)
    {
        UpdateChildSource();
    }

    public InlineMedia(UIElement childUIElement, TextPointer insertPosition)
        : base(childUIElement, insertPosition)
    {
        UpdateChildSource();
    }

    public static readonly DependencyProperty ChildSourceProperty = DependencyProperty.Register
    (
        "ChildSource",
        typeof(string),
        typeof(InlineMedia),
        new FrameworkPropertyMetadata(null, OnChildSourceChanged));

    public string ChildSource
    {
        get
        {
            return (string)GetValue(ChildSourceProperty);
        }
        set
        {
            SetValue(ChildSourceProperty, value);
        }
    }

    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    public new UIElement Child
    {
        get
        {
            return base.Child;
        }
        set
        {
            base.Child = value;
            UpdateChildSource();
        }
    }

    public void UpdateChildSource()
    {
        IsInternalChildSourceChange = true;
        try
        {
            ChildSource = Save();
        }
        finally
        {
            IsInternalChildSourceChange = false;
        }
    }


    public string Save()
    {
        if(Child == null)
        {
            return null;
        }

        using(var stream = new MemoryStream())
        {
            XamlWriter.Save(Child, stream);
            stream.Position = 0;
            using(var reader = new StreamReader(stream, Encoding.UTF8))
            {
                return reader.ReadToEnd();
            }
        }
    }

    public void Load(string sourceData)
    {
        if(string.IsNullOrEmpty(sourceData))
        {
            base.Child = null;
        }
        else
        {
            using(var stream = new MemoryStream(Encoding.UTF8.GetBytes(sourceData)))
            {
                var child = XamlReader.Load(stream);
                base.Child = (UIElement)child;
            }
        }
    }

    private static void OnChildSourceChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
    {
        var img = (InlineMedia) sender;
        if(img != null && !img.IsInternalChildSourceChange)
        {
            img.Load((string)e.NewValue);
        }
    }

    protected bool IsInternalChildSourceChange { get; private set; }
}

CustomRichTextBox control code:

public class CustomRichTextBox: RichTextBox
{
    public CustomRichTextBox()
    {
        DataObject.AddCopyingHandler(this, OnCopy);
    }

    private void OnCopy(object sender, DataObjectCopyingEventArgs e)
    {
        if(e.DataObject != null)
        {
            UpdateDocument();
            var range = new TextRange(Selection.Start, Selection.End);
            using(var ms = new MemoryStream())
            {
                range.Save(ms, DataFormats.Xaml, true);
                ms.Position = 0;
                using(var reader = new StreamReader(ms, Encoding.UTF8))
                {
                    var xaml = reader.ReadToEnd();
                    e.DataObject.SetData(DataFormats.Xaml, xaml);
                }
            }
            e.Handled = true;
        }
    }

    public void UpdateDocument()
    {
        ObjectHelper.ExecuteRecursive<InlineMedia>(Document, i => i.UpdateChildSource(), FlowDocumentVisitors);
    }

    private static readonly Func<object, object>[] FlowDocumentVisitors =
    {
        x => (x is FlowDocument) ? ((FlowDocument) x).Blocks : null,
        x => (x is Section) ? ((Section) x).Blocks : null,
        x => (x is BlockUIContainer) ? ((BlockUIContainer) x).Child : null,
        x => (x is InlineUIContainer) ? ((InlineUIContainer) x).Child : null,
        x => (x is Span) ? ((Span) x).Inlines : null,
        x => (x is Paragraph) ? ((Paragraph) x).Inlines : null,
        x => (x is Table) ? ((Table) x).RowGroups : null,
        x => (x is Table) ? ((Table) x).Columns : null,
        x => (x is Table) ? ((Table) x).RowGroups.SelectMany(rg => rg.Rows) : null,
        x => (x is Table) ? ((Table) x).RowGroups.SelectMany(rg => rg.Rows).SelectMany(r => r.Cells) : null,
        x => (x is TableCell) ? ((TableCell) x).Blocks : null,
        x => (x is TableCell) ? ((TableCell) x).BorderBrush : null,
        x => (x is List) ? ((List) x).ListItems : null,
        x => (x is ListItem) ? ((ListItem) x).Blocks : null
    };
}

and finally ObjectHelper class - a visitor helper:

public static class ObjectHelper
{
    public static void ExecuteRecursive(object item, Action<object> execute, params Func<object, object>[] childSelectors)
    {
        ExecuteRecursive<object, object>(item, null, (c, i) => execute(i), childSelectors);
    }

    public static void ExecuteRecursive<TObject>(object item, Action<TObject> execute, params Func<object, object>[] childSelectors)
    {
        ExecuteRecursive<object, TObject>(item, null, (c, i) => execute(i), childSelectors);
    }

    public static void ExecuteRecursive<TContext, TObject>(object item, TContext context, Action<TContext, TObject> execute, params Func<object, object>[] childSelectors)
    {
        ExecuteRecursive(item, context, (c, i) =>
        {
            if(i is TObject)
            {
                execute(c, (TObject)i);
            }
        }, childSelectors);
    }

    public static void ExecuteRecursive<TContext>(object item, TContext context, Action<TContext, object> execute, params Func<object, object>[] childSelectors)
    {
        execute(context, item);
        if(item is IEnumerable)
        {
            foreach(var subItem in item as IEnumerable)
            {
                ExecuteRecursive(subItem, context, execute, childSelectors);
            }
        }
        if(childSelectors != null)
        {
            foreach(var subItem in childSelectors.Select(x => x(item)).Where(x => x != null))
            {
                ExecuteRecursive(subItem, context, execute, childSelectors);
            }
        }
    }
}
Malemute answered 15/9, 2014 at 10:55 Comment(0)
B
1

1.In current solution a whole FlowDocument object needs to be serialized/deserialized. Performance-wise it should not be a problem but I need to store information what selection range needs to be pasted from it (CustomRichTextBoxTag class).

This smells like an opportunity to use an attached property based on the intended behavior you identified. I understand attached properties as a way of adding additional behavior to an element. When you register an attached property, you can add an event handler for when that property value changes. To take advantage of this, I would wire this attached property to a DataTrigger to update the selection range value for your copy/paste operation.

2.Apparently objects cannot be removed from one document and added to another (a dead-end I discovered recently): 'InlineCollection' element cannot be inserted in a tree because it is already a child of a tree.

You can get around this by constructing your elements programmatically and also removing your elements programmatically. At the end of the day, you're mainly dealing with either an ItemsControl or a ContentControl. In this case your working with an ItemsControl (i.e. document). As a result just add and remove child elements from your ItemsControl (document) programmatically.

Bunyip answered 14/9, 2014 at 11:41 Comment(2)
+1 as your first suggestion is a good idea how to add new information (if necessary in the solution) to the FlowDocument, thanks for that, although it does not solve the original problem. The second suggestion creates a new problem - constructing elements programmatically won't copy all properties of original, copied elements.Balliett
Reference this link for copying xaml elements. #1969125Bunyip

© 2022 - 2024 — McMap. All rights reserved.