Underline not detected after reloading RTF
Asked Answered
B

4

6

I'm currently trying to get a RichTextBox with basic formatting working for my new beta notes software, Lilly Notes. Brian Lagunas' article on the subject put me in the right direction, however I'm having a bit of an issue. If you click on underlined text, the Underline button becomes pressed, so the state is being recognised. However if I serialize it to RTF and then deserialize it back into the RichTextBox, then it doesn't get detected. Since the code in Lilly Notes is not trivial to demonstrate here, I have created a SSCCE to demonstrate the problem.

First, MainWindow.xaml:

<Window x:Class="WpfRichTextBoxUnderline.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow"
        Height="350"
        Width="525">
    <DockPanel LastChildFill="True">
        <Button Name="SaveAndReloadButton"
                Content="Save and Reload"
                DockPanel.Dock="Bottom"
                Click="SaveAndReloadButton_Click" />
        <ToggleButton Name="UnderlineButton"
                      DockPanel.Dock="Top"
                      Width="20"
                      Command="{x:Static EditingCommands.ToggleUnderline}"
                      CommandTarget="{Binding ElementName=RichText}">
            <ToggleButton.Content>
                <TextBlock Text="U"
                           TextDecorations="Underline" />
            </ToggleButton.Content>
        </ToggleButton>
        <RichTextBox Name="RichText"
                     SelectionChanged="RichTextBox_SelectionChanged" />
    </DockPanel>
</Window>

This is what it looks like:

enter image description here

In the codebehind, I have code to detect the state of the formatting when the selection changes, and update the state of the Underline button accordingly. This is no different from Brian Lagunas' method.

private void RichTextBox_SelectionChanged(object sender, RoutedEventArgs e)
{
    if (this.RichText.Selection != null)
    {
        object currentValue = this.RichText.Selection.GetPropertyValue(Inline.TextDecorationsProperty);
        this.UnderlineButton.IsChecked = (currentValue == DependencyProperty.UnsetValue) ? false : currentValue != null && currentValue.Equals(TextDecorations.Underline);
    }
}

Then I have a method (and another helper method) which saves the RTF to a string and then applies it to the RichTextBox. Again I'm doing this just to keep it simple - in Lilly Notes, I'm saving that string to a database, and then loading it back when the the application is run again.

public Stream GenerateStreamFromString(string s)
{
    MemoryStream stream = new MemoryStream();
    StreamWriter writer = new StreamWriter(stream);
    writer.Write(s);
    writer.Flush();
    stream.Position = 0;
    return stream;
}

private async void SaveAndReloadButton_Click(object sender, RoutedEventArgs e)
{
    string data = null;
    var range = new TextRange(this.RichText.Document.ContentStart, this.RichText.Document.ContentEnd);
    using (var memoryStream = new MemoryStream())
    {
        range.Save(memoryStream, DataFormats.Rtf);
        memoryStream.Position = 0;

        using (StreamReader reader = new StreamReader(memoryStream))
        {
            data = await reader.ReadToEndAsync();
        }
    }

    // load

    var stream = GenerateStreamFromString(data);
    range = new TextRange(this.RichText.Document.ContentStart, this.RichText.Document.ContentEnd);
    range.Load(stream, DataFormats.Rtf);
}

After I click the Save and Reload button, and the RTF gets serialized to a string and deserialized back into the RichTextBox, underline detection does not work any more, and when I click the underlined text, the button remains as if the underline was not working:

enter image description here

Now, when I debugged this, I noticed this:

enter image description here

Initially, when you click on a piece of underlined text, you get a TextDecorationCollection with a Count of 1. But after saving and reloading, you get a Count of zero, which is why the detection is not working.

Note that this problem applies only to underline/strikethrough, which belong to the TextDecorationCollection in WPF. Bold and Italic do not exhibit this issue.

Is this happening because I am doing something wrong, or is this a bug with the RichTextBox?

You can find the SSCCE code here at my BitBucket repo.

Bathtub answered 9/8, 2014 at 10:23 Comment(0)
K
5

Inline.TextDecorations is a Collection, so probably comparing it directly is not a good idea.

Perhaps this will work better:

TextDecorationCollection currentValue = this.RichText.Selection.GetPropertyValue(Inline.TextDecorationsProperty) as TextDecorationCollection;
this.UnderlineButton.IsChecked = (currentValue == DependencyProperty.UnsetValue) ? false : currentValue != null && currentValue.Contains(TextDecorations.Underline);

EDIT

After going through the code provided, I discovered the probable cause:

before

This image was done before saving and reloading as RTF.

In the image above, notice that the inlines of the paragraph are Run and the parent of the caret is also a Run and both have TextDecorations in place.

Now lets save and reload!

after

In the image above, notice that the the inlines of paragraph are now Span and the parent of the caret is Run. but the weird thing is that the Span has the TextDecoration in place, but the parent Run does not have any TextDecoration in it.

Solution

Here is a possible solution, or better said a workaround:

    private void RichTextBox_SelectionChanged(object sender, RoutedEventArgs e)
    {
        var caret = RichText.CaretPosition;
        Paragraph paragraph = RichText.Document.Blocks.FirstOrDefault(x => x.ContentStart.CompareTo(caret) == -1 && x.ContentEnd.CompareTo(caret) == 1) as Paragraph;

        if (paragraph != null)
        {
            Inline inline = paragraph.Inlines.FirstOrDefault(x => x.ContentStart.CompareTo(caret) == -1 && x.ContentEnd.CompareTo(caret) == 1) as Inline;
            if (inline != null)
            {
                TextDecorationCollection decorations = inline.TextDecorations;
                this.UnderlineButton.IsChecked = (decorations == DependencyProperty.UnsetValue) ? false : decorations != null && decorations.Contains(TextDecorations.Underline[0]);
            }
        }
    }

