Share via



April 2009

Volume 24 Number 04

Dynamic WPF - Create Flexible UIs With Flow Documents And Data Binding

By Vincent Van Den Berghe | April 2009

Code download available

This article discusses:

  • Flow Documents
  • Data binding issues
  • Building a data-bound control
  • Using templates
This article uses the following technologies:
WPF, .NET 3.5 SP1

Contents

Objective
The Bindable Run
Templates for FrameworkContentElements
The ItemsContent Implementation
Conclusion

Flow documents areideal for displaying read-only data in Windows Presentation Foundation (WPF). They offer enormous flexibility in arranging text layout and pagination. In addition, flow document controls provide you with annotations, printing support and clipboard functionality almost for free. Doing this with traditional WPF controls (TextBlock, ItemsControl and derivatives) is much more difficult.

While there are many great features in flow documents, if your documents are generated from dynamic data, you have a bit of a problem: there is no support for data binding in flow documents. The flow document elements (Section, Table, Run, Paragraph and the like) are dependency objects, but don't define any dependency properties that would allow you to dynamically change or generate content.

Any update to portions of the flow document must be done by the component that generated the flow document in the first place, since flow documents do not support data binding directly. In this respect, flow documents are much less flexible than the other WPF framework elements.

Adding the missing dependency properties looks like an easy solution but sadly, this isn't the case. As often is the case, the devil is in the details, and you'll have to get quite a few details right to make it work well.

To make this problem more concrete, consider the following simple XML document sample contained in an XmlDataProvider:

<XmlDataProvider x:Key="DataSource"> <x:XData> <NorthwindNames > <Person FirstName="Nancy" LastName="Davolio" /> <Person FirstName="Andrew" LastName="Fuller" /> <Person FirstName="Janet" LastName="Leverling" /> <Person FirstName="Margaret" LastName="Peacock" /> <Person FirstName="Steven" LastName="Buchanan" /> </NorthwindNames> </x:XData> </XmlDataProvider>

Displaying this data using normal data binding methods and our trusty ListView control is a piece of cake. Here's the code:

<ListView ItemsSource="{Binding Source={StaticResource DataSource}, XPath=NorthwindNames/Person}"> <ListView.View> <GridView> <GridViewColumn Header="First name" DisplayMemberBinding="{Binding XPath=@FirstName}" /> <GridViewColumn Header="Last name" DisplayMemberBinding="{Binding XPath=@LastName}" /> </GridView> </ListView.View> </ListView>

The result is as expected, as you can see in Figure 1.

fig01.gif

Figure 1 The Data in a ListView

Now I want to show this as a table in a flow document. Ideally, I would like a component similar to ListView, but which understands binding to an item's source, and which can generate the table content dynamically inside a FlowDocument. In other words, I need a kind of ItemsControl, but for flow documents. And no cheating with InlineUIContainer or BlockUIContainer: although these allow you to embed any WPF control like ListView, you'll lose their content when copying to the clipboard. Embedding controls this way doesn't magically turn them into flow document elements.

Such a component doesn't exist, so I'll need to write it myself. I'll call it ItemsContent, to reflect the fact that it generates flow content for data items. To get a better idea about what I need to design, let's think about what the markup of that component would look like. Inspired by features ItemsControl provides, something like Figure 2would fit the bill nicely.

Figure 2 The ItemsContent Control

<ItemsContent ItemsSource="{Binding Source={StaticResource DataSource}, XPath=NorthwindNames/Person}" > <ItemsContent.ItemsPanel> <DataTemplate> <Table BorderThickness="1" BorderBrush="Black"> <TableRowGroup IsItemsHost="True"> <TableRow Background="LightBlue"> <TableCell> <Paragraph>First name</Paragraph> </TableCell> <TableCell> <Paragraph>Last name</Paragraph> </TableCell> </TableRow> </TableRowGroup> </Table> </DataTemplate> </ItemsContent.ItemsPanel> <ItemsContent.ItemTemplate> <DataTemplate> <TableRow> <TableCell> <Paragraph> <Run Text="{Binding XPath=@FirstName}"/> </Paragraph> </TableCell> <TableCell> <Paragraph> <Run Text="{Binding XPath=@LastName}"/> </Paragraph> </TableCell> </TableRow> </DataTemplate> </ItemsContent.ItemTemplate> </ ItemsContent>

