7

We have a rather large WPF business application and I am working on a retool of an existing WPF FixedPage/FixedDocument report.

It's a somewhat busy ecosystem. We have a built-in forms generator, with lots of different controls you can put on (think like a mini built-in visual studio). All that works fine. You fill in the form on the screen, and then you can print out (to XPS) the identical copy to standard 8.5x11 paper.

In the code, we break out this report into vertical chunks. Say each chunk would be an inch or two tall on a printed piece of paper. This is how we handle pagination. If the next chunk is too tall for the page, we do a NewPage() and repeat. As I mentioned, this was working fine.

WPF has an enormous learning curve and I've been going back over old code and refactoring things and happily working with DataTemplates, strongly typed ViewModels, and generic ContentControls in order to reduce the size of our code. The on-screen forms generator still works, but the FixedDocument report has gotten weird.

Going back to those vertical slices, we print the user's forms to paper as individual Grid controls. Nothing fancy. Each grid (as I mentioned above) may be an inch or two high, containing any random mixture of checkboxes, radiobuttons, textblocks, and so on.

When the grids contained these stock (standard) MS WPF controls, I could do this all day long:

System.Windows.Controls.Grid g = .....

g.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
g.Arrange(new Rect(g.DesiredSize));

And get back proper sizes, i.e. 100 x 67.

Now, sometimes the grids have just one control - a header if you will (i.e. "This Month's Schedule). The only child control added to that grid is a ContentControl.

The ContentControl is simply bound to a ViewModel:

<ContentControl Content="{Binding}" />

There's then two DataTemplates in the resource dictionary that picks up this binding. Here, I'll show that:

<UserControl.Resources>

    <w:MarginConverter x:Key="boilerMargin" />

    <DataTemplate DataType="{x:Type render:BoilerViewModel}">
        <render:RtfViewer
            Width="{Binding Path=Width}"
            TextRTF="{Binding Path=Rtf}"/>
    </DataTemplate>

    <DataTemplate DataType="{x:Type render:Qst2NodeViewModel}">
        <ContentControl Content="{Binding Path=BoilerVm}">
            <ContentControl.Margin>
                <MultiBinding Converter="{StaticResource boilerMargin}">
                    <Binding Path="NodeCaptionVm.Height" />
                    <Binding Path="NodeLeft" />
                </MultiBinding>
            </ContentControl.Margin>
        </ContentControl>
    </DataTemplate>
</UserControl.Resources>

The ContentControl will pick up that bottom-most datatemplate. That template will then in turn use the smaller one above.

The fancy converter just sets a margin. It may be fugly to read, but this all displays correctly on the screen within the parent usercontrol. It's all the right size and justification and all that.

On the printed report side (XPS), I have to create these controls in code and measure them to see if they'll fit on the current FixedPage. When I go to do this step: (on the grid containing this ContentControl)

g.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
g.Arrange(new Rect(g.DesiredSize));

I get back 0,0 size. Even though it should be like 730x27 for instance. Again, on the screen, hosted in a UserControl, this all works fine. Just trying to instantiate it and measure it purely in code fails. I've confirmed that the control is added to the grid, has its row and col set, has been added to the Children collection, etc...

If I prepend those two statements with an UpdateLayout call, like this, then it works:

g.UpdateLayout();  //this fixes it
g.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
g.Arrange(new Rect(g.DesiredSize));

I've been reading that UpdateLayout is expensive and to be avoided, and I'd rather not be calling this on each grid section before I add it to my FixedPage of the FixedDocument report. There could be dozens or even hundreds of iterations. And, again, if the Grid has regular WPF controls in it, without any ContentControls and fancy finding and looking up datatemplates, the measuring works fine without the UpdateLayout call.

Any advice? Thank you!

I just don't understand why it became necessary to start calling it once I started utilizing the Xaml engine. It almost feels like I'm being punished for using the advanced features.

John Doh
  • 249
  • 6
  • 15

2 Answers2

7

Its complicated to explain that but let me try using plain words... In wpf everything works with dispatcher. Futhermore like you may already know dispatcher deals with tasks ordered by priority.

For example first a control is being initalized, then binding is being triggered, then values are updated, in the end all that is being measured.. etc etc

What you managed somehow is by setting all those contentcontrol inside contentcontrol stuff, you screwed up that order

Calling UpdateLayout basically forces dispatcher to finish its pending work in layout so you can work with clean layout afterwards

Screwing with dispatcher is quite common in wpf since some controls or values may be added later which ends in remeasuring things.

In your case you seem to be creating all at once in one method call without letting dispatcher take a breath. Therefore you need UpdateLayout method to normalize dispatchers queue.

I hope this helps you. You can also solve your issue by using Dispatcher.BeginInvoke.

dev hedgehog
  • 8,698
  • 3
  • 28
  • 55
  • I think you are on to something here. If I bypass the measuring code and just add the grid to my report (the current FixedPage of the FixedDocument), something very interesting happens. First, the grid is Measured and then Arranged. I stepped through this. It has the same problem as the arrangeBounds coming up as 0,0. Then, for some reason, it comes back and repeats the measuring. The second time though, it comes up with the actual/true sizes of the grid. I wish I knew why it was repeating itself. That might give me a better understanding of what I am missing – John Doh Jan 12 '15 at 15:06
  • Hard to tell for me i dont know your code but yea thats how things roll :) – dev hedgehog Jan 12 '15 at 15:14
  • On the second pass, I looked at the "external code" in the call stack. The second pass is when it actually gets the correct dimensions of the control. And I notice the WPF engine is doing this by itself calling UpdateLayout. On the first past, when it does not. Very interesting. Well, at least I know how to get my measurements, I still wish I understood why it works the first time when there's just normal controls on there that aren't templated or ContentControls. Probably for the reasons you state. If I find anything more, I'll update. thanks again – John Doh Jan 12 '15 at 16:04
  • As you maybe already know Microsoft has made their source code public. Here is the link: http://referencesource.microsoft.com/ If you take a better look in there, you will see what I was talking about in my answer. It took me ages to understand measuring in wpf but that link is the right place to start looking for explainations – dev hedgehog Jan 13 '15 at 07:47
2

UpdateLayout does not work in my case. I had to wait until dispatcher finishes processing of the layout tasks.

toPrint.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
Application.Current.Dispatcher.Invoke(new Action(() => { }), DispatcherPriority.ContextIdle);
toPrint.Arrange(new Rect(new Point(0, 0), toPrint.DesiredSize));

I found another article about this approach.

Der_Meister
  • 4,771
  • 2
  • 46
  • 53