Josh Smith on WPF

Theoretical and practical information pertaining to Windows Presentation Foundation (WPF) by Josh Smith.

Subscriptions

<January 2007>
SuMoTuWeThFrSa
31123456
78910111213
14151617181920
21222324252627
28293031123
45678910

Navigation

Post Categories

Other Things to Read

UI-Driven Resource Unloading via the Service Provider Model

This blog entry examines how to simplify resource management by using a class that exposes its functionality as an attached property, which can be used directly in XAML.

I recently answered a question on the WPF forum where someone was concerned about the memory consumption associated with having a ListView displaying thousands of images.  The images were displayed in a VirtualizingStackPanel (whose IsVirtualizing property was set to true) and they were loaded on-demand.  The question posed was something to the effect of “How can I release the BitmapImage objects associated with Image elements that have been discarded by the VirtualizingStackPanel?”  Keep in mind, when a visual object is scrolled out of view in a VirtualizingStackPanel, it will throw away the “out of view” element to improve performance.

My answer was to hook the Unloaded event of the Images in the ListView and, in the event handling method, throw away the BitmapImage displayed by the unloaded Image element.  When a FrameworkElement (such as Image) is removed from an element tree, its Unloaded event will be raised.  Accordingly, when the VirtualizingStackPanel discards the elements it contains, their Unloaded event will fire.  This provides a perfect opportunity to do any necessary clean-up, such as releasing references to hefty BitmapImages.

Since answering that question my mind has stubbornly refused to stop thinking about the situation and possible ways to create a general solution.  The way I see it, the work done by the VirtualizingStackPanel is only half the battle; it throws away unnecessary visual objects.  It is up to the application developer to release the unnecessary domain objects.  Once the domain objects (such as BitmapImages) are released properly, then we can see some powerful advantages to using a virtualized presentation mechanism.  The rest of this blog entry demonstrates a simple class you can use to simplify the job of releasing the domain objects that correspond to unloaded visual objects.

I created a static class called UnloadedManager.  It exposes one Boolean attached property called IsManaged.  Any FrameworkElement can have the IsManaged property set to true for it, which means that when the element’s Unloaded event is raised, the UnloadedManager will respond.  In response to an element’s Unloaded event being raised, the UnloadedManager attempts to “unload” the element’s DataContext.  What does it mean to “unload” an element’s DataContext?  That’s up to you.  Let me explain…

Since the UnloadedManager does not know or care what domain object is associated with an unloaded FrameworkElement, it does not know how to “unload” it.  In order to make this work and be reusable, I also introduced a very simple interface called IUnloadable.  Here’s that interface, in all its glory:

public interface IUnloadable
{
 void Unload();
}

When a FrameworkElement which is managed by the UnloadedManager raises its Unloaded event, the UnloadedManager checks to see if the element’s DataContext implements IUnloadable.  If it does, that object’s Unload method is called.  The domain object’s Unload method is responsible for, say, releasing a reference to a BitmapImage, or some other memory hog.

Initially I was going to use the IDisposable interface as the means of unloading domain objects.  After thinking about it more I decided that might not be a good idea.  Many classes which implement IDisposable expect to never be used again after the Dispose method is called.  It is entirely possible that the domain objects that are unloaded by the UnloadedManager will be used again, such as when an object is scrolled back into view in a ListView.  I think it is better to avoid diverging from the standard IDisposable semantics, so I created IUnloadable instead.

Here is the UnloadedManager class:

///
/// A service provider class which provides a means of releasing resources
/// when a FrameworkElement's Unloaded event fires.  If the DataContext of
/// the element implements IUnloadable, it's Unload method will be invoked
/// when the elements Unloaded event fires.
///
public static class UnloadedManager
{
 public static readonly DependencyProperty IsManagedProperty =
  DependencyProperty.RegisterAttached(
   "IsManaged",
   typeof( bool ),
   typeof( UnloadedManager ),
   new UIPropertyMetadata( false, OnIsManagedChanged ) );

 public static bool GetIsManaged( FrameworkElement obj )
 {
  return (bool)obj.GetValue( IsManagedProperty );
 }

 public static void SetIsManaged( FrameworkElement obj, bool value )
 {
  obj.SetValue( IsManagedProperty, value );
 }

