Search code examples
c#wpfrichtextboxrtf

How to get all numbered lists from WPF RichTextBox


enter image description here

Question: From the above WPF RichTextBox, how can we programmatically get the list shown above?

Details: Using following Extension Methods, I can read all paragraphs from the above RichTextBox. The btnTest_Click(...) event (code shown below) returns all the paragraphs as follows (as you may have guessed from the above image), each line is a paragraph. But below code does not tell me which object (element) is a list:

Output of btnTest_Click(...) event:

This is a test.
Following is a list number:
1.   Item 1
2.   Item 2
3.   Item 3
End of test.

MainWindow.xaml:

.....
   <RichTextBox x:Name="rtbTest" AcceptsTab="True" FontFamily="Calibri"/>
.....

File1.cs:

using System.Windows.Documents;
using System.Linq;

namespace MyProjectName
{
    public static class FlowDocumentExtensions
    {
        public static IEnumerable<Paragraph> Paragraphs(this FlowDocument doc)
        {
            return doc.Descendants().OfType<Paragraph>();
        }
    }
}

File2.cs:

using System.Windows;
using System.Linq;

namespace MyProjectName
{
    public static class DependencyObjectExtensions
    {
        public static IEnumerable<DependencyObject> Descendants(this DependencyObject root)
        {
            if (root == null)
                yield break;
            yield return root;
            foreach (var child in LogicalTreeHelper.GetChildren(root).OfType<DependencyObject>())
                foreach (var descendent in child.Descendants())
                    yield return descendent;
        }
    }
}

Code to read all paragraphs:

private void btnTest_Click(object sender, RoutedEventArgs e)
{
    foreach (Paragraph paragraph in rtbTest.Document.Paragraphs())
    {
        System.System.Diagnostics.Debug.WriteLine(new TextRange(paragraph.ContentStart, paragraph.ContentEnd).Text);
    }
}

Solution

  • To enumerate all list items in a RichTextBox select all System.Windows.Documents.List from the RichTextBox.Document.Blocks collection:

    The MainWindow.xaml:

    <Window ...
            Title="MainWindow" Height="350" Width="500">
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="*"/>
                <RowDefinition Height="Auto"/>            
            </Grid.RowDefinitions>
            <RichTextBox Grid.Row="0" x:Name="rtb" AllowDrop="True" VerticalScrollBarVisibility="Auto" Padding="2">
                <FlowDocument>
                    <Paragraph>
                        <Run Text="First list:"/>
                    </Paragraph>
                    <List MarkerStyle="Decimal">
                        <ListItem>
                            <Paragraph>C++</Paragraph>
                        </ListItem>
                        <ListItem>
                            <Paragraph>C#</Paragraph>
                            <List MarkerStyle="LowerLatin">
                                <ListItem>
                                    <Paragraph>v 7.0</Paragraph>
                                </ListItem>
                                <ListItem>
                                    <Paragraph>v 8.0</Paragraph>
                                </ListItem>
                            </List>
                        </ListItem>
                    </List>
                    <Paragraph>
                        <Run Text="Second list:"/>
                    </Paragraph>
                    <List MarkerStyle="Decimal">
                        <ListItem>
                            <Paragraph>Perl</Paragraph>
                        </ListItem>
                        <ListItem>
                            <Paragraph>Logo</Paragraph>
                        </ListItem>
                    </List>
                </FlowDocument>
            </RichTextBox>
            <Button Grid.Row="1" Click="EnumerateList">Enumerate List</Button>
        </Grid>
    </Window>
    

    The MainWindow.xaml.cs:

    using System.Collections.Generic;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Documents;
    
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }
    
        private void EnumerateList(object sender, RoutedEventArgs e)
        {
             var lists = rtb.EnumerateLists();
             for (int i=0; i < lists.Count; i++)
             {
                 System.Diagnostics.Debug.WriteLine("\nList " + (i+1));
                 foreach (var litem in lists[i])
                 {
                    TextRange range = new TextRange(litem.ElementStart, litem.ElementEnd);
                    System.Diagnostics.Debug.WriteLine(range.Text);
                 }
             }
        }
    }
    
    public static class RichTextBoxExt
    {
        public static List<List<ListItem>> EnumerateLists(this RichTextBox rtb)
        {
            var result = new List<List<ListItem>>();
            foreach (var block in rtb.Document.Blocks)
            {
                if (block is List list && list.ListItems.Count > 0)
                {
                    //var marker = list.MarkerStyle;
                    result.Add(list.EnumerateList().ToList());
                }
            }
            return result;
        } 
    
        private static IEnumerable<ListItem> EnumerateList(this System.Windows.Documents.List list)
        {
            foreach (var litem in list.ListItems) yield return litem;            
        }    
    }
    

    The code above produces the following output:

    List 1
    1.  C++
    2.  C#
    a.  v 7.0
    b.  v 8.0
    
    List 2
    1.  Perl
    2.  Logo
    

    NOTE:

    If some list item has subitems (SiblingListItems), in example above the range.Text will contain "2.\tC#\r\na.\tv 7.0\r\nb.\tv 8.0". That means all sibling list items included to the text of parent item but separated by \r\n.