Click here to Skip to main content
15,867,704 members
Articles / Desktop Programming / WPF

A Bindable WPF RichTextBox

Rate me:
Please Sign up or sign in to vote.
4.99/5 (48 votes)
13 Aug 2010CPOL14 min read 259.8K   12.6K   88   56
An article about a Bindable WPF RichTextBox
Screenshot

Introduction

WPF’s RichTextBox (RTB) is very good, but it suffers from several shortcomings:

  • It doesn't data-bind well, which makes it harder to use with the MVVM pattern; and
  • It outputs its content as a WPF FlowDocument, rather than as an XAML markup string.
  • It doesn't have a built-in formatting toolbar, which means additional work to set up the control in a host application.

As it turns out, the first two characteristics were not oversights, and they probably make a lot of sense. Even so, they are inconveniences, and it would be nice to have a version of the control that eliminates all of these problems. The control provided in this article does exactly that. It is included, along with a demo app, as both Visual Studio 2008 and Visual Studio 2010 RC solutions. Both solutions are included in the zip file at the top of this article.

Updates from Version 1.1

The current version of the control is Version 1.2; it incorporates the following changes from Version 1.1:

  • The List Numbering and List Bulleting buttons have been made toggle buttons and have been grouped together.
  • The text alignment buttons should be treated as a single-select button group--when one button is selected, the previously-selected button should be deselected. Version 1.1 did not implement this visual behavior; Version 1.2 does.
  • Version 1.2 adds two new text-deiting buttons, 'Format as code block', and 'Format as inline code'. These two buttons can be hidden by setting the CodeControlsVisibility visibility property to Visibility.Collapsed.
  • The source code is provided in WPF 4.0 format; I have dropped the WPF 3.5 version. If there is demand for a WPF 3.5 version, I'll consider backporting.

Why the WPF RichTextBox Behaves as It Does

The WPF RTB control outputs its content in its Document property. Unfortunately, this property is not a dependency property, which means that WPF won't data-bind to the property. As I noted above, this makes the control more difficult to use with the MVVM pattern, which has become the standard design pattern for WPF applications.

I have seen several explanations on the Web as to why the Document property isn't bindable. Now, I haven't seen anything from Microsoft’s WPF team, so the following is a bit speculative, but here is why I think the property isn't bindable: Like the WinForms RichTextBox, the WPF RichTextBox isn't really designed to be bound to a database. Instead, I suspect its designers intended it to be used more like a word processor, whose documents are loaded and saved to separate files. In that context, the lack of data binding is understandable.

Another reason to omit data binding from the control’s design has to do with processing load. One has to assume a rich text document can grow quite large. Any data bindings on the text should be updated whenever the text changes. That means whenever a character is typed. As a result, a data-bound RTB would be constantly updating the bindings, moving a large amount of formatted text as it does so. If the control is bound to a database, typing a character in the RTB could trigger a round trip to a database. Another understandable reason for making the RTB’s Document property non-bindable.

The Design of the FS RichTextBox

The FS RTB control is designed to make it easy to use an RTB in a data-bound view, while minimizing the processing load that comes with processing data-bound rich text. The control adds both a formatting toolbar and a Document dependency property to the WPF RTB. Since the Document property is a dependency property, the FS RTB can be data-bound to a view model, as is done in the demo app.

How does the control minimize the processing load associated with data-binding a rich text document? It does it by handling updates differently, depending on the direction of an update:

  • Updates coming from the view model update the RTB automatically. So, when an app loads a new document for display in the UI, it need only place that document in a view model property. The document will immediately appear in the RTB.
  • Updates coming from the RTB must be triggered by the host app. When the user enters text into the RTB, the controls Documents property is not updated until the host app calls the control’s UpdateDocumentBindings() method.

The host app determines when the Document property gets updates. It triggers the update by calling the UpdateDocumentBindings() method on the FS RTB. When that happens is entirely up to the host app. For example, it can use a LostFocus event handler to update the bindings whenever the FS RTB control loses focus. Or, it might trigger the update before it takes an action that would result in a loss of text in the control. For example, consider an app that loads a daily log entry into an RTB when a date is clicked on a calendar control. The app's date selection can call the UpdateDocumentBindings() method before it loads the new date’s text into the FS RTB.

Note that the FS RTB’s Document property is of type FlowDocument. At first glance, this appears to be a bad choice, since FlowDocuments are more difficult to work with than strings. Why not make the Document property of type String, and extract the XAML document markup from the FlowDocument as a string inside the control? It would certainly be easy enough to do. Here's why: Some developers may prefer to use binary serialization to persist the RTB text to a database, particularly for longer documents. In that case, the view model property to which the control is bound would probably be a binary type, rather than a string type.

