Reliable way of exporting complex UIElements to a bitmap
- Dominant language
- C#
- Stars
- 7.7k
- Forks
- 1.3k
- Avg merge
- 1d 11h
- Merged PRs (30d)
- 61
Description
One 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:
```csharp
// 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:
```csharp
// 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](https://github.com/dotnet/wpf/blob/d49f8ddb889b5717437d03caa04d7c56819c16aa/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/LayoutManager.cs#L297-L309). 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?
Contributor guide
Assessment
This issue has not been assessed yet.