 // Invoked when the IsManaged attached property is set for a FrameworkElement.
 static void OnIsManagedChanged( DependencyObject depObj, DependencyPropertyChangedEventArgs e )
 {
  FrameworkElement elem = depObj as FrameworkElement;
  if( elem == null )
   return;

  bool isManaged = (bool)e.NewValue;
  if( isManaged )   
   elem.Unloaded += elem_Unloaded;   
  else
   elem.Unloaded -= elem_Unloaded;
 }

 static void elem_Unloaded( object sender, RoutedEventArgs e )
 {
  // Call Unload() on the element's DataContext.

  FrameworkElement elem = sender as FrameworkElement;
  if( elem == null )
   return;

  IUnloadable unloadable = elem.DataContext as IUnloadable;
  if( unloadable == null )
   return;

  unloadable.Unload();

  // Set IsManaged to false for the element so that the Unloaded
  // event handler is detached, which ensures that the object is not
  // referenced any longer.  That will allow the GC to collect it.

  SetIsManaged( elem, false );
 }
}

The Unloaded event handling method (elem_Unloaded) is where the IUnloadable interface is used to tell a domain object to clean itself up.  At the end of the method I set the IsManaged attached property to false for the unloaded element.  Doing so removes the delegate attached to its Unloaded event, which removes all references to that element (a delegate contains a reference to the invocation target, for instance methods).  That ensures that the Garbage Collector can collect the unloaded element.

Next we’ll examine a simple domain class which implements IUnloadable.  This class wraps a BitmapImage, which is lazily loaded.

///
/// Lazily loads a BitmapImage, based on the FileName property.
///
public class ImageInfo : IUnloadable
{
 private string fileName;
 private BitmapImage image;

 public ImageInfo( string fileName )
 {
  this.fileName = fileName;
 }

 public string FileName
 {
  get { return this.fileName; }   
 }

 public BitmapImage Image
 {
  get
  {
   if( this.image == null )
   {
    this.image = new BitmapImage( new Uri( this.FileName, UriKind.RelativeOrAbsolute ) );
   }

   return this.image;
  }
 }

 void IUnloadable.Unload()
 {
  this.image = null;
 }
}

The last method in that class is where the magic happens.  For an ImageInfo object, to “unload” means to release its reference to a BitmapImage.  If you had more sophisticated requirements for unloading images, you could simply code that logic into the Unload method of ImageInfo and it would be nicely encapsulated.  For example, if some of the images were downloaded off a network drive, you might decide to not release those BitmapImages because reloading them could take a long time.

Finally, let’s take a look at the Window of the demo application which uses these classes:

public partial class Window1 : Window
{
 public Window1()
 {
  InitializeComponent();
  this.Loaded += Window1_Loaded;
 }

 void Window1_Loaded( object sender, RoutedEventArgs e )
 {
  // Populate the ListBox with some ImageInfo objects.

  List<ImageInfo> imageInfos = new List<ImageInfo>();
  
  foreach( string fileName in Directory.GetFiles( @"..\..\Images", "*.jpg" ) )
   imageInfos.Add( new ImageInfo( fileName ) );

  listBox.ItemsSource = imageInfos;
 }
}

<Window x:Class="UnloadingImages.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:UnloadingImages"
    Title="UnloadingImages" Height="300" Width="300"
    >
  <Window.Resources>
    <DataTemplate DataType="{x:Type local:ImageInfo}">
      <Image
        Source="{Binding Image}"
        local:UnloadedManager.IsManaged="True"
        Width="100" Height="120" />
    </DataTemplate>
  </Window.Resources>
  <Grid>
    <ListBox Name="listBox" />
  </Grid>
</Window>

As you can see from the code above, the Window's code-behind has no logic to unload the BitmapImages that are scrolled out of view.  All of the resource unloading is handled by the UnloadedManager in conjunction with the domain objects (i.e. ImageInfo instances).  The benefit of having that logic abstracted away from the Window would become even more apparent if the ImageInfo class was reused in many Windows.

This code was compiled and tested against the RC1 of the .NET Framework 3.0.

posted on October 28, 2006 6:57 AM by Josh

Powered by Community Server, by Telligent Systems