ThumbnailImage - A WPF Custom Control

ThumbnailImage is a custom WPF control I've created to use in Comicster. It saves a local, resized version of an image given by a URI and uses the local copy instead of the online one.

Here's how you might use it:

<c:ThumbnailImage CacheId="{Binding Id}" UriSource="{Binding ImageSource}" />

Those two properties are the only ones that need to be set. CacheId, a string, represents the identity of the image and is used as a filename when it's saved locally. UriSource, as the name implies, is a URI that points to the image online.

A ThumbnailImage instance

In this post I'll outline some of the features of the ThumbnailImage control.

Cache Folder

ThumbnailImage saves its thumbnails in the Temporary Internet Files folder by default, but you can change that by specifying a folder in the CacheFolder property:

<c:ThumbnailImage CacheId="{Binding Id}" 
                  UriSource="{Binding ImageSource}" 
                  CacheFolder="d:\" />

Obviously setting a hard-coded value like this isn't a great idea, and you might want to specify a global folder for all instances of ThumbnailImage. That's easily accomplished by overriding the metadata for that property. You could do that in your App.xaml.cs file like this:

ThumbnailImage.CacheFolderProperty.OverrideMetadata(
    typeof(ThumbnailImage),
    new PropertyMetadata(@"d:\"));

Since you've just set "d:\" as the default value for the CacheFolder property, all ThumbnailImage instances will save their thumbnails to that directory.

Fallback Images

By default, ThumbnailImage obviously doesn't have any images cached. It won't download them automatically, so when it's first displayed it'll fall back to the image specified in the FallbackSource property. You might, for example, want to fall back to a local resource in your application:

<c:ThumbnailImage CacheId="{Binding Id}" 
                  UriSource="{Binding ImageSource}" 
                  FallbackSource="pack://application:,,,/Images/none.jpg" />

Note the use of the pack URI syntax there, since FallbackSource is also defined as a URI and will be downloaded and resized just like other images.

Just like the CacheFolder property, you can override the metadata for the FallbackSource property to give every ThumbnailImage instance the same fallback URI:

ThumbnailImage.FallbackSourceProperty.OverrideMetadata(typeof(ThumbnailImage),
    new PropertyMetadata(new Uri("pack://application:,,,/Images/none.png")));

Default Thumbnail Size

Thumbnails are generated using a default width of 120 pixels, but you can override that using the ThumbnailWidth property:

<c:ThumbnailImage CacheId="{Binding Id}" 
                  UriSource="{Binding ImageSource}" 
                  ThumbnailWidth="200" />

Unlike the aforementioned properties, ThumbnailWidth has a default value and can't be overwritten at the DependencyProperty level.

Forcing a Reload

To force the ThumbnailImage control to create its local cached image, you can right-click on it at runtime and select the "Reload" menu item. You can achieve the same thing programmatically by calling the Reload method:

thumbnailImage1.Reload();

If the download fails, the image will display a red border and the error returned will be available to the user as a ToolTip. To get to the error from code, check out the DownloadError property:

public Exception DownloadError { get { ... } }

Clearing the Cache

To reset the control, which will delete the cached thumbnail file and return to the fallback image, right-click on it and select the "Reset" menu item. Predictably, you can do this in code by calling the Reset method:

thumbnailImage1.Reset();

Manipulating the Cache Without an Instance

The last use-case for ThumbnailImage is when you want to generate a cached thumbnail without having an actual instance of the control. To do this in a lightweight way, call the static Reload method:

string cacheId = ...;
Uri uriSource = ...;
ThumbnailImage.Reload(cacheId, uriSource);

The IsReloading Property

The ThumbnailImage control has a read-only property called IsReloading which you can use to see if the image specified in UriSource is currently being downloaded. This is handy if you'd like the control to display some sort of feedback to the user while it's reloading. You can do this in a global style, perhaps in your App.xaml file.

The following style causes a ThumbnailImage to "flash" while it's reloading:

<Style TargetType="{x:Type c:ThumbnailImage}">
    <Style.Triggers>
        <DataTrigger Binding="{Binding IsReloading,RelativeSource={RelativeSource Self}}"
                     Value="True">
            <DataTrigger.EnterActions>
                <BeginStoryboard>
                    <Storyboard>
                        <DoubleAnimation Storyboard.TargetProperty="Opacity"
                                         To="0.5"
                                         Duration="0:0:0.5" 
                                         RepeatBehavior="Forever"
                                         AutoReverse="True"/>
                    </Storyboard>
                </BeginStoryboard>
            </DataTrigger.EnterActions>

            <DataTrigger.ExitActions>
                <BeginStoryboard>
                    <Storyboard FillBehavior="Stop">
                        <DoubleAnimation Storyboard.TargetProperty="Opacity"
                                         To="1" 
                                         Duration="0:0:0.2" />
                    </Storyboard>
                </BeginStoryboard>
            </DataTrigger.ExitActions>
        </DataTrigger>
    </Style.Triggers>
</Style>

Other Tips

The ThumbnailImage class keeps a private static ConcurrentDictionary that it uses to track other instances of the control with the same CacheId. That way if you start one instance reloading, other instances will be notified and set their IsReloading property accordingly. When one instance updates, all of them will update.

Getting the Control

The ThumbnailImage control's source code is part of Comicster right now, but you can check it out in Comicster's BitBucket repository. It's in the "Comicster.Controls" project.

If you can improve the control, let me know by commenting here or sending me a pull request!

.net wpf controls thumbnail image comicster
Posted by: Matt Hamilton
Last revised: 28 Jul, 2017 06:55 PM History