Read-only Run elements in a WPF RichTextBox?

Rob picture Rob · Jul 5, 2009 · Viewed 7.6k times · Source

I may be completely imagining this, but I could have sworn there was a way to make individual Run (or Parapgraph) elements in a RichTextBox read-only. I also could have sworn I tried a method for doing this out myself a few weeks ago and was satisfied with the results - I vaguely remember it looked something like this:

<RichTextBox x:Name="richTextBox"
             AcceptsTab="True"
             AcceptsReturn="True"
             FontFamily="Courier New"
             FontSize="14">
    <FlowDocument>
        <Paragraph>
            <Run IsReadOnly="True">I wish this was read-only!</Run>
        </Paragraph>
    </FlowDocument>
</RichTextBox>

Now, a few weeks later, I go to try to make Run elements read-only in a RichTextBox only to find it doesn't seem to be possible.

This post on the MSDN forums seems to confirm that.

Did I completely imagine this? Or is there a way to do what I want to do?

Answer

Rob picture Rob · Jul 5, 2009

Alright, I've come up with a solution that works for my case - but may not work for everyone who wants something like this. It's messy, but it does the job.

I'm not going to accept my own answer for a few days, just in case someone else has a better way of accomplishing this.

Here we go, first, the XAML:

<Window x:Class="WpfApplication1.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Window1"
        Height="500"
        Width="600">
    <DockPanel LastChildFill="True">
        <RichTextBox x:Name="rtb"
                     FontFamily="Courier New"
                     FontSize="14"
                     PreviewKeyDown="rtb_PreviewKeyDown">
            <FlowDocument>
                <Paragraph>
                    <InlineUIContainer Unloaded="InlineUIContainer_Unloaded">
                        <TextBlock FontFamily="Courier New" FontSize="14">This line of text is not editable.</TextBlock>
                    </InlineUIContainer>
                    <Run Foreground="Blue">But this is editable.</Run>
                </Paragraph>
            </FlowDocument>
        </RichTextBox>
    </DockPanel>
</Window>

And the code behind:

using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;

namespace WpfApplication1
{
    public partial class Window1 : Window
    {
        public Window1()
        {
            InitializeComponent();
        }

        private void InlineUIContainer_Unloaded(object sender, RoutedEventArgs e)
        {
            (sender as InlineUIContainer).Unloaded -= new RoutedEventHandler(InlineUIContainer_Unloaded);

            TextBlock tb = new TextBlock();
            tb.FontFamily = new FontFamily("Courier New");
            tb.FontSize = 14;
            tb.Text = "This line of text is not editable.";

            TextPointer tp = rtb.CaretPosition.GetInsertionPosition(LogicalDirection.Forward);
            InlineUIContainer iuic = new InlineUIContainer(tb, tp);
            iuic.Unloaded += new RoutedEventHandler(InlineUIContainer_Unloaded);
        }

        private void rtb_PreviewKeyDown(object sender, KeyEventArgs e)
        {
            if (e.Key == Key.Enter)
            {
                var newPointer = rtb.Selection.Start.InsertLineBreak();
                rtb.Selection.Select(newPointer, newPointer);

                e.Handled = true;
            }
        }
    }
}

My solution relies on the fact that when an InlineUIContainer is removed from the UI, it's Unloaded() method is called. At that point, I simply reinsert the deleted InlineUIContainer at the current caret position.

As with any hack, there are a bunch of disadvantages. The disadvantages I'm finding are the following:

  • The text I want to be read-only needs to be wrapped in a InlineUIContainer. That is a little limiting for this solution.
  • I have to capture the 'Enter' key and insert line breaks manually, otherwise, InlineUIContainer.Unloaded() keeps firing everytime the Enter key is pressed. Not fun, but it works for my case.

It's not a great solution, but I think it will work for me. Like I said, I'm not going to mark this as an answer to my own question yet - hopefully someone else will have a better way of doing this.