As you can see, the markup follows the pattern of the existing ItemsControl closely. There's an ItemsPanel template which determines how the generated flow document content will be embedded.

The ItemsPanel tells the control to generate a two-column table with fixed header information in the first row, with a light blue background. Additional rows will be appended to the TableRowGroup containing IsItemsHost="True" (which happens to be that same table row group as our header, which is OK). This is similar to how you mark a panel hosting items in the ItemsPanel template of a ItemsControl.

The ItemTemplate will determine the content to be generated for each item in our ItemsSource.

These rows will be appended to the TableRowGroup in our ItemsPanel as explained above.

Executing this markup against the XML document would generate the FlowDocument fragment shown in Figure 3.

Figure 3 FlowDocument Excerpt

<Table BorderThickness="1" BorderBrush="Black"> <TableRowGroup> <TableRow Background="LightBlue"> <TableCell><Paragraph>First name</Paragraph></TableCell> <TableCell><Paragraph>Last name</Paragraph></TableCell> </TableRow> <TableRow> <TableCell><Paragraph>Nancy</Paragraph></TableCell> <TableCell><Paragraph>Davolio</Paragraph></TableCell> </TableRow> <TableRow> <TableCell><Paragraph>Andrew</Paragraph></TableCell> <TableCell><Paragraph>Fuller</Paragraph></TableCell> </TableRow> <TableRow> <TableCell><Paragraph>Janet</Paragraph></TableCell> <TableCell><Paragraph>Leverling</Paragraph></TableCell> </TableRow> <TableRow> <TableCell><Paragraph>Margaret</Paragraph></TableCell> <TableCell><Paragraph>Peacock</Paragraph></TableCell> </TableRow> <TableRow> <TableCell><Paragraph>Steven</Paragraph></TableCell> <TableCell><Paragraph>Buchanan</Paragraph></TableCell> </TableRow> </TableRowGroup> </Table>

That code, in turn, renders the table in Figure 4.

fig04.gif

Figure 4 The Data in a Flow Document

Figure 5 BindableRun

public class BindableRun : Run { public static readonly DependencyProperty BoundTextProperty = DependencyProperty.Register("BoundText", typeof(string), typeof(BindableRun), new PropertyMetadata(OnBoundTextChanged)); public BindableRun() { Helpers.FixupDataContext(this); } private static void OnBoundTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { ((Run)d).Text = (string)e.NewValue; } public String BoundText { get { return (string)GetValue(BoundTextProperty); } set { SetValue(BoundTextProperty, value); } } }

Wouldn't it be nice if we had such a thing? As you can see, this component is remarkably flexible. Besides data binding, it also enables the use of templates in flow documents. It doesn't solve every flow document data binding problem out there, but if we can pull this off, all other data binding problems will look almost trivial in comparison.

In the remainder of the article I'll show how to build such a beast, or at least something that resembles it closely. In addition, I hope to provide sufficient background information for you to tackle any other flow document data binding problem. Read on.

The Bindable Run

If you search the Web for flow documents and data binding examples, you're bound (so to speak) to stumble across BindableRun (fortes.com/2007/03/bindablerun). It comes in many flavors, and in its simplest form looks like Figure 5.

The implementation is straightforward: we inherit from Run, define a dependency property BoundText, and synchronize Run's Text property with its value. Run is a dependency object, but its Text content isn't a dependency property. BoundText is simply the dependency property version of Text, which allows us to use data binding expressions. It's that simple.

Its usage is therefore straightforward. Instead of <Run …/>, you use the following code where flowdoc is the XML namespace where your definitions reside:

