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:
- 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).
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:
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.
- 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.