How do I cache images on the client for a WPF application?
We are developing a WPF desktop application that is displaying image开发者_开发百科s that are currently being fetched over HTTP.
The images are already optimised for quality/size but there is an obvious wait each time that the image is fetched.
Is there a way to cache images on the client so that they aren't downloaded each time?
I know this question is very old, but I had to use caching recently in a WPF application and found that there is a much better option in .Net 3.5 with BitmapImage by setting UriCachePolicy, that will use system-level caching:
<Image.Source>
<BitmapImage UriCachePolicy="Revalidate"
UriSource="https://farm3.staticflickr.com/2345/2077570455_03891081db.jpg"/>
</Image.Source>
You can even set the value in the app.config to make all your app use a default value for caching:
<system.net>
<requestCaching defaultPolicyLevel="CacheIfAvailable"/>
</system.net>
You will find an explanation of the RequestCacheLevel values here: http://msdn.microsoft.com/en-us/library/system.net.cache.requestcachelevel(v=vs.110).aspx
This functionality understands HTTP/1.1 headers, so if you set Revalidate it uses If-Modified-Since header to avoid downloading it each time, but still checking if the image has been changed so you always have the correct one.
For people coming here via Google, I have packaged the original implementation that Simon Hartcher posted, refactored by Jeroen van Langen (along with the tweaks from Ivan Leonenko to make it bindable), into an Open Source NuGet package.
Please find the details here - http://floydpink.github.io/CachedImage/
i've read your blog, and that brought me to this (i think much easier) concept setup:
As you will noticed, i reused some of your code you shared, so i'll share mine back.
Create a new custom control called CachedImage.
public class CachedImage : Image
{
private string _imageUrl;
static CachedImage()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(CachedImage), new FrameworkPropertyMetadata(typeof(CachedImage)));
}
public string ImageUrl
{
get
{
return _imageUrl;
}
set
{
if (value != _imageUrl)
{
Source = new BitmapImage(new Uri(FileCache.FromUrl(value)));
_imageUrl = value;
}
}
}
}
Next i've made a FileCache class (so i have control on all caching not only images)
public class FileCache
{
public static string AppCacheDirectory { get; set; }
static FileCache()
{
// default cache directory, can be changed in de app.xaml.
AppCacheDirectory = String.Format("{0}/Cache/", Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData));
}
public static string FromUrl(string url)
{
//Check to see if the directory in AppData has been created
if (!Directory.Exists(AppCacheDirectory))
{
//Create it
Directory.CreateDirectory(AppCacheDirectory);
}
//Cast the string into a Uri so we can access the image name without regex
var uri = new Uri(url);
var localFile = String.Format("{0}{1}", AppCacheDirectory, uri.Segments[uri.Segments.Length - 1]);
if (!File.Exists(localFile))
{
HttpHelper.GetAndSaveToFile(url, localFile);
}
//The full path of the image on the local computer
return localFile;
}
}
Also for downloading content I made a helper class:
public class HttpHelper
{
public static byte[] Get(string url)
{
WebRequest request = HttpWebRequest.Create(url);
WebResponse response = request.GetResponse();
return response.ReadToEnd();
}
public static void GetAndSaveToFile(string url, string filename)
{
using (FileStream stream = new FileStream(filename, FileMode.Create, FileAccess.Write))
{
byte[] data = Get(url);
stream.Write(data, 0, data.Length);
}
}
}
The HttpHelper uses an extension on the WebResponse class for reading the result to an array
public static class WebResponse_extension
{
public static byte[] ReadToEnd(this WebResponse webresponse)
{
Stream responseStream = webresponse.GetResponseStream();
using (MemoryStream memoryStream = new MemoryStream((int)webresponse.ContentLength))
{
responseStream.CopyTo(memoryStream);
return memoryStream.ToArray();
}
}
}
Now you got it complete, lets use it in xaml
<Grid>
<local:CachedImage ImageUrl="http://host/image.png" />
</Grid>
That's all, it's reusable and robust.
The only disadvance is, that the image is never downloaded again until you cleanup the cache directory.
The first time the image is downloaded from the web and saved in the cache directory. Eventually the image is loaded from the cache and assign to the source of the parent class (Image).
Kind regards, Jeroen van Langen.
I have solved this by creating a Binding Converter using the IValueConverter interface. Given that I tried to find a solid solution for this for at least a week, I figured I should share my solution for those with this problem in the future.
Here is my blog post: Image Caching for a WPF Desktop Application
If you're just trying to cache within the same run, then a local dictionary could function as a runtime cache.
If you're trying to cache between application runs, it gets trickier.
If this is a desktop application, just save the cached images locally in the user's application data folder.
If it's an XBAP application (WPF in Browser), you'll only be able to setup a local cache in the user's Isolated Storage, due to security.
Based on this I made custom control which:
- can download images asynchronously and get them from cache if image
- is thread safe
- was downloaded has dependency property to which you can bind to
- update images, providing new names in initial feed (don’t forget to maintain cache clean operation, e.g. you can parse your feed and asynchronously delete images with no links in feed)
I made a blog post:, and here's the code:
public class CachedImage : Image
{
static CachedImage()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(CachedImage), new FrameworkPropertyMetadata(typeof(CachedImage)));
}
public readonly static DependencyProperty ImageUrlProperty = DependencyProperty.Register("ImageUrl", typeof(string), typeof(CachedImage), new PropertyMetadata("", ImageUrlPropertyChanged));
public string ImageUrl
{
get
{
return (string)GetValue(ImageUrlProperty);
}
set
{
SetValue(ImageUrlProperty, value);
}
}
private static readonly object SafeCopy = new object();
private static void ImageUrlPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
var url = (String)e.NewValue;
if (String.IsNullOrEmpty(url))
return;
var uri = new Uri(url);
var localFile = String.Format(Path.Combine(Globals.CacheFolder, uri.Segments[uri.Segments.Length - 1]));
var tempFile = String.Format(Path.Combine(Globals.CacheFolder, Guid.NewGuid().ToString()));
if (File.Exists(localFile))
{
SetSource((CachedImage)obj, localFile);
}
else
{
var webClient = new WebClient();
webClient.DownloadFileCompleted += (sender, args) =>
{
if (args.Error != null)
{
File.Delete(tempFile);
return;
}
if (File.Exists(localFile))
return;
lock (SafeCopy)
{
File.Move(tempFile, localFile);
}
SetSource((CachedImage)obj, localFile);
};
webClient.DownloadFileAsync(uri, tempFile);
}
}
private static void SetSource(Image inst, String path)
{
inst.Source = new BitmapImage(new Uri(path));
}
}
Now you can bind to it:
<Cache:CachedImage ImageUrl="{Binding Icon}"/>
Just a update from Jeroen van Langen reply,
You can save a bunch of line
remove HttpHelper
class and the WebResponse_extension
replace
HttpHelper.GetAndSaveToFile(url, localFile);
by
WebClient webClient = new WebClient();
webClient.DownloadFile(url, localFile);
This cachedImage works great, but..
Any advice on how I could use the same cached image type capability for ImageSource
for an ImageBrush?
<Rectangle
Width="32"
Height="32"
Margin="2,1"
RadiusX="16"
RadiusY="16"
RenderOptions.BitmapScalingMode="HighQuality">
<Rectangle.Fill>
<ImageBrush ImageSource="{Binding Image}" />
</Rectangle.Fill>
</Rectangle>
Note there may be a better way to do rounded images (for say profile images)
精彩评论