<flowdoc:BindableRun BoundText="{Binding …}" />

Unfortunately, this class will sometimes fail. Specifically, the exception "Collection was modified; enumeration operation may not execute" will be thrown if the binding expression for BoundText depends on the value of an inherited dependency property such as DataContext, and binding evaluation was performed during inheritance propagation. (See the MSDN forum thread about data flowfor an explanation of that error.)

In the light of the component we're trying to develop, it's important to fully understand the mechanism that causes this exception, because you'll face it again. For this, you need to look deeply into WPF to see what happens during property value inheritance for objects whose type derives from FrameworkContentElement. We can restrict ourselves to classes that derive from FrameworkContentElement, since all flow document elements derive from it. (For a discussion about FrameworkContentElement and FrameworkElement see wpfdisciples.wordpress.com/2008/10/09.)

When the value of an inheritable dependency property like DataContext is set on some logical ancestor of a flow document element, the following actions happen:

  • DependencyObject.SetValue is called to set the value of the dependency property on the ancestor object.
  • DependencyObject calls its internal NotifyPropertyChange method.
  • NotifyPropertyChange calls the virtual method OnNotifyPropertyChange, which is overridden by FrameworkContent-Element.
  • FrameworkContentElement's implementation of OnNotifyPropertyChange realizes that an inheritable property has changed, and it needs to notify all its (logical) descendants of that fact.

For this purpose, the internal TreeWalkHelper.InvalidateOnInheritable method is called, which recursively enumerates all the descendants (using the internal class DescendantsWalker) and visits each one, calling TreeWalkHelper.OnInheritablePropertyChanged for each.

So far so good. As long as you're working with standard flow document elements, the above steps will always succeed. But if one of your descendants in the inheritance chain is the BindableRun defined earlier and your BoundText depends on the inherited DataContext value, then the following happens during value propagation:

  • The value for the dependency property BoundText is changed due to the reevaluation of its associated binding expression.
  • BindableRun.OnBoundTextChanged is fired and sets the Text property.
  • The setter for Text calls BeginChange on the Run's TextContainer and TextContainer increments its generation number (essentially an internal version number used to track changes to the container).
  • The current active enumerator TreeWalkHelper was using (a RangeContentEnumerator) detects the change of generation number during its next call to MoveNext() and fails with the exception "Collection was modified; enumeration operation may not execute."

You can't solve this problem, since the above process is baked into WPF and the crucial types and methods implementing it are internal. You can, however, circumvent the problem by "breaking" the value propagation. One way of doing this is to bind BindableRun's DataContext to the corresponding property of a FrameworkElement ancestor. This is so useful that I've put it in its own helper class:

public static void FixupDataContext(FrameworkContentElement element) { Binding b = new Binding( FrameworkContentElement.DataContextProperty.Name); b.RelativeSource = new RelativeSource( RelativeSourceMode.FindAncestor, typeof(FrameworkElement), 1); element.SetBinding(FrameworkContentElement.DataContextProperty, b); }

Potentially, this workaround needs to be applied for all the flow document elements we derive from.

This works, as long as a FrameworkElement always exists up in the logical parent chain, and the dependency property (BoundText in case of BindableRun) doesn't depend on any other inherited property propagated by the system.

By design, the first point will always be true (reading the next section will make you understand why). However, you can't always be sure of the second; clearly, some data binding scenarios will cause this workaround to fail, and you'll have to revisit it later.

Templates for FrameworkContentElements

To implement the control, you need the flexibility of templates. Ideally, you want to be able to define fragments of flow documents in data templates. With BindableRun already at your disposal, you want to write the code in Figure 6. Here flowdoc is the chosen namespace where you defined the new classes.

Figure 6 New Classes in flowdoc

