|
Welcome,
Guest
|
|
TOPIC: A Bindable WPF RichTextBox
A Bindable WPF RichTextBox 21 Mar 2010 13:27 #318
|
Introduction
WPF’s RichTextBox (RTB) is very good, but it suffers from several shortcomings:
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. 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: <fsc:FsRichTextBox ... Document="{Binding Path=DocumentXaml, Converter={StaticResource The full implementation appears in MainWindow.xaml. Here is the code for the value converter: using System.Windows.Data;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: <fsc:FsRichTextBox x:Name="EditBox" Document="{Binding Path=DocumentXaml, And here is the text box declaration: <TextBox Text="{Binding DocumentXaml}" Margin="10,5,10,10" Grid.Row="2" 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 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 section of the user control XAML: <ControlTemplate x:Key="FlatButtonControlTemplate" TargetType="{x:Type Button}">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: /// <summary>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: // Document propertyThe 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. /// <summary>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. This attachment is hidden for guests. Please log in or register to see it.
Attachments:
|
|
|
|
|
Moderators: mnjon
Time to create page: 1.03 seconds







