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:
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:
Now, when I debugged this, I noticed this:
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.
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:
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!
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With