<DataTemplate> <TableRow> <TableCell> <Paragraph> <flowdoc:BindableRun BoundText="{Binding XPath=@FirstName}" /> </Paragraph> </TableCell> <TableCell> <Paragraph> <flowdoc:BindableRun BoundText="{Binding XPath=@LastName}"/> </Paragraph> </TableCell> </TableRow> </DataTemplate>

Try this in your favorite designer (using DataTemplate or ControlTemplate, it doesn't matter), and you'll get the following error:

"Property 'VisualTree' does not support values of type 'TableRow'."

VisualTree is the default content property of FrameworkTemplate, the common base type of both DataTemplate and ControlTemplate. The documentation for FrameworkTemplate is sparse, but its description clearly states that it enables the instantiation of a tree of FrameworkElement and/or FrameworkContentElement objects.

fig07.gif

Figure 7 FrameworkElement and FrameworkContentElement

This description is misleading or incomplete at best. It turns out that a FrameworkTemplate can instantiate only a tree whose root node is a FrameworkElement. If you need to instantiate a tree whose root is a FrameworkContentElement, you're out of luck. As the error message indicates, the VisualTree property is the key to understanding this: it points to a FrameworkElementFactory, which can only produce FrameworkElements, at least as root nodes.

This, by the way, is why a FrameworkElement always exists up in the logical parent chain, as noted in the previous section.

You may wonder why such restriction exists. Indeed, this need not be a conceptual limitation; one can easily see the usefulness of templates that are able to generate instances of both classes. Moreover, the public and protected members of both classes are very similar, and they implement many of the same interfaces (IFrameworkInputElement, IInputElement, ISupportInitialize).

One of the key differences that prevents factoring out their commonalities is the fact that they occur at different points in the inheritance hierarchy, as you see in Figure 7.

This looks like a job for mix-in classes, but for this you need multiple inheritance, which isn't possible in .NET. Maybe there's truth in the old saying that you don't need multiple inheritance often, but if you do, you need it badly.

Still, one wonders if the design couldn't have been better. A closer examination of the implementation of FrameworkElementFactory shows a hardcoded knowledge of types like Grid, GridViewRowPresenter, RowDefinition, ColumnDefinition and Visual3D. One would expect the knowledge of constructing specific instances of types to be abstracted from the implementation of their common factory. Otherwise, you end up with a very rigid and inextensible factory implementation, which, in addition, will force you to revisit it every time you add another special type to the system (and you should somehow know which types are special). This smells like a violation of the Open/Closed design principles.

Since we have insufficient information to ascertain what should have been done (and we can't change a thing anyway), I'll leave this discussion here and focus on a solution: how do we define templates of FrameworkContentElements? The easiest solution, requiring the least amount of code, is to build a bridge between the two types.

Such bridges already exist in WPF: The BlockUIContainer is a flow document element for embedding UIElements in flow documents. So it's a bridge, but we need one in the opposite direction.

Another existing bridge is a TextBlock: a FrameworkElement that can contain a collection of inlines, which are a special kind of FrameworkContentElement. If you restrict yourself to inlines, this would be perfect, but you don't want this restriction. That bridge is too small for your purposes.

It turns out that the bridge class is easy to write. Let's call it Fragment, because it will hold fragments of flow documents (see Figure 8).

Figure 8 The Fragment Class

[ContentProperty("Content")] public class Fragment : FrameworkElement { private static readonly DependencyProperty ContentProperty = DependencyProperty.Register("Content", typeof(FrameworkContentElement), typeof(Fragment)); public FrameworkContentElement Content { get { return (FrameworkContentElement)GetValue(ContentProperty); } set { SetValue(ContentProperty, value); } } }

As you can see, there's nothing earth-shattering here: it's a simple class deriving from FrameworkElement (of course), with a dependency property holding a FrameworkContentElement root as default content.

With this class, you can now write something like this:

<DataTemplate> <flowdoc:Fragment> <TableRow>…</TableRow> </flowdoc:Fragment> </DataTemplate>

To make use of these template definitions, you need to be able to load it at some point. You need a way to specialize the template-loading mechanism and return the content of the fragment. You can take care of this once and for all in another helper function, which is shown in Figure 9.

Figure 9 Helper Function

public static FrameworkContentElement LoadDataTemplate (DataTemplate dataTemplate) { object content = dataTemplate.LoadContent(); if (content is Fragment) return (FrameworkContentElement)((Fragment)content).Content; else if (content is TextBlock) { InlineCollection inlines = ((TextBlock)content).Inlines; if (inlines.Count == 1) return inlines.FirstInline; else { Paragraph paragraph = new Paragraph(); while (inlines.FirstInline != null) paragraph.Inlines.Add(inlines.FirstInline); return paragraph; } } else throw new Exception("Data template needs to contain a <Fragment> or <TextBlock>"); }

This helper function also handles the existing TextBlock bridge. If the TextBlock contains only one inline element, that element is returned. Otherwise, the entire inline collection is nicely wrapped in a Paragraph. This wrapping is needed, because an InlineCollection is itself not a FrameworkContentElement.

Note that the obvious way to move inline elements from one text container to another does not work:

foreach (var inline in inlines) paragraph.Inlines.Add(inline);

The code will throw the exception "Collection is modified, enumeration operation may not execute" as described before, and for similar reasons: adding an element from one text container (a TextBlock) to another (a Paragraph) implicitly removes it from the first text container, causing its generation number to change. Since a TextElementEnumerator, like a RangeContentEnumerator, also checks for its TextContainer generation number, the same exception will be thrown.

The difference between this situation and the previous one is that you now have sufficient control to circumvent the problem at the source. That's why you can write the following, which accomplishes the same thing, but without using an enumerator:

while (inlines.FirstInline != null) paragraph.Inlines.Add(inlines.FirstInline);

The ItemsContent Implementation

You almost have all the pieces of the puzzle for your implementation, but there's one design decision left to make. You need to decide whether your flow content element will be a block-level element or an inline-level element. The decision is arbitrary, but one needs to make it, since no control can be both.

In this article, the ItemsContent control will be a block-level element. The inline version is left as an exercise for the reader. I won't derive from Block directly, but from Section, which has a convenient Blocks collection property, to which I'll add my dynamic content. Section also doesn't apply any default formatting to the elements it contains, which is exactly what I need for a custom control.

Be careful about which base class you derive from. You may be tempted to derive from Block directly, but you won't get far. Crucial helper classes like BlockCollection (a derivative of the generic TextElementCollection<>) have internal constructors and are therefore unusable in your own code.

Basically, the implementation of ItemsContent is straightforward: just a bunch of dependency properties, and the usual suspects to track their changes. The real action happens in the GenerateContent method, which is the guts of the control and deserves closer inspection (see Figure 10). (Note, the remainder of the code can be found in the code download.)

Figure 10 The GenerateContent Method

private void GenerateContent(DataTemplate itemsPanel, DataTemplate itemTemplate, IEnumerable itemsSource) { Blocks.Clear(); if (itemTemplate != null && itemsSource != null) { FrameworkContentElement panel = null; foreach (object data in itemsSource) { if (panel == null) { if (itemsPanel == null) panel = this; else { FrameworkContentElement p = Helpers.LoadDataTemplate(itemsPanel); Blocks.Add((Block)p); panel = Attached.GetItemsHost(p); if (panel == null) throw new Exception("ItemsHost not found. Did you forget to specify Attached.IsItemsHost?"); } } FrameworkContentElement element = Helpers.LoadDataTemplate(itemTemplate); element.DataContext = data; Helpers.UnFixupDataContext(element); if (panel is Section) ((Section)panel).Blocks.Add(Helpers.ConvertToBlock(data, element)); else if (panel is TableRowGroup) ((TableRowGroup)panel).Rows.Add((TableRow)element); else throw new Exception(String.Format("Don't know how to add an instance of {0} to an instance of {1}", element.GetType(), panel.GetType())); } } } private void GenerateContent() { GenerateContent(ItemsPanel, ItemTemplate, ItemsSource); } private void OnItemsSourceChanged(IEnumerable newValue) { if (IsLoaded) GenerateContent(ItemsPanel, ItemTemplate, newValue); } private void OnItemTemplateChanged(DataTemplate newValue) { if (IsLoaded) GenerateContent(ItemsPanel, newValue, ItemsSource); } private void OnItemsPanelChanged(DataTemplate newValue) { if (IsLoaded) GenerateContent(newValue, ItemTemplate, ItemsSource); } private static void OnItemsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { ((ItemsContent)d).OnItemsSourceChanged((IEnumerable)e.NewValue); } private static void OnItemTemplateChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { ((ItemsContent)d).OnItemTemplateChanged((DataTemplate)e.NewValue); } private static void OnItemsPanelChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { ((ItemsContent)d).OnItemsPanelChanged((DataTemplate)e.NewValue); } private void GenerateContent(DataTemplate itemsPanel, DataTemplate itemTemplate, IEnumerable itemsSource) { Blocks.Clear(); if (itemTemplate != null && itemsSource != null) { FrameworkContentElement panel = null; foreach (object data in itemsSource) { if (panel == null) { if (itemsPanel == null) panel = this; else { FrameworkContentElement p = Helpers.LoadDataTemplate(itemsPanel); if (!(p is Block)) throw new Exception("ItemsPanel must be a block element"); Blocks.Add((Block)p); panel = Attached.GetItemsHost(p); if (panel == null) throw new Exception("ItemsHost not found. Did you forget to specify Attached.IsItemsHost?"); } } FrameworkContentElement element = Helpers.LoadDataTemplate(itemTemplate); element.DataContext = data; Helpers.UnFixupDataContext(element); if (panel is Section) ((Section)panel).Blocks.Add(Helpers.ConvertToBlock(data, element)); else if (panel is TableRowGroup) ((TableRowGroup)panel).Rows.Add((TableRow)element); else throw new Exception(String.Format("Don't know how to add an instance of {0} to an instance of {1}", element.GetType(), panel.GetType())); } } } } }

