How to ensure that image files are not locked by a Store App

Nearly two years ago, I explained how to force WPF to cache images while loading, in order to not lock the underlying image file while being displayed. Now I’ve stumbled across a similar problem in Windows Store Apps: When populating an Image control by data-binding a (local) URI:

public Uri Path { get; set; } = @"ms-appdata:///local/some_folder/my_file.png";
<Image Source="{Binding Path}" />

…as soon as the Image control is displayed, the source file is locked and can’t be replaced or deleted – not even its name can be changed. Imagine a page that displays an image and allows the user to delete it on a button tap – if simply binding the image file’s URI to the Image control, the user will just face a UnauthorizedAccessException because the file is in use and can’t be deleted.

Last time, the solution was rather simple thanks to WPF’s BitmapCacheOption.OnLoad option, and the challenge was mainly to wrap this in a nice converter to not break data-binding syntax. In WinRT and UWP, it’s more difficult than that: The BitmapImage class that we typically use as ImageSource does not provide a CacheOption property, nor does the BitmapCacheOption enumeration exist on the WinRT / UWP platforms!

The only option that sounds similar is the Image control’s CacheMode attribute. Unfortunately, this is not what we’re looking for as it is not unique to the Image control (but can be applied to all visual components), and it controls whether parts of the UI should be cached to the GPU and has nothing to do with file access.

Our best option is to “manually” load image data from the file, and assign it as content of a BitmapImage. The simple approach is to do so in the page’s (which contains the image) Loaded event handler, but we’re looking for a more elegant solution that allows for direct data-binding of a URI. Instead of defining a value converter, this time we’ll go for an attached property.

This property is declared within a separate class, and consists (in its simplest form) of a registration line and setter and getter methods. When using ReSharper, the code skeleton can be generated automatically with help of the attachedProperty snippet; but even without ReSharper the propdp snippet that is built into Visual Studio can be used to generate a conventional dependency property, in this case we just need to replace the DependencyProperty.Register call with DependencyProperty.RegisterAttached. Our attached property will be of type Uri, and we’ll call it CachedSource with reference to the Image control’s original Source property:

public class ImageHelper : DependencyObject
{
	public static readonly DependencyProperty CachedSourceProperty = DependencyProperty.RegisterAttached(
		"CachedSource", typeof(Uri), typeof(ImageHelper), new PropertyMetadata(default(Uri)));

	public static void SetCachedSource(DependencyObject element, Uri value)
	{
		element.SetValue(CachedSourceProperty, value);
	}

	public static Uri GetCachedSource(DependencyObject element)
	{
		return (Uri) element.GetValue(CachedSourceProperty);
	}
}

This base property can be attached to any XAML control – to limit it to be used only on the Image control, we simply declare the desired target control type on the getter and setter method’s signature:

public static void SetCachedSource(Image element, Uri value)
{
	element.SetValue(CachedSourceProperty, value);
}

public static Uri GetCachedSource(Image element)
{
	return (Uri)element.GetValue(CachedSourceProperty);
}

The main business logic (reading an image file and loading its content into memory) should be executed when the CachedSource property’s value is set for the first time, and whenever it is changed (either through data binding, or by explicitly setting it in code). To be able to execute code on each change of the property’s value, we register a callback method at the end of the RegisterAttached call:

public static readonly DependencyProperty CachedSourceProperty = DependencyProperty.RegisterAttached(
	"CachedSource", typeof(Uri), typeof(ImageHelper), new PropertyMetadata(default(Uri), CachedSourceChanged));

private static async void CachedSourceChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
{
	// This method is invoked each time the CachedSource property's value is about to change
}

The actual control which shall display the image is passed as the first parameter of the CachedSourceChanged method, we can safely cast it since we specified that our CachedSource property may only be attached to Image controls:

private static async void CachedSourceChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
{
	var imageControl = sender as Image;
}

In addition, we’ll obviously need the URI value that is set as the attached property’s new value – this is passed within the DependencyPropertyChangedEventArgs. Due to the generic signature, it is passed as object, so we need to cast it as well to meet our property’s target type:

private static async void CachedSourceChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
{
	var imageControl = sender as Image;
	var uri = args.NewValue as Uri;
}

The final part: Create a new instance of type BitmapImage, open the file specified through its URI and read its content in a stream, apply the content to the BitmapImage, and set this as the Image control’s image source (note that I’ve added null checks after retrieving the control and its new value, just to be absolutely sure that no unexpected errors can occur):

private static async void CachedSourceChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
{
	var imageControl = sender as Image;
	var uri = args.NewValue as Uri;
	if (imageControl == null || uri == null)
		return;

	var img = new BitmapImage();
	StorageFile file = await StorageFile.GetFileFromApplicationUriAsync(uri);
	using (IRandomAccessStreamWithContentType stream = await file.OpenReadAsync())
	{
		await img.SetSourceAsync(stream);
	}
	imageControl.Source = img;
}

Now we can attach this property to any Image control and bind URIs directly within XAML code:

<Image helpers:ImageHelper.CachedSource="{Binding Path}" />