Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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.

like image 445
too Avatar asked Sep 04 '14 09:09

too


2 Answers

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);
            }
        }
    }
}
like image 167
too Avatar answered Oct 19 '22 21:10

too


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.

like image 24
Scott Nimrod Avatar answered Oct 19 '22 21:10

Scott Nimrod