wpf.net-6.0richtextboxrtfflowdocument

How to get all selected TableCell from a Table in WPF RichTextBox? I tried to get all TableCell by using TextSelection but this gives wrong results


I am new to Windows Presentation Framework, don't know most of it. Please answer with example code, how to get only all selected TableCell from a Table in WPF RichTextBox? I tried to get all the selected TableCell items by using TextSelection.Start and TextSelection.End but this gives wrong results. This yields selected cells but also one or more unselected unwanted cells. It seems a bug in WPF Richtextbox, or my code implementation is wrong and some other code is to be implemented. No idea what the code is to get only the selected TableCell items without any additional unselected TableCell. Kindly also inform example code to get all selected TableRow of a Table.

public List<TableCell> GetSelectedCells(System.Windows.Controls.RichTextBox rtb, TextSelection selection)
{
    List<TableCell> cells = new List<TableCell>();
    var curCaret = CaretPosition;
    Table table = rtb.Document.Blocks.OfType<Table>().Where(x => x.ContentStart.CompareTo(curCaret) == -1 && x.ContentEnd.CompareTo(curCaret) == 1).FirstOrDefault();
    if (table == null) return cells;
    if (table.RowGroups.Count == 0) return cells;

    foreach (TableRowGroup rowGroup in table.RowGroups)
    {
        foreach (TableRow row in rowGroup.Rows)
        {
            // option 1, this also yields additional unselected cells
            List<TableCell> selcells = row.Cells.Where(w => selection.Contains(w.ContentStart) && selection.Contains(w.ContentEnd)).ToList();

            // option 2, this also yields additional unselected cells.
            foreach (TableCell cell in selcells)
            {
                if (selection.Contains(cell.ElementStart) && selection.Contains(cell.ElementEnd))
                    cells.Add(cell);
            }
        }
    }
    return cells;
}

I tried 2 different code which is described as "option 1" or "option 2" or both options. i cannot get perfect and exact results but only wrong results with additional unselected unwanted TableCell. I want to get all the selected TableCell items only, but not any other TableCell.


