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

时间:2014-09-04 09:52:15

标签: c# wpf flowdocument xamlreader xamlwriter

我将FlowDocument与BlockUIContainer和InlineUIContainer元素一起使用,包含(或作为基类)一些自定义块 - SVG,数学公式等。 因为使用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的所有必需属性和对象,但Copy + Paste必须手动实现为默认实现复制+粘贴使用Selection.Load / Save。

复制/粘贴非常重要,因为它还用于处理RichTextBox控件中或之间的元素拖动 - 这是在没有自定义拖动代码的情况下操作对象的唯一方法。

这就是我希望使用FlowDocument序列化实现复制/粘贴的原因,但不幸的是它存在一些问题:

  1. 在当前的解决方案中,需要对整个FlowDocument对象进行序列化/反序列化。性能方面它不应该是一个问题,但我需要存储信息需要从中粘贴的选择范围(CustomRichTextBoxTag类)。
  2. 显然,对象无法从一个文档中删除并添加到另一个文档中(我最近发现的一个死胡同):' InlineCollection'元素不能插入树中,因为它已经是树的子节点。

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

    我想在所有需要移动到另一个文档的元素中使用反射来设置FrameworkContentElement._parent,但这是最后的手段,这是一个黑客和肮脏的解决方案:

  3. 理论上,我只能复制所需的对象:(可选)部分运行在选择开始时使用文本,所有段落和内联之间和(可能)部分运行在最后,将这些封装在自定义中使用XamlReader / XamlWriter进行类和序列化/反序列化。

  4. 我没有想到的另一个解决方案。
  5. 以下是自定义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自定义复制+粘贴代码的现有解决方案? 如何修复上面的代码,以便它不会抱怨不同的FlowDocument对象或解决这个限制?

    编辑:作为实验,我实施了2),以便可以将对象从一个FlowDocument移动到另一个。上面的代码已更新 - 所有对&#34;字段的引用&#34;变量

2 个答案:

答案 0 :(得分:2)

似乎赏金期即将到期,我突破了如何实现上述问题,所以我将在此分享。

首先,TextRange.Save有一个&#34; preserveTextElements&#34;可用于序列化InlineUIContainer和BlockUIContainer元素的参数。此外,这两个控件都没有密封,因此可以用作自定义TextElement实现的基类。

考虑到上述因素:

  1. 我创建了一个继承自InlineUIContainer的InlineMedia元素,该元素序列化了它的孩子&#34;手动&#34;进入&#34; ChildSource&#34;依赖属性使用XamlReader和XamlWriter并隐藏原始&#34; Child&#34;来自默认序列化程序

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

  3. 您可以注意到,由于Xaml在剪贴板中交换原始Xaml后很好地反序列化,因此不需要特殊的粘贴处理,这意味着拖动可以作为所有CustomRichtextBox控件的复制,而粘贴甚至可以用于普通的RichTextBox。

    唯一的限制是,对于所有InlineMedia控件,在序列化整个文档之前,需要通过序列化它的Child来更新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)

  

1.在当前的解决方案中,需要对整个FlowDocument对象进行序列化/反序列化。在性能方面它应该不是问题   但我需要存储选择范围需要的信息   从它粘贴(CustomRichTextBoxTag类)。

这就像是根据您确定的行为使用附加财产的机会。我理解附加属性是一种向元素添加其他行为的方法。注册附加属性时,可以为该属性值更改时添加事件处理程序。 为了利用这一点,我将这个附加属性连接到DataTrigger,以更新复制/粘贴操作的选择范围值。

  

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

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