In the solution above, I tried to get the underlying Run or Span, by using the current caret position. The rest remains similar.

Kaitlynkaitlynn answered 9/8, 2014 at 11:15 Comment(8)
Valid point - in fact in my proper code I'm traversing the collection. However since the collection is empty, this is never going to work (see last part of question).Bathtub
I noticed it as well. perhaps saving and restoring the document as xaml may help in this case. try XamlReader/Writer with Document property of RTB and see if issue persist. I'll try to reproduce the same with your code.Kaitlynkaitlynn
I don't think I can go down that route. As far as I know you can't save images when serializing as XAML, hence why I'm using RTF.Bathtub
updated my answer hoping this may highlight the problem and solve the issue as well.Kaitlynkaitlynn
Hmmm, so it is indeed a bug with serialising/deserialising the RichTextBox data. Richard Vella in the other answer noticed a similar issue when copying/pasting text, although that is different and can be solved by comparing the TextDecorationCollections with SequenceEquals. Your solution works, except when you take the cursor at the very end. All you need to do is add an else matching the last if that sets isChecked to false.Bathtub
Nice post pushpraj. This does indeed fix visually the issue reported however I have noticed some weird behavior when you press save and reload. For example typing Hello World (with the word World being underlined), pressing Save and reload, and then moving the caret over the underlined text you will see that the button does indeed get checked as expected. However if you try and press the button to remove the underline it wont work until you press it twice indicating that the internal state of the toggle button is corrupted due to this internal bug in WPF.Phage
@Kaitlynkaitlynn what is that tool in the screenshot?Bathtub
@RichardVella, I suspect the behavior is due two different states of TextDecorations between the mentioned Span and Run, and the ToggleUnderline command toggle(not check or uncheck) on the WPF layer i.e. Run. this behavior still persist without this fix but other way, try toggling a underlined word. as a fix we can probably sync these two layers when the caret hover them. Secondly the screens are from a Watch window of VS, not a separate tool. I was trying to find out what is the reason why the TextDecorations disappears and still the word is underlined, and I came across this weird behavior.Kaitlynkaitlynn
P
1

There seems to be a bug in GetCharacterValueFromPosition being used by RichTextBox.Selection.GetPropertyValue. Have a look at this post: link

When that method is getting the TextDecorationsProperty, it is walking up the logical tree to find an element which has a non-null value for the property. When it finds such a value, it returns it. The problem is that it should be checking for both a null value and an empty TextDecorationCollection.

If you use the suggested implementation it will fix the checked state issue on your toggle button. However there is still the issue that it doesn't set/unset underlines properly.

Phage answered 9/8, 2014 at 13:52 Comment(0)
P
0

Try changing your selection event handler to the following:

private void RichTextBox_SelectionChanged(object sender, RoutedEventArgs e)
{
    if (this.RichText.Selection != null)
    {
        var currentValue = new TextRange(this.RichText.Selection.Start, this.RichText.Selection.End);
        if (currentValue.GetPropertyValue(Inline.TextDecorationsProperty) == TextDecorations.Underline)
        {
            this.UnderlineButton.IsChecked = true;
        }
        else
        {
            this.UnderlineButton.IsChecked = false;
        }
}
Phage answered 9/8, 2014 at 11:34 Comment(1)
Nope, that doesn't solve it. When I go on the underlined text, the Underline button needs to become checked.Bathtub
C
0

I know this is an old question, and already has an accepted answer, but not exactly the right answer, and having the same problem, I write for future users of the solution that I found. Is in VB...

    Dim TextRange = New TextRange(richTxtEditor.Selection.Start, richTxtEditor.Selection.End)
Dim textDecor As TextDecorationCollection
Dim DecorationFound as Boolean = false
If TextRange.IsEmpty Then
    textDecor = txtRange.GetPropertyValue(Inline.TextDecorationsProperty)

    If textDecor.Equals(TextDecorations.Underline) Then
        MsgBox("Is Underline, and it only works on a new document, not a document loaded.")
        DecorationFound = true
    End If

    If textDecor.Equals(TextDecorations.Strikethrough) Then
        MsgBox("Is Strikethrough, and it only works on a new document, not a document loaded.")
        DecorationFound = true
    End If

    ' ### START # From here starts the solution to the problem !!! ###

    If NOT DecorationFound Then
        For i = 0 To textDecor.Count - 1
            If textDecor.Item(i).Location = TextDecorationLocation.Underline Then
                MsgBox("Is Underline, and it works on a loaded document.")
            ElseIf textDecor.Item(i).Location = TextDecorationLocation.Strikethrough Then
                MsgBox("Is Strikethrough, and it works on a loaded document.") 
            End If
        Next
    End If

    ' ### END SOLUTION ###

End If
Calabar answered 25/4, 2016 at 12:11 Comment(3)
Can you specify how the accepted answer is not adequate and how yours is better?Bathtub
@Bathtub Is not necessarily better, and is not necessarily wrong the Richard's answer, but trying his solution, in my case it did not work ... I wanted to contribute with a solution that in my case it worked :)Calabar
Yes, and thank you for your answer - just wanted to clarify for the people who have to choose between two possible solutions. :)Bathtub

© 2022 - 2024 — McMap. All rights reserved.