2014-09-04 2 views
4

Я использую FlowDocument с элементами BlockUIContainer и InlineUIContainer, содержащими (или как базовые классы) некоторые пользовательские блоки - SVG, математические формулы и т. Д. Из-за этого с помощью Selection.Load (поток, DataFormats.XamlPackage) не будет работать, как сериализации будет падать содержимое * UIContainers за исключением, если свойство Child является изображение доступно в Microsoft эталонного источника:Вставьте содержимое из одного FlowDocument в другое при использовании XamlReader и XamlWriter

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. Другое решение, о котором я не думал.

Вот реализация пользовательского RichTextBox управления с частично работает пользовательский код Copy/Paste:

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

И вопрос - есть ли существующее решение для пользовательского Copy + кода Paste для FlowDocument с помощью XamlReader и XamlWriter ? Как сделать исправить код выше, чтобы он не стал жаловаться на разные объекты FlowDocument или работать вокруг этого ограничения?

EDIT: В качестве эксперимента я реализовал 2), чтобы объекты могли перемещаться из одного FlowDocument в другой. Обновленный код обновлен - все ссылки на переменную «field».

+0

, если я понимаю, что вы хотите скопировать содержимое документа потока другого, и вы попытались сохранить/загрузить xaml, сериализовать/десериализовать, скопировать/вставить d перетащить. вы также пробовали/хотя и воссоздавали документ? – pushpraj

+0

@pushpraj: если путем воссоздания вы подразумеваете вставку части FlowDocument, десериализованной после вставки в другой FlowDocument, путем копирования структуры и всех свойств с помощью посетителя и метода клонирования, чем нет, я не рассматривал это как не знаю о простой и надежный метод клонирования структуры объектов FlowDocument (таблицы, пользовательские объекты, абзацы и пробеги со всеми их свойствами) без сериализации/десериализации всего. – too

+0

Гость - хороший выбор, если вы можете предсказать ожидаемые элементы. отдых - это всего лишь блоки и строки. – pushpraj

ответ

1

Кажется, период щедрости истекает, и я сделал прорыв, как реализовать вышеупомянутую проблему, поэтому я поделюсь ею здесь.

Прежде всего TextRange.Save имеет аргумент "preserveTextElements", который может использоваться для сериализации элементов InlineUIContainer и BlockUIContainer. Кроме того, оба этих элемента управления не запечатаны, поэтому их можно использовать в качестве базовых классов для пользовательской реализации TextElement.

С учетом вышеизложенным:

  1. Я создал элемент InlineMedia унаследованный от InlineUIContainer который сериализующее это ребенок «вручную» в свойство зависимостей в «ChildSource» с использованием XamlReader и XamlWriter и скрывает оригинальный «ребенок» из стандартного сериализатора

  2. Я изменил приведенную выше реализацию CustomRichTextBox, чтобы скопировать выделение с использованием диапазона. Сохранение (ms, DataFormats.Xaml, true).

Как вы можете заметить, никакой специальной обработки пасты не требуется, как Xaml красиво десериализации после замены оригинального Xaml в буфер обмена, а это означает, перетаскивая работы как копии всех элементов управления CustomRichtextBox и Paste работает даже в нормальном 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.Apparently не могут быть удалены из одного документа и добавляется к другому (тупиковый я обнаружил недавно): «InlineCollection» элемент не может быть вставлен в дерево, потому что это уже ребенок из дерева ,

Вы можете обойти это, создавая программные элементы, а также программно удаляя свои элементы. В конце концов, вы в основном имеете дело с ItemsControl или ContentControl. В этом случае вы работаете с элементом ItemsControl (т. Е. Документом). В результате просто добавьте и удалите дочерние элементы из вашего ItemsControl (документа) программно.

+0

+1, поскольку ваше первое предложение - это хорошая идея, как добавить новую информацию (если необходимо в решении) в FlowDocument, спасибо за это, хотя это не решает проблему оригинальная проблема. Второе предложение создает новую проблему: программные элементы построения не будут копировать все свойства исходных, скопированных элементов. – too

+0

Ссылка на эту ссылку для копирования элементов xaml. http://stackoverflow.com/questions/1968625/is-there-an-easy-built-in-way-to-get-an-exact-copy-clone-of-a-xaml-element –

Смежные вопросы