But that doesn't mean that we are stuck with working with a FlowDocument in the host app. In the demo app, the FS RTB’s Document property is bound to a view mode string property called DocumentXaml. The demo uses a simple value converter to perform the conversion in both directions:

XML
<fsc:FsRichTextBox ... Document="{Binding Path=DocumentXaml, Converter={StaticResource 
    flowDocumentConverter}, Mode=TwoWay}" ... />

The full implementation appears in MainWindow.xaml. Here is the code for the value converter:

C#
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Markup;

namespace FsRichTextBoxDemo
{
    [ValueConversion(typeof(string), typeof(FlowDocument))]
    public class FlowDocumentToXamlConverter : IValueConverter
    {
        #region IValueConverter Members

        /// <summary>
        /// Converts from XAML markup to a WPF FlowDocument.
        /// </summary>
        public object Convert(object value, System.Type targetType, 
		object parameter, System.Globalization.CultureInfo culture)
        {
            /* See http://stackoverflow.com/questions/897505/
		getting-a-flowdocument-from-a-xaml-template-file */

            var flowDocument = new FlowDocument();
            if (value != null)
            {
                var xamlText = (string) value;
                flowDocument = (FlowDocument)XamlReader.Parse(xamlText); 
            }

            // Set return value
            return flowDocument; 
        }

        /// <summary>
        /// Converts from a WPF FlowDocument to a XAML markup string.
        /// </summary>
        public object ConvertBack(object value, System.Type targetType, 
		object parameter, System.Globalization.CultureInfo culture)
        {
            /* This converter does not insert returns or indentation into the XAML. 
             * If you need to indent the XAML in a text box, 
             * see http://www.knowdotnet.com/articles/indentxml.html */

            // Exit if FlowDocument is null
            if (value == null) return string.Empty;

            // Get flow document from value passed in
            var flowDocument = (FlowDocument)value;

            // Convert to XAML and return
            return XamlWriter.Save(flowDocument);
        }

        #endregion
    }
}

Value conversion provides a more flexible solution that allows a developer to bind the FS RTB to many different property types in a view model.

Demo Walkthrough

The demo app has a single window with four controls; an FS RTB, two buttons, and a gray text box. The FS RTB is discussed above. The two buttons simulate a host app taking a couple of different actions, and the text box shows the XAML markup generated by those actions.

The RTB and the text box are both bound to the DocumentXaml property in MainWindowViewModel.cs. Here is the RTB declaration:

XML
<fsc:FsRichTextBox x:Name="EditBox" Document="{Binding Path=DocumentXaml, 
    Converter={StaticResource flowDocumentConverter}, Mode=TwoWay}" Grid.Row="0" 
    Margin="10,10,10,5" />

And here is the text box declaration:

XML
<TextBox Text="{Binding DocumentXaml}" Margin="10,5,10,10" Grid.Row="2" 
	Background="Gainsboro" 
    	TextWrapping="Wrap">

As we note above, the RTB uses a value converter, FlowDocumentToXamlConverter, to perform the conversion between the Document property on the FS RTB control and the DocumentXaml property on the view model. Since the text box is also bound to the DocumentXaml property, the text box will update in response to any property updates.

When the demo starts, the RTB and text box will be empty. As a first step, type some text into the RTB. Note that the text box remains empty. That’s because the text in the RTB hasn't yet been pushed out to the FS RTB’s Document property. Remember, the property gets updated only when the host app invokes the UpdateDocumentBindings() method.

Now click the ForceUpdate button. This button invokes the UpdateDocumentBindings() method, the same way an app would before taking an action that would result in a loss of text in the RTB. An XAML representation of the text in the RTB immediately appears in the text box. What happened is that the UpdateDocumentBindings() method pushed the RTB’s text out to the FS RTB’s Document property, which is data-bound to the view model’s DocumentXaml property. When the DocumentXaml property got updated, the change flowed back to the text box in the UI, since it is bound to that property, as well.

Finally, click the Load Document button. This button simulates the host app loading a rich text document from a data store. In the demo app, the button is bound to an ICommand in the view model that simply sets the view model’s DocumentXaml property with some hard-coded XAML. However, the effect is the same as if the command had gotten the XAML from a data store and then set the property.

