Snapshooter

I’ve been working on a very large-scale canvas application for a while now, and I finally checked in the first working prototype today.   The biggest challenge for this application is that the elements on the canvas are very rich.  They aren’t just simple boxes and lines.  Some of the elements actually contain full blown FlowDocuments that can be thousands of lines long!

One of the the biggest problems I’ve been facing has been around scalability.  WPF can show one or two RichTextBoxes without a problem, but what about a canvas full of thousands of them?  I can put thousands of Rectangles on the canvas without too much slowdown, but I can only put about 10 RichTextBoxes before WPF crawls to a halt.  This is because a single RichTextBox can contain hundreds (if not thousands) of child elements in its visual tree.

To get around this issue, I created the Snapshooter class.  The Snapshooter is a Decorator that takes a snapshot of its Child and displays it as a single static image instead of displaying the whole live visual tree.  Freezing a control in time is interesting, but it’s actually the side effect that’s relevant to my interests, since displaying a single static image is much much faster than displaying a whole live visual tree!

To use the Snapshooter, you simply wrap it around your element and set the IsSnapshot property to true:

 <Snapshooter IsSnapshot="True">
    <MyComplexVisualTree>
        . . .
    </MyComplexVisualTree>
</Snapshooter>

To take the snapshot, I use RenderTargetBitmap to render the visual tree into a bitmap, and then I use PngBitmapEncoder to compress it.

 public void TakeSnapshot()
{
    var child = Child;
    if (child != null)
    {
        var width = child.RenderSize.Width;
        var height = child.RenderSize.Height;
        if (width > 0 && height > 0)
        {
            var source = new RenderTargetBitmap(
                             (int)Math.Ceiling(child.RenderSize.Width),
                             (int)Math.Ceiling(child.RenderSize.Height),
                             0, 0, PixelFormats.Default);
            source.Render(child);

            var encoder = new PngBitmapEncoder();
            encoder.Frames.Add(BitmapFrame.Create(source));

            var stream = new MemoryStream();
            encoder.Save(stream);
            stream.Seek(0, SeekOrigin.Begin);

            var snapshot = BitmapFrame.Create(stream);
            snapshot.Freeze();
            Snapshot = snapshot;
        }
    }
}

Then, I set the Opacity of the real visual tree to 0.0 and display the image in OnRender instead:

 protected override void OnRender(DrawingContext drawingContext)
{
    base.OnRender(drawingContext);

    if (IsSnapshot && Snapshot != null)
    {
        drawingContext.DrawImage(Snapshot,
                       new Rect(0, 0, Snapshot.Width, Snapshot.Height));
    }
}

Setting the Opacity to 0.0 greatly speeds up rendering when doing things like applying Transforms to the canvas, and it still allows hit-testing and IsMouseOver events to work properly.  The only thing that won’t work when IsSnapshot = true is any visual updates to the real visual tree.  If the visual tree changes size then a new snapshot will be taken automatically, but if the visual tree doesn’t change size (e.g. it just changes color) then the Snapshooter has no way of knowing it needs to take another snapshot.  In order to discard the old snapshot and take a new one, you need to call InvalidateSnapshot or TakeSnapshot on the Snapshooter.  TakeSnapshot will take a new snapshot immediately, and InvalidateSnapshot will call TakeSnapshot during the next application idle period.

Wiring up code-behind event handlers to call InvalidateSnapshot every time something changes in your visual tree is a real nuisance, and it would prevent you from ever writing a pure-XAML solution if you need Snapshooter to update spontaneously.  To help with these issues, Snapshooter has an attached property called AffectsSnapshot that will do this for you.  You can set AffectsSnapshot anywhere in the visual tree, and you can set the value to anything (including a {Binding}) that you know will affect what the user sees.  The AffectsSnapshot property can be set to any object but it isn’t actually used by anything; the only effect it has is that whenever that value changes, InvalidateSnapshot will be called automatically. 

So for example, if somewhere deep in your visual tree you have a <Border> that changes color spontaneously, and a <Slider> that the user can manipulate, then you can bind both of these things to Snapshooter.AffectsSnapshot simultaneously:

<Snapshooter IsSnapshot="True">

<MyDeepVisualTree>

<Border Snapshooter.AffectsSnapshot="{Binding Background,

RelativeSource={RelativeSource Self}}">

<Slider Snapshooter.AffectsSnapshot="{Binding Value,

RelativeSource={RelativeSource Self}}"/>

</Border>

</MyDeepVisualTree>

</Snapshooter>

Now whenever the value of Background on the <Border> changes or the Value of the <Slider> changes, the Snapshooter will call InvalidateSnapshot automatically.  Note that the {Binding}s don’t always have to be to {RelativeSource Self} . They can be to anything, including the DataContext like normal.  Also, in that example I’m able to set AffectsSnapshot in two places since I have two separate Visuals to set it on, but you can still bind it to multiple properties even on a single element by using a <MultiBinding>.

I’ve attached the full Snapshooter class to this post.  Enjoy!

Snapshooter.cs