« Welcome to code.logos.com! | Main | Null-propagating extension method »

January 11, 2008

Data binding in a FlowDocument or Text Block

One of the great features of WPF is the flow content model for text. The TextBlock element can be used to display inline elements that allow bold, italic, hyperlinks, etc. The FlowDocument content element can represent entire documents of rich content -- paragraphs, tables, figures, etc. FlowDocumentScrollViewer and similar elements can be used to display a FlowDocument.

However, one of the most annoying things about working with the flow content model is the poor support for data binding. In particular, the Text property of the Run element is not a dependency property and thus is not bindable, which means that simple data-bound placeholders in the content aren't feasible.

<!-- this doesn't work -->

<TextBlock>Name: <Run Text="{Binding Name}" />

</TextBlock>

A common workaround for this problem is to replace the Run with a TextBlock, since the Text property of a TextBlock is bindable. This workaround is unsatisfying, however, because the text in the TextBlock does not wrap properly with surrounding text, selection doesn't work properly, etc.

<!-- this isn't ideal -->

<TextBlock>Name: <TextBlock Text="{Binding Name}" />

</TextBlock>

A better workaround is provided by Filipe Fortes and Paul Stovell. Filipe proposes a BindableRun class that derives from Run and provides a BoundText dependency property that sets the Text property whenever it changes. Paul proposes an attached dependency property BindableText that has the same effect. We use a similar attached property at Logos, but here's an example of Filipe's solution:

<!-- this usually works -->

<TextBlock>Name: <bt:BindableRun BoundText="{Binding Name}" />

</TextBlock>

Either solution is great -- except for the error. In comments to both Filipe's and Paul's posts, as well as in an MSDN forum post, you'll find that users of this technique sometimes encounter a confusing error: Collection was modified; enumeration operation may not execute.

A reply to the MSDN forum post by Ifeanyi Echeruo provides a workaround for the error -- delay the setting of the Text property by using Dispatcher.BeginInvoke. This workaround is effective, but it can result in annoying flickering -- the user will probably be able to see the text of the Run change from the empty string to the bound value.

Fortunately, we've found an improved workaround. Here's the short version: set the DataContext property on the BindableRun as follows (but read the update below):

<!-- this always works -->

<TextBlock>Name: <bt:BindableRun BoundText="{Binding Name}"

    DataContext="{Binding DataContext, RelativeSource=

    {RelativeSource AncestorType=FrameworkElement}}" />

</TextBlock>

The error occurs when the binding is updated because of a change to an inherited dependency property. The most common scenario is when the inherited DataContext changes. I don't have access to the WPF source code, but, based on the call stack, it appears that an inherited properly like DataContext is propagated to its descendants. When the enumeration of descendants gets to the BindableRun, the BindableText properly changes according to the new DataContext, which sets the Run property. However, for some reason, changing the flow content invalidates the enumeration and raises an exception.

Direct binding doesn't cause the enumeration of descendants. So, to avoid the error, don't allow your binding to depend on any inherited dependency properties. The most commonly used inherited dependency property is the DataContext, which is used implicitly by data bindings that don't specify an explicit source. You can avoid using the inherited DataContext by setting the Source or RelativeSource on the binding. Easier still, you can set the DataContext of the BindableRun directly, which updates your DataContext without relying on the enumeration described above. The very simplest way to solve the problem, then, as shown above, is to bind the DataContext to that of its FrameworkElement ancestor, which is outside of the flow content and thus doesn't update while the flow content is being enumerated.

Update: In some circumstances, the use of RelativeSource for the DataContext binding can result in the DataContext being null when the UI element is first displayed, only to be corrected after a moment. To avoid this behavior, bind directly to the ancestor by name.

<!-- this works even better -->

<TextBlock x:Name="tb">Name: <bt:BindableRun

    BoundText="{Binding Name}"

    DataContext="{Binding DataContext, ElementName=tb}}" />

</TextBlock>

We also experienced printing problems when using the RelativeSource solution, so I highly recommend the ElementName solution.

Posted by Ed Ball at January 11, 2008 12:35 PM

Trackback Pings

TrackBack URL for this entry:
http://ancientblogs.logos.com/mt-cgi/mt-tb.cgi/172

Listed below are links to weblogs that reference Data binding in a FlowDocument or Text Block:

» The Weekly Source Code 40 - TweetSharp and Introducing Tweet Sandwich from Scott Hanselman's Computer Zen
[Read More]

Tracked on April 3, 2009 12:04 AM