The first order of business when generating or regenerating the content is to clear the Blocks collection. A simple Blocks.Clear(); effectively clears the content of the section control.

Next, you need to determine the panel you'll use to host your content. If an ItemsPanel template hasn't been specified, the ItemsContent control itself will be the host:

if (itemsPanel == null) panel = this;

Otherwise, the ItemsPanel template will be loaded and added to the final content, and the element specifying IsItemsHost will be used as the panel to host the content. Note that because the control is a block element, the panel is also assumed to be a block element.

FrameworkContentElement p = Helpers.LoadDataTemplate(itemsPanel); if (!(p is Block)) throw new Exception("ItemsPanel must be a block element"); Blocks.Add((Block)p); panel = Attached.GetItemsHost(p); if (panel == null) throw new Exception("ItemsHost not found. Did you forget to specify Attached.IsItemsHost?");

The IsItemsHost is defined as an attached property, because you want to be able to specify any (block) element as a host for your items. This has been neatly encapsulated in the Attached class, whose trivial implementation I won't discuss here.

All this is performed only once, if there is at least one item in the ItemsSource. For every item in the items source, the ItemTemplate is loaded, and the data context of the element is set to our current item:

FrameworkContentElement element = Helpers.LoadDataTemplate(itemTemplate); element.DataContext = data; Helpers.UnFixupDataContext(element);

