1

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);
    }
}
nam
  • 21,967
  • 37
  • 158
  • 332

1 Answers1

2

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.

Jackdaw
  • 7,626
  • 5
  • 15
  • 33
  • If there are multiple lists (not necessarily nested sublists) how would you get a collection of those lists? I see [ListItemCollection](https://learn.microsoft.com/en-us/dotnet/api/system.windows.documents.listitemcollection?view=net-5.0) in official MS documentation but not the list collection. – nam Apr 03 '21 at 22:24
  • @nam: I have updated the code above to enumerate all lists in the document. The `EnumerateLists()` method generate `List` for each list in the `RitchTextBox`. – Jackdaw Apr 03 '21 at 22:40
  • Works great (thank you). Minor typos, you probably meant `lists.Count()` in `for (int i = 0; i < lists.Count; i++)`, and meant `lists.ToList()[i]` in `foreach (var litem in lists[i])`. – nam Apr 03 '21 at 23:13
  • @nam: The `Count` is the property, therefore no parentheses needed. And the `lists` is already list and so converting to list does not required. You can put break-point in the debugger and see it. You are welcome! – Jackdaw Apr 03 '21 at 23:23
  • 1
    @ Jackdaw Agreed. But, for `count` I got the error: `operator < cannot be applied to operand of type int and method group`, and for `lists`, I got the error: `cannot apply indexing with [] to an expression of type IEnumerable>`. Not sure if it has anything to do with the line `result.Add(list.EnumerateList().ToList());` in your extension method. I'm using `VS2019 - ver16.9.3`. – nam Apr 04 '21 at 00:18
  • I see now a probable cause of the error on my end. I'm using explicit type in line `IEnumerable> lists = rtbTest.EnumerateLists();` of `EnumerateList(...)` method whereas you are using `var lists = rtb.EnumerateLists();`. I'm not sure if that was a reason for the error on my end. An interesting post with 87 votes on relevant topic [Count property vs Count() method?](https://stackoverflow.com/q/7969354/1232087). – nam Apr 04 '21 at 00:30
  • 1
    @nam: You're right. As mentioned in the post you referenced, performance is only one reason _(and I wanted to print serial list number)_ to chose to use `Count` property instead of `Count()` extension method. If you want to use LINQ and an explicit type declaration then you can replace code fragment above by: `IEnumerable> lists = rtb.EnumerateLists(); foreach(var list in lists) foreach (var litem in list) { TextRange range = new TextRange(litem.ElementStart, litem.ElementEnd); /* TODO: Your code here… */ }` – Jackdaw Apr 04 '21 at 07:00