使用XamlReader和XamlWriter时将一个FlowDocument的内容插入另一个FlowDocument中

8

我使用FlowDocument与包含(或作为基类)一些自定义块 - SVG、数学公式等的BlockUIContainer和InlineUIContainer元素。 因此,使用Selection.Load(stream, DataFormats.XamlPackage)无法正常工作,因为序列化将删除*UIContainers的内容,除非Child属性是图像,如Microsoft参考源代码中提供的那样:

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

在这种情况下,唯一的选择是使用XamlWriter.Save和XamlReader.Load进行序列化和反序列化所需的FlowDocument属性和对象。然而,默认的复制/粘贴实现使用Selection.Load/Save,因此必须手动实现Copy+Paste。复制/粘贴非常关键,因为它还用于处理RichTextBox控件中元素的拖放 - 这是在不使用自定义代码的情况下操作对象的唯一方式。
这就是我想要使用FlowDocument序列化实现复制/粘贴的原因,但不幸的是,它存在一些问题。
  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.

这里是自定义RichTextBox控件的实现,其中包含部分可用的自定义复制/粘贴代码:

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);
                }
            }
        }
    }
}

问题是:是否存在使用XamlReader和XamlWriter为FlowDocument自定义Copy+Paste代码的现有解决方案?如何解决此限制或绕过此限制?

编辑:作为一个实验,我实现了2),以便可以将对象从一个FlowDocument移动到另一个FlowDocument。上面的代码已更新-所有对“field”变量的引用。


如果我理解正确,您想将一个流文档的内容复制到另一个文档中,并尝试了XAML保存/加载、序列化/反序列化、复制/粘贴和拖放。您是否也考虑过重新创建文档? - pushpraj
如果您能预测预期的元素,visitor是一个不错的选择。其余的只涉及块和内联。 - pushpraj
这只是解决方案的一部分,克隆每个访问的对象都是一个问题 - 这些需要再次进行序列化和反序列化,或者在复制时将整个结构序列化为自定义树状结构。在此过程中,代码需要考虑到文本运行的部分选择的所有问题。我认为这是可行的,但相当复杂,因为它基本上意味着重新实现Microsoft的FlowDocument定位代码。 - too
现在我正在尝试如何绕过TextRange的限制:从InlineUIContainer和BlockUIContainer继承,并使这些类序列化它们各自的子UIElement,但如果这不是死路,我不确定如何做。 - too
问题是什么?您想要基于所选内容实现复制粘贴,将其移动到另一个FlowDocument中。是这样吗? - 123 456 789 0
显示剩余3条评论
2个回答

4
似乎赏金期即将到期,我已经有了如何实现上述问题的突破口,所以我将在这里分享它。
首先,TextRange.Save有一个"preserveTextElements"参数,可用于序列化InlineUIContainer和BlockUIContainer元素。此外,这两个控件都没有密封,因此可以用作自定义TextElement实现的基类。
考虑到以上情况:
  1. 我创建了一个继承自InlineUIContainer的InlineMedia元素,它将其Child手动序列化到“ChildSource”依赖属性中,并使用XamlReader和XamlWriter隐藏原始“Child”。

  2. 我改变了CustomRichTextBox的上述实现,使用range.Save(ms, DataFormats.Xaml, true)来复制选择。

正如您所注意到的,不需要特殊的粘贴处理,因为在交换剪贴板中的原始Xaml后,Xaml会被很好地反序列化,这意味着从所有CustomRichtextBox控件复制的拖动操作均有效,甚至可以将其粘贴到普通的RichTextBox中。
唯一的限制是对于所有InlineMedia控件,需要在序列化整个文档之前更新ChildSource属性,我找不到自动执行此操作的方法(在元素保存之前挂钩到TextRange.Save中)。
我可以接受这一点,但一个没有这个问题的更好的解决方案仍然会得到赏金!
InlineMedia元素代码:
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控件代码:

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
    };
}

最后是ObjectHelper类——一个访问者助手:
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);
            }
        }
    }
}

1

1.当前解决方案需要序列化/反序列化整个FlowDocument对象。从性能上来说,这不应该是一个问题,但我需要存储信息,以便从中粘贴选择范围(CustomRichTextBoxTag类)。

这似乎是一种使用基于预期行为的附加属性的机会。我了解附加属性作为向元素添加附加行为的一种方式。当您注册附加属性时,可以添加一个事件处理程序,用于更改该属性值的情况。 为了利用这一点,我将将此附加属性连接到DataTrigger以更新复制/粘贴操作的选择范围值。

2.显然无法从一个文档中删除对象并将其添加到另一个文档中(我最近发现的死胡同):“InlineCollection”元素无法插入树中,因为它已经是树的子项。

您可以通过以编程方式构建元素并以编程方式删除元素来解决此问题。归根结底,您主要处理的是ItemsControl或ContentControl。在这种情况下,您正在使用ItemsControl(即文档)。因此,只需以编程方式添加和删除子元素即可。


+1 作为您的第一个建议是如何向 FlowDocument 添加新信息的好主意(如果需要在解决方案中),感谢您的建议,尽管它并没有解决原始问题。第二个建议会创建一个新问题-以编程方式构造元素不会复制原始复制元素的所有属性。 - too
请参考以下链接,复制XAML元素。https://dev59.com/EHI-5IYBdhLWcg3wNle1 - Scott Nimrod

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接