The crucial thing to do at each iteration is to set the data context to the "current" item. This will enable the familiar per-item binding scenarios. The assignment is necessary but not sufficient: remember that for items like BindableRun, we've changed the DataContext binding behind the scenes. As a result, the DataContext property for these items will never have the intended value.

To make it work correctly, you need to remove all these bindings. This is the task of the Helpers.UnFixupDataContext function. That helper function traverses the logical tree, removing all bindings that were set by Helpers.FixupDataContext.

Note that the order of execution is extremely important. If you called Helpers.UnFixupDataContext before assigning the data context, the "Collection was modified; enumeration operation may not execute" exception would raise its ugly head again. You need to set the DataContext first, and then remove all your implicit bindings, one item at a time.

This makes the implementation of Helpers.UnFixupDataContext a little unusual, because the logical tree traversal is careful enough not to use any enumerators that could fail. There's one function that removes a single binding and then returns. It's shown in Figure 11.

Figure 11 InternalUnFixupDataContext

private static bool InternalUnFixupDataContext(DependencyObject dp) { // only consider those elements for which we've called // FixupDataContext(): // they all belong to this namespace if (dp is FrameworkContentElement && dp.GetType().Namespace == typeof(Helpers).Namespace) { Binding binding = BindingOperations.GetBinding(dp, FrameworkContentElement.DataContextProperty); if (binding != null && binding.Path != null && binding.Path.Path == FrameworkContentElement.DataContextProperty.Name && binding.RelativeSource != null && binding.RelativeSource. Mode == RelativeSourceMode.FindAncestor && binding.RelativeSource. AncestorType == typeof(FrameworkElement) && binding.RelativeSource. AncestorLevel == 1) { BindingOperations.ClearBinding(dp, FrameworkContentElement. DataContextProperty); return true; } } // as soon as we have disconnected a binding, return. Don't continue //the enumeration, // since the collection may have changed foreach (object child in LogicalTreeHelper.GetChildren(dp)) if (child is DependencyObject) if (InternalUnFixupDataContext((DependencyObject)child)) return true; return false; }

If a binding was undone, the function returns true, otherwise it returns false. The function is continually called until no bindings are left to undo, as you see here:

public static void UnFixupDataContext(DependencyObject dp) { while (InternalUnFixupDataContext(dp)) ; }

It's a little inefficient, but doesn't harm any enumerators in the process.

Conclusion

I set out to write a control that would enable you to use data binding and templates in flow documents. I even wrote the markup I would like to use. The control has been developed, and Figure 12shows the final markup for the example.

Figure 12 The Final Markup

<flowdoc:ItemsContent ItemsSource="{Binding Source= {StaticResource DataSource},XPath=FortressGuys/Person}" > <flowdoc:ItemsContent.ItemsPanel> <DataTemplate> <flowdoc:Fragment> <Table BorderThickness="1" BorderBrush="Black"> <TableRowGroup flowdoc:Attached.IsItemsHost="True"> <TableRow Background="LightBlue"> <TableCell> <Paragraph>First name</Paragraph> </TableCell> <TableCell> <Paragraph>Last name</Paragraph> </TableCell> </TableRow> </TableRowGroup> </Table> </flowdoc:Fragment> </DataTemplate> </flowdoc:ItemsContent.ItemsPanel> <flowdoc:ItemsContent.ItemTemplate> <DataTemplate> <flowdoc:Fragment> <TableRow> <TableCell> <Paragraph> <flowdoc:BindableRun BoundText="{Binding XPath=@FirstName}" /> </Paragraph> </TableCell> <TableCell> <Paragraph> <flowdoc:BindableRun BoundText="{Binding XPath=@LastName}"/> </Paragraph> </TableCell> </TableRow> </flowdoc:Fragment> </DataTemplate> </flowdoc:ItemsContent.ItemTemplate> </flowdoc:ItemsContent>

As you can see, this markup is quite close to what I wished for originally. Improvements are always possible, but for the amount of code we had to write, this is a very good result indeed. Mission accomplished. The accompanying code in the code download shows the component in action.

Vincent Van Den Berghe holds Masters degrees in Computing Science and Artificial Intelligence. He is a software engineer working for Bureau van Dijk Electronic Publishing. His current work involves developing LOB applications with WCF and WPF. Vincent is an ACM (Association for Computing Machinery) professional member.