When you click the Load Document button, the hard-coded text immediately appears in both the RTB and the text box, since both are bound to the view model’s DocumentXaml property. The point is that changes to a view model property bound to the FS RTB’s Document property are automatically pushed through to the RTB--no host app triggering is required. In short, changes from the UI to the view model need to be triggered by the host app, but changes from the view model to the UI are automatic.

How the Control Works

The FsRichTextBox control itself is pretty straightforward. It is a user control with two constituent controls; a WPF RichTextBox control and a formatting toolbar. The formatting buttons are wired to commands from the WPF command library.

The toolbar itself merits a comment. I decided against using a WPF toolbar, because it carries a lot of visual and logical overhead to support features like dragability and overflow buttons. These features have no meaning in the context of this particular toolbar, so I used a StackPanel to emulate a toolbar. The primary disadvantages of this approach are that buttons lose their ‘toolbar look’ (they appear as standard silver buttons in a StackPanel), and the ‘toolbar’ loses the <separator>tag that the WPF toolbar uses to create separators.

The control restores the toolbar look to the formatting buttons by creating a simple button control template that styles the formatting buttons to look like toolbar buttons. The control template is located in the <usercontrol.resources>section of the user control XAML:

XML
<ControlTemplate x:Key="FlatButtonControlTemplate" TargetType="{x:Type Button}">
    <Border x:Name="OuterBorder" BorderBrush="Transparent" 
	BorderThickness="1" CornerRadius="2">
        <Border x:Name="InnerBorder" Background="Transparent" 
	BorderBrush="Transparent" BorderThickness="1" CornerRadius="2">
            <ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" 
		RecognizesAccessKey="True" Margin="{TemplateBinding Padding}" />
        </Border>
    </Border>
    <ControlTemplate.Triggers>
        <Trigger Property="IsMouseOver" Value="True">
            <Setter TargetName="OuterBorder" Property="BorderBrush" Value="#FF7CA0CC" />
            <Setter TargetName="InnerBorder" Property="BorderBrush" Value="#FFE4EFFD" />
            <Setter TargetName="InnerBorder" Property="Background" Value="#FFDAE7F5" />
        </Trigger>
        <Trigger Property="IsPressed" Value="True">
            <Setter TargetName="OuterBorder" Property="BorderBrush" Value="#FF2E4E76" />
            <Setter TargetName="InnerBorder" Property="BorderBrush" Value="#FF116EE4" />
            <Setter TargetName="InnerBorder" Property="Background" Value="#FF3272B8" />
        </Trigger>
    </ControlTemplate.Triggers>
</ControlTemplate>

The control restores the separator feature with a simple image that reproduces the appearance of a separator. The result is a lightweight toolbar that does only what it needs to do.

As is discussed above, the host app forces an update to the FS RTB’s Document property by calling the control’s UpdateDocumentBindings() method. That method reads as follows:

C#
/// <summary>
/// Forces an update of the Document property.
/// </summary>
public void UpdateDocumentBindings()
{
    // Exit if text hasn't changed
    if (!m_TextHasChanged) return;

    // Set 'Internal Update Pending' flag
    m_InternalUpdatePending = 2;

    // Set Document property
    SetValue(DocumentProperty, this.TextBox.Document); 
}

The method first checks to see if the text in the RTB has actually changed. If all the user has done is view the text, we don't need to update the property, and we can avoid a round-trip to the database. Since the control performs this check, the host app can call the method whenever an action would result in the loss of edited text, without worrying whether the user has actually edited the text or not. Next, the method sets an InternalUpdatePending flag, which is discussed below. Finally, the method copies the contents of the WPF RTB to the FS RTB control’s Document property. From there, WPF data-binding takes over.

The heart of the FS RTB control is the Document dependency property added to FsRichTextBox.xaml.cs:

C#
// Document property
public static readonly DependencyProperty DocumentProperty = 
    DependencyProperty.Register("Document", typeof(FlowDocument), 
    typeof(FsRichTextBox), new PropertyMetadata(OnDocumentChanged));

The Document property utilizes a PropertyChanged callback method, OnDocumentChanged(). This method determines whether the property change is coming from the control (because the app has triggered a bindings update), or from the view model. If the change is coming from the view model, the method passes the change through to the WPF RTB in the control. However, if the change is coming from the control, the method does nothing—remember, the property has already been changed.

The OnDocumentChanged() method uses the InternalUpdatePending flag to determine the origin of the change. Recall that the flag was set by the UpdateDocumentBindings() method when the host app (or the user) triggered an update. Note that the flag is an integer, rather than a Boolean—more on that in a minute. The OnDocumentChanged() method checks this flag and, if it is set, does nothing, other than decrementing the flag.