Solution

  • FlowDocument is a component based document model.
    This means, it is generally a bad idea to search the document text based when you are interested in elements of the content model, for example a TableCell.

    The general approach is to use a TextPointer context based search as this is the only convenient way to get access to the content elements.

    The idea is to get the TextPointer that points to the start of an element. Then check if this element is the element of interest. If it's not, advance the TextPointer to the next position by using the TextPointer API.

    To support your open source project I will post a modified example of some library code. It's reduced to satisfy the minimum requirement without sacrificing the generic nature. I have also annotated it with some comments that help to understand the implementation. For convenience, I have converted the code into an extension method.

    The following algorithm is a generic implementation that allows to find any kind of ContentElement or UIElement.

    As a general note, the implementation of TryGetNextElement shows the common algorithm to step through a FlowDocument context based to inspect its elements:

    TextRangeHelper.cs

    public static class TextRangeHelper
    {
      public static IEnumerable<TElement> EnumerateElements<TElement>(this TextRange documentRange)
          where TElement : DependencyObject
      {
        if (!TryGetFirstElementStartPositionInContentRange<TElement>(documentRange, out TextPointer selectedElementStart))
        {
            yield break;
        }
    
        TextPointer contentPosition = selectedElementStart;
        while (TryGetNextElement(ref contentPosition, documentRange.End, out TElement contentElement))
        {
          yield return contentElement;
          contentPosition = contentPosition.GetNextContextPosition(LogicalDirection.Forward);
        }
    
        yield break;
      }
    
      // In case the TextRange e.g. content selection starts inside the requested element,
      // we need to adjust the text range to include the current element.
      private static bool TryGetFirstElementStartPositionInContentRange<TElement>(TextRange documentRange, out TextPointer selectedElementStart)
        where TElement : DependencyObject
      {
        selectedElementStart = documentRange.Start;
        DependencyObject? contentElement = selectedElementStart?.GetAdjacentElement(LogicalDirection.Forward);
        if (contentElement is TElement)
        {
          return true;
        }
    
        // Step backward to capture the first TableCell element in the current selection
        while (selectedElementStart is not null)
        {
          TextPointerContext textPointerContext = selectedElementStart!.GetPointerContext(LogicalDirection.Forward);
    
          // Filter pointer context of content elements (TextPointerContext.ElementStart)
          // and UIElements contained in a BlockUIContainer or InlineUIContainer (TextPointerContext.EmbeddedElement)
          if (textPointerContext is not (TextPointerContext.ElementStart or TextPointerContext.EmbeddedElement))
          {
            selectedElementStart = selectedElementStart!.GetNextContextPosition(LogicalDirection.Backward);
            continue;
          }
    
          contentElement = selectedElementStart!.GetAdjacentElement(LogicalDirection.Forward);
    
          // Selection starts inside or after the potential target element.
          // This means we must return the coerced start pointer to make the selection start ahead of the original.
          if (contentElement is TElement)
          {
            return true;
          }
    
          // Selection starts before the potential target element.
          // This means we can return the original selection start pointer.
          else if (contentElement is not (Block or Inline)
            || (contentElement is FrameworkContentElement frameworkContentElement && frameworkContentElement.Parent is FlowDocument))
          {
            selectedElementStart = documentRange.Start;
            return true;
          }
    
          selectedElementStart = selectedElementStart!.GetNextContextPosition(LogicalDirection.Backward);
        }
    
        return false;
      }
    
      private static bool TryGetNextElement<TElement>(ref TextPointer contentPosition, TextPointer contentEndPosition, out TElement nextElement)
        where TElement : DependencyObject
      {
        nextElement = default;
    
        if (IsEndOfContent(contentPosition, contentEndPosition))
        {
          return false;
        }
    
        nextElement = contentPosition?.GetAdjacentElement(LogicalDirection.Forward) as TElement;
        if (nextElement is not null)
        {
          return true;
        }
    
        // Step forward to find and collect next TableCell in current selection
        while (contentPosition is not null
          && !IsEndOfContent(contentPosition, contentEndPosition))
        {
          TextPointerContext contentPositionContext = contentPosition!.GetPointerContext(LogicalDirection.Forward);
    
          // Filter pointer context of content elements (TextPointerContext.ElementStart)
          // and UIElements contained in a BlockUIContainer or InlineUIContainer (TextPointerContext.EmbeddedElement)
          if (contentPositionContext is not (TextPointerContext.ElementStart or TextPointerContext.EmbeddedElement))
          {
            contentPosition = contentPosition!.GetNextContextPosition(LogicalDirection.Forward);
            continue;
          }
    
          nextElement = contentPosition!.GetAdjacentElement(LogicalDirection.Forward) as TElement;
          if (nextElement is not null)
          {
            return true;
          }
    
          contentPosition = contentPosition!.GetNextContextPosition(LogicalDirection.Forward);
        }
    
        return false;
      }
    
      private static bool IsEndOfContent(TextPointer contentPosition, TextPointer contentEndPosition)
      {
        if (contentPosition is null || contentEndPosition is null)
        {
          return true;
        }
    
        // If the pointer is pointing to the next insertion point (LogicalDirection.Forward),
        // we must reverse the end pointer to exclude the next trailing content.
        if (contentEndPosition.LogicalDirection == LogicalDirection.Forward)
        {
          contentEndPosition = contentEndPosition?.GetNextInsertionPosition(LogicalDirection.Backward);
        }
    
        return contentPosition?.CompareTo(contentEndPosition) >= 0;
      }
    }
    

    Usage Example

    After selecting some text in the Table within a FlowDocument, a Button.Click handler will actually execute the extraction of the selected TableCell items and their cell values:

    MainWindow.xaml.cs

    private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
    {
      IEnumerable<TableCell> cells = this.RichTextBox.Selection.EnumerateElements<TableCell>();
      IEnumerable<string> cellValues = cells.Select(cell => new TextRange(cell.ContentStart, cell.ContentEnd).Text);
    }