PETZOLD BOOK BLOG

Charles Petzold on writing books, reading books, and exercising the internal UTM


Recent Entries
< PreviousBrowse the ArchivesNext >
Subscribe to the RSS Feed

WPF Retained Graphics and the SubPropertiesDoNotAffectRender Flag

November 3, 2008
New York, N.Y.

How well do you understand the retained-graphics system incorporated into the Windows Presentation Foundatation? Try to anticipate the visual appearance of the following FrameworkElement derivative:

A week ago Sunday (the day before the beginning of the Microsoft Professional Developers Conference in Los Angeles) I did a six-hour "pre-con" tutorial on WPF programming. This RainbowAttempt class is similar to one of the demo programs I wrote in preparation for that presentation, and I was surprised to see not a rainbow at all, but solid violet.

To see for yourself, download RetainedGraphicsDemo.zip containing a Solution for Visual Studio 2008. Within this Solution, the RetainedGraphicsLib project creates a DLL containing the various FrameworkElement derivatives I'll be showing here, including RainbowAttempt. All other projects are demo programs that reference that DLL. The TryRainbowAttempt project simply creates a window whose content is set to a RainbowAttempt object.

If the OnRender method defined by UIElement simply transferred pixels to a display surface — much like traditional WM_PAINT processing or the WinForms OnPaint method — the RainbowAttempt class would indeed draw a rainbow.

However, the WPF has a retained graphics system. What is seemingly "drawn" in the OnRender method does not go directly to the screen. It is instead retained and combined with other visual objects into a coherent whole. Retained graphics serves to facilitate several crucial features of WPF graphics, particularly transparency and animation.

As the RainbowAttempt class demonstrates, the retained graphics system in WPF does not even retain some kind of visual rendering that results from the various DrawingContext calls. It is retaining the actual visual objects including the brushes and pens passed through the DrawingContext methods. When RainbowAttempt tries to use the same SolidColorBrush object for all seven rectangles, the brush is actually shared among the rendered rectangles. In effect, the brush remains "alive" within the composition system, and any changes to that brush are reflected in the visuals.

There are several ways that RainbowAttempt can be "fixed." The creation of the SolidColorBrush object can be moved inside the foreach loop. Or, the array of Color objects can be replaced with an array of Brush objects obtained from the Brushes class. But let's instead apply what we've discovered here to other FrameworkElement derivatives.

Here's a simple ellipse class. It has only one dependency property named Fill of type Brush. As is typical with such properties, the AffectsRender flag indicates that changes to the Fill property affect the visuals of the element, and should trigger a call to OnRender:

Setting the AffectsRender flag is equivalent to defining a property-changed handler for the dependency property and calling the InvalidateVisual method in that handler.

Notice I've put a Console.WriteLine call in the OnRender method. This allows us to easily see just how often OnRender is called.

The AnimateSimpleEllipse1 project contains the following XAML file. The file creates an instance of SimpleEllipse1, assigns a SolidColorBrush to its Fill property, and animates the Color property of that SolidColorBrush:

The AnimateSimpleEllipse1 project has its output type set to Console Application rather than Windows Application; the console window that's created shows numerous calls to the OnRender method in SimpleBrush1 as the brush is being animated.

Are all these OnRender calls really necessary? This program contains only one object of type SolidColorBrush. This is the brush that is assigned to the Fill property of SimpleEllipse1 when the element is created. This SolidColorBrush object never itself changes. Only a property of this object changes.

Normally, if you define a dependency property with the AffectsRender flag, OnRender is called only when the dependency property itself changes, and not when any sub-properties change. But Brush derives from Freezable, and Freezable changes the rules regarding dependency properties. Any change to any sub-property (or sub-sub-property, etc.) of type Freezable will register the same as if the dependency property itself changed.

The problem with this class is that OnRender is called unnecessarily. When a single SolidColorBrush is assigned to the Fill property, it should be possible to animate a property of that brush without getting OnRender involved.

To demonstrate this, let's write a new class named SimpleEllipse2 whose Fill property is of type Color:

This class retains a private field of type SolidColorBrush and uses that object in the OnRender override. When the Fill property changes, the property-changed handler simply sets the Color property of that brush to the new color value.

Here's a program that animates the Fill property of SimpleEllipse2:

If you run this program you'll see that OnRender is called only once, yet the animation still works. In effect, the animation reaches through the SimpleEllipse2 class into the WPF composition system to animate a property of the brush. (Of course, we haven't eliminated all subsequent calls to OnRender. The method will be called when the size of the ellipse changes, for example, by changing the size of the window.)

The only problem with SimpleEllipse2 is that we've restricted outselves to solid-color brushes. We've lost the ability to use gradient brushes or tiled brushes. Is there a way to get the best of both worlds? To provide a property of type Brush yet to step out of the way when a property of that brush changes?

There are at least two ways to do it. Here's one. This SimpleEllipse3 class restores the Fill property to type Brush. Rather than specifying the AffectsRender flag, this class defines a property-changed handler. The handler calls InvalidateVisual only if the Fill property itself changes — not if any sub-property of the brush changes.

As you can note by running the AnimateSimpleEllipse3 program, the brush is successfully animated with no additional calls to the OnRender overide.

Another approach is to make use of another member of the FrameworkMetadataOptionsFlag enumeration named SubPropertiesDoNotAffectRender. This flag is specifically for the problem we've encountered — when you want the visual to be invalidated when the dependency property changes, but not when a sub-property changes. The SimpleEllipse4 class uses that flag:

The AnimateSimpleEllipse4 program (which unlike the earlier programs animates one of the GradientStop objects of a LinearGradientBrush) demonstrates that the animation occurs without any successive calls to the OnRender method.

In case you were wondering, the Shape class seems to use the SubPropertiesDoNotAffectRender flag for its own Fill property.


Comments:

Awesome post, Charles. Your insight and dogged determination to get to the bottom of any topic is both appreciated and the reason I LOVE to read your work - especially when your attention is focused on WPF.

Paul Jackson, Wed, 5 Nov 2008 03:42:22 -0500 (EST)

Great post!

Patrick Klug, Wed, 5 Nov 2008 18:22:15 -0500 (EST)

Nothing special to me. Things highlighted in topic are pretty obvious. Hope it really helped someone.

Paul, Thu, 22 Jan 2009 11:29:15 -0500 (EST)

That last post was quite insulting. I thought this was one of the best written posts I've ever seen about wpf.

— Fred, Thu, 5 Mar 2009 14:54:07 -0500 (EST)

This is a great post and it really helped me a lot, however I could not understand one thing: obviously ColorAnimation does not call InvalidateVisual, so how does it manage to refresh the view? Assume that I want to change color on a button click or MouseMove event, what should I do to achieve the same effect (i.e. view is rendered but OnRender is not called) without using storyboards or animations?

Ugur, Sun, 10 May 2009 09:49:39 -0400 (EDT)


Recent Entries
< PreviousBrowse the ArchivesNext >
Subscribe to the RSS Feed

(c) Copyright Charles Petzold
www.charlespetzold.com