C#
/// <summary>
/// Called when the Document property is changed
/// </summary>
private static void OnDocumentChanged
	(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    /* For unknown reasons, this method gets called twice when the 
     * Document property is set. Until I figure out why, I initialize
     * the flag to 2 and decrement it each time through this method. */

    // Initialize
    var thisControl = (FsRichTextBox)d;

    // Exit if this update was internally generated
    if (thisControl.m_InternalUpdatePending > 0)
    {

        // Decrement flag and exit
        thisControl.m_InternalUpdatePending--;
        return;
    }

    // Set Document property on RichTextBox
    thisControl.TextBox.Document = (e.NewValue == null)? 
        new FlowDocument(): 
        (FlowDocument)e.NewValue;

    // Reset the TextChanged flag
    thisControl.m_TextHasChanged = false;
}

There is an anomaly concerning the OnDocumentChanged() method. For some reason, the method is being called twice when the FS RTB’s Document property changes. Quite frankly, I haven't been able to determine why this is happening, and so I have resorted to hacking my way around the problem. The InternalUpdatePending flag is created as an integer variable, rather than a Boolean, and is instantiated to a value of 2. On each pass through the OnDocumentChanged() method, the flag’s value is decremented, with the result that it causes the control to do nothing on both passes through the method, and it is cleared on the last pass through.

If a reader can tell me why the OnDocumentChanged() method is getting called twice, I'd be most appreciative. I'll replace the hack with a proper Boolean flag and will be happy to credit your contribution in an article update. In the meantime, the hack doesn't affect either the performance or the output of the control. It’s ugly, but it works.

MVVM in the Demo App

The demo app uses the MVVM pattern, so that you can see how the FS RTB control fits within MVVM. Part of the strength of the MVVM pattern is its flexibility; developers can implement the pattern in a number of ways, all of which may be regarded as good practice. With that in mind, a word or two on my implementation of MVVM may make the demo app easier to figure out. But you can easily skip this section, unless you are having a problem figuring out how I structured the demo app.

image002.gif

I use a variation of the View-first approach to MVVM. I implement a view model using multiple classes. The main class is, of course, the ViewModel class, which in the demo app is MainWindowViewModel.cs. I set this class as the DataContext of the view (MainWindow.xaml in the demo) in a third class that instantiates both the View and the ViewModel classes. In the demo app, I perform this step in App.xaml.cs, by overriding the OnStartup() method. Note that I also remove the StartupUri property from the < Application> tag in App.xaml.

So, in my implementation, neither the View nor the ViewModel instantiates the other. I use this third-class approach to loosen the couplings between the View and the ViewModel classes.

There will invariably be some dependencies between the View and the ViewModel, and I generally maintain these dependencies so that they run from the View to the ViewModel. In other words, the View knows about the ViewModel, but not vice-versa. Running the dependencies in the other direction, as is done in the PresentationModel pattern, is of course good practice; this is simply my preferred style of MVVM. In any event, the direction of the dependencies has little if any impact in the demo app.

I bind buttons and other command controls in the view to ICommand properties in the ViewModel class. These properties are bound to ICommand classes that I store in my ViewModel folder. The Execute() methods of my commands take simple actions themselves and delegate more complex actions to service classes in the business layer of the application. In the demo app, there is one command, LoadDocument. Its Execute() action is simple, so it implements it directly.

To keep things simple, I wired the Force Update button directly to an event handler in the MainWindow code-behind. That’s not good MVVM practice, and I wouldn't do that in a production app. Since this is a simple demo, I don't think it really matters.

If you are just learning MVVM, you might find my article, MVVM and the WPF DataGrid, helpful. I explain the MVVM approach I use and how I use it to structure a simple application.

Conclusion

As always, I appreciate the comments of the folks who read these articles. I hope you find the FS RTB useful. I plan to update this article from time-to-time to incorporate any suggestions made by readers, and I will, of course, provide credit in the update for those suggestions.

History

  • 16th March, 2010: Initial version
  • 17th March, 2010: Added VS2008 version of the code
  • 12th August, 2010: Article updated

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) Foresight Systems
United States United States
David Veeneman is a financial planner and software developer. He is the author of "The Fortune in Your Future" (McGraw-Hill 1998). His company, Foresight Systems, develops planning and financial software.

Comments and Discussions

 
GeneralMy Vote of 5 Pin
Nigel Shaw14-Dec-12 7:01
Nigel Shaw14-Dec-12 7:01 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.