nam
nam

Reputation: 23868

How to programmatically replace some content of WPF RichTextBox without loosing the formatting?

Question: How can we programmatically replace some text in a WPF RichTextBox without loosing its formatting? In the following code I am clearly not doing something right. My online search gives some relevant suggestions but they seem to be using Winform where RichTextBox has rtf property - such as this one.

Following code correctly replaces text abcd with rstu inside a WPF RichTexBox but it looses the formatting of the RichTextBox as shown in the two images below:

//rtbTest is the name of the RichTextBox
TextRange textRange = new TextRange(rtbTest.Document.ContentStart, rtbTest.Document.ContentEnd);
string oldText = textRange.Text;
string newText = oldText.Replace("abcd", "rstu");
textRange.Text = newText;

Screenshot of RichTextBox BEFORE replacing abcd with rstu:

enter image description here

Screenshot of RichTextBox AFTER replacing abcd with rstu:

As we can see the formatting is lost. The list shown below is not really a formatted numbered list, it probably is just unformatted text (like 1. Item 1 etc.)

enter image description here

Upvotes: 1

Views: 711

Answers (2)

Victor
Victor

Reputation: 8985

When modifying the RichTextBox content it's very important to analyzing the TextPointer context. How it can be implemented see in the example below:

MainWindow.xaml

<Window ...
        Title="MainWindow" Height="350" Width="500">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="Auto"/>            
        </Grid.RowDefinitions>
        <RichTextBox x:Name="rtb" AllowDrop="True" VerticalScrollBarVisibility="Auto" Padding="2">
            <FlowDocument>
                <Paragraph>
                    <Run Text="Paste a content to the document..."/>                                      
                </Paragraph>
            </FlowDocument>
        </RichTextBox>
        <Button Grid.Row="1" Click="FindAndReplace">Find &amp; Replace </Button>
    </Grid>
</Window>

Part of the MainWindow.xaml.cs

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;

private void FindAndReplace(object sender, RoutedEventArgs e)
{
    var textToFind = "ABC";
    var textReplaceBy = "<A-B-C>";

    TextPointer start = rtb.Document.ContentStart;
    while (true)
    {
        var searchRange = new TextRange(start, rtb.Document.ContentEnd);
        TextRange foundRange = searchRange.FindText(textToFind);
        if (foundRange == null)
            break;

        foundRange.Text = textReplaceBy;             
        start = foundRange.End; // Continue the searching 
    }
    rtb.Focus();
}

TextRangeExt.cs

using System;
using System.Windows.Documents;

namespace WpfApp7
{
    public static class TextRangeExt
    {
        public static TextRange FindText(this TextRange searchRange, string searchText)
        {
            TextRange result = null;
            int offset = searchRange.Text.IndexOf(searchText, StringComparison.OrdinalIgnoreCase);
            if (offset >= 0)
            {
                var start = searchRange.Start.GetTextPositionAtOffset(offset);
                result = new TextRange(start, start.GetTextPositionAtOffset(searchText.Length));
            }
            return result;
        }

        private static TextPointer GetTextPositionAtOffset(this TextPointer position, int offset)
        {
            for (TextPointer current = position; current != null; current = position.GetNextContextPosition(LogicalDirection.Forward))
            {
                position = current;
                var adjacent = position.GetAdjacentElement(LogicalDirection.Forward);
                var context = position.GetPointerContext(LogicalDirection.Forward);
                switch (context)
                {
                    case TextPointerContext.Text:
                        int count = position.GetTextRunLength(LogicalDirection.Forward);
                        if (offset <= count)
                        {
                            return position.GetPositionAtOffset(offset);
                        }
                        offset -= count;
                        break;
                    case TextPointerContext.ElementStart:
                        if (adjacent is InlineUIContainer)
                        {
                            offset--;
                        }
                        else if (adjacent is ListItem lsItem)
                        {
                            var trange = new TextRange(lsItem.ElementStart, lsItem.ElementEnd);
                            var index = trange.Text.IndexOf('\t');
                            if (index >= 0)
                            {
                                offset -= index + 1;
                            }                            
                        }
                        break;
                    case TextPointerContext.ElementEnd:
                        if (adjacent is Paragraph)
                            offset -= 2;                        
                        break;
                }
            }
            return position;
        }

    }
}

Upvotes: 1

Abdullah Leghari
Abdullah Leghari

Reputation: 2470

It is losing the formatting because you are storing the RTF into a string and the string doesn't keep RTF formatting.

You can preserve it as following,

TextRange textRange = new TextRange(rtbTest.Document.ContentStart, rtbTest.Document.ContentEnd);
string rtf;
using (var memoryStream = new MemoryStream())
{
    textRange.Save(memoryStream, DataFormats.Rtf);
    rtf = ASCIIEncoding.Default.GetString(memoryStream.ToArray());
}

rtf = rtf.Replace("abcd", "rstu");


MemoryStream stream = new MemoryStream(ASCIIEncoding.Default.GetBytes(rtf));
rtbTest.SelectAll();
rtbTest.Selection.Load(stream, DataFormats.Rtf);

enter image description here

Upvotes: 2

Related Questions