Reliable way of exporting complex UIElements to a bitmap
See original GitHub issueOne topic that has come up in the previous years a lot from customers of our WPF-based library is that image exports can be unreliable when using XAML-based controls inside. The exhibited problems vary between:
- missing visuals
- misplaced visuals
- bindings not applied
- triggers not run
- values from converters not applied
Last year I’ve fixed those, or so I thought, by changing how we actually export our images. The previous code path looked basically as follows:
// Export visual
var visual = ourControl.ExportEverythingAsVisual();
// Place in container
// container is a custom class, but basically a UIElement that can contain both Visuals and other UIElements
// and passes Measure/Arrange calls to its children, as well as implement our own layout logic, which uses a custom AttachedProperty on our control
container.Add(visual);
// Perform a single layout pass (or so we thought)
container.Measure(new Size(imageWidth, imageHeight));
container.Arrange(new Rect(0, 0, imageWidth, imageHeight));
// write to RenderTargetBitmap
bitmap.Render(container);
// encode and save to file; not relevant here, I guess
Upon reading the ContextLayoutManager source code a bit I noticed that there are some things that won’t ever work correctly with explicit calls to Measure/Arrange since they defer part of the work into a second layout pass, which only the actual layout manager can resolve and perform. I think things like event handlers on SizeChanged
can cause this, or SharedSizeGroups
for grids.
So I then set out to try to fix our export with the knowledge I had at that time. I even finally had a test case by a customer that was simple enough to unit-test to verify that things worked correctly after the fix, whereas they were broken before. The new code basically looked as follows:
// Perform a single layout pass - doesn't help much, but shouldn't hurt much either, except performance
container.Measure(new Size(imageWidth, imageHeight));
container.Arrange(new Rect(0, 0, imageWidth, imageHeight));
// Wait for WPF's layout to finish by scheduling at a priority _after_ layout
ourControl.Dispatcher.Invoke(DispatcherPriority.Loaded, new Action(() => {
// write to RenderTargetBitmap
bitmap.Render(container);
// encode and save to file; not relevant here, I guess
}));
Turns out, this works most of the time, but apparently not always. One customer performed image exports of different diagrams in a loop and found random elements to be missing from the export. From reading the source code I remembered that the layout manager may defer remaining elements to a later frame if layout takes too long. What I didn’t realize at the time was that those deferred updated are posted at Background
priority, so waiting for Loaded
will get all updates from the first 153 elements + 153×2 ms, but would not get the results of the later layouts that are happening at a lower priority.
My current guess would be that things might work when changing Loaded
above to ContextIdle
. But it still nags at me that I cannot figure out the »correct« way of exporting UIElements so that all layout has happened at export time. VisualBrush
somehow manages to wait for all that before painting and so do printing or XPS export, but those have the benefit of being able to call internal APIs.
Is there an official correct way of waiting for layout completion before rendering to a bitmap? Am I on a completely dangerous and wrong path? Almost correct but missing an important detail?
Issue Analytics
- State:
- Created 3 years ago
- Reactions:4
- Comments:7 (3 by maintainers)
Top GitHub Comments
Try adding the grid to a convenient place in the visual tree, but setting Visibility=“Hidden” to keep it from displaying. (Convenient means some place where its layout space won’t affect anything else.)
If your container isn’t getting added to the measure queue, it won’t get all the benefits of the real layout algorithm. For example, it will see the Measure you call explicitly, but not any subsequent re-measures that may arise due to changes in its subtree (such as a binding providing a new value that changes the size of a TextBlock).
First, don’t call Measure/Arrange directly; instead just call
container.InvalidateMeasure()
(after ensuring itsHeight
andWidth
are set to the desired values). This marks your container as “dirty” and causes it to participate in the next layout pass.Second, there is no guaranteed way to tell when layout is complete. Each layout pass calls Measure and Arrange on a set of dirty elements, then raises events about the changes that happened. Any of these calls can introduce new elements or make existing elements dirty, requiring another layout pass. The “move layout to Background” logic you mentioned is WPF’s way of coping with this potentially never-ending process; it keeps complicated layouts from starving the render thread or the reaction to user input.
In practice, layouts are rarely that complicated; waiting for Loaded priority usually works, and ContextIdle should work except in cases that explicitly ask for infinite layout (which, like infinite loops in programs, are impossible to detect in all cases). Another alternative is to listen for the
LayoutUpdated
event, which is raised near the end of each layout pass.Third, you can query the
IsMeasureValid
andIsArrangeValid
properties on your container to see whether its layout is current. If not, simply callBeginInvoke
to try again later.