summaryrefslogtreecommitdiff
path: root/Juick
diff options
context:
space:
mode:
authorGravatar Vitaly Takmazov2013-03-29 14:58:12 +0400
committerGravatar Vitaly Takmazov2013-03-29 14:58:12 +0400
commit9f19cd09bfad13715bb4eda46e7782f56674e26c (patch)
treef2d7f8be17c0ab9a00056d372a364bb6938f2429 /Juick
parent32fc287a28c24ca980f4fe67ccab178c04cd9159 (diff)
using PersistentImageCache for avatars
Diffstat (limited to 'Juick')
-rw-r--r--Juick/Controls/MessageList.xaml3
-rw-r--r--Juick/Converters/UriToImageSourceConverter.cs31
-rw-r--r--Juick/Juick.csproj5
-rw-r--r--Juick/Storage/ImageCache.cs160
-rw-r--r--Juick/Storage/PersistentImageCache.cs880
-rw-r--r--Juick/Storage/SystemImageCache.cs50
-rw-r--r--Juick/Threading/OneShotDispatcherTimer.cs160
-rw-r--r--Juick/ViewModels/PostItem.cs14
-rw-r--r--Juick/ViewModels/ViewModelBase.cs2
9 files changed, 1297 insertions, 8 deletions
diff --git a/Juick/Controls/MessageList.xaml b/Juick/Controls/MessageList.xaml
index 7a5882f..b53b7b8 100644
--- a/Juick/Controls/MessageList.xaml
+++ b/Juick/Controls/MessageList.xaml
@@ -13,6 +13,7 @@
d:DesignHeight="480" d:DesignWidth="480">
<UserControl.Resources>
<converters:MidToUriConverter x:Key="uriConverter" />
+ <converters:UriToImageSourceConverter x:Key="imgCacheConverter" />
</UserControl.Resources>
<Grid x:Name="LayoutRoot">
@@ -37,7 +38,7 @@
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
- <Image bindings:LowProfileImageLoader.UriSource="{Binding AvatarUri}" Grid.Row="0" Grid.Column="0" Margin="3" />
+ <Image Source="{Binding AvatarUri, Converter={StaticResource imgCacheConverter}}" Grid.Row="0" Grid.Column="0" Margin="3" />
<TextBlock Text="{Binding Username}" Grid.Row="0" Grid.Column="1"
Margin="5,0,5,5" VerticalAlignment="Top"
HorizontalAlignment="Left"
diff --git a/Juick/Converters/UriToImageSourceConverter.cs b/Juick/Converters/UriToImageSourceConverter.cs
new file mode 100644
index 0000000..4c26254
--- /dev/null
+++ b/Juick/Converters/UriToImageSourceConverter.cs
@@ -0,0 +1,31 @@
+using System;
+using System.Net;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Documents;
+using System.Windows.Ink;
+using System.Windows.Input;
+using System.Windows.Media;
+using System.Windows.Media.Animation;
+using System.Windows.Shapes;
+using System.Windows.Data;
+using System.Globalization;
+using Kawagoe.Storage;
+
+namespace Juick.Converters
+{
+ public class UriToImageSourceConverter : IValueConverter
+ {
+ static ImageCache _cache = new PersistentImageCache("avatarsCache");
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ Uri avatarUri = (Uri)value;
+ return _cache.Get(avatarUri);
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ return null;
+ }
+ }
+}
diff --git a/Juick/Juick.csproj b/Juick/Juick.csproj
index 8e47876..3b467f4 100644
--- a/Juick/Juick.csproj
+++ b/Juick/Juick.csproj
@@ -88,6 +88,7 @@
<DependentUpon>MessageList.xaml</DependentUpon>
</Compile>
<Compile Include="Converters\MidToUriConverter.cs" />
+ <Compile Include="Converters\UriToImageSourceConverter.cs" />
<Compile Include="LoginView.xaml.cs">
<DependentUpon>LoginView.xaml</DependentUpon>
</Compile>
@@ -98,6 +99,10 @@
<DependentUpon>NewPostView.xaml</DependentUpon>
</Compile>
<Compile Include="Properties\AssemblyInfo.cs" />
+ <Compile Include="Storage\ImageCache.cs" />
+ <Compile Include="Storage\PersistentImageCache.cs" />
+ <Compile Include="Storage\SystemImageCache.cs" />
+ <Compile Include="Threading\OneShotDispatcherTimer.cs" />
<Compile Include="ThreadView.xaml.cs">
<DependentUpon>ThreadView.xaml</DependentUpon>
</Compile>
diff --git a/Juick/Storage/ImageCache.cs b/Juick/Storage/ImageCache.cs
new file mode 100644
index 0000000..e45dc62
--- /dev/null
+++ b/Juick/Storage/ImageCache.cs
@@ -0,0 +1,160 @@
+// Copyright 2010 Andreas Saudemont (andreas.saudemont@gmail.com)
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using System;
+using System.Windows;
+using System.Windows.Media;
+
+namespace Kawagoe.Storage
+{
+ /// <summary>
+ /// Defines the base clase for image cache implementations.
+ /// </summary>
+ public abstract class ImageCache
+ {
+ /// <summary>
+ /// The name of the default image cache.
+ /// </summary>
+ public const string DefaultImageCacheName = "default";
+
+ private static ImageCache _defaultImageCache = null;
+ private static object _defaultImageCacheLock = new object();
+
+ /// <summary>
+ /// The default image cache.
+ /// If not set explicitely, a <see cref="PersistentImageCache"/> instance is used by default.
+ /// </summary>
+ public static ImageCache Default
+ {
+ get
+ {
+ if (!Deployment.Current.Dispatcher.CheckAccess())
+ {
+ throw new UnauthorizedAccessException("invalid cross-thread access");
+ }
+ lock (_defaultImageCacheLock)
+ {
+ if (_defaultImageCache == null)
+ {
+ _defaultImageCache = new PersistentImageCache(DefaultImageCacheName);
+ }
+ return _defaultImageCache;
+ }
+ }
+ set
+ {
+ if (!Deployment.Current.Dispatcher.CheckAccess())
+ {
+ throw new UnauthorizedAccessException("invalid cross-thread access");
+ }
+ lock (_defaultImageCacheLock)
+ {
+ _defaultImageCache = value;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Initializes a new <see cref="ImageCache"/> instance.
+ /// </summary>
+ protected ImageCache(string name)
+ {
+ if (string.IsNullOrEmpty(name))
+ {
+ throw new ArgumentException();
+ }
+ Name = name;
+ }
+
+ /// <summary>
+ /// The name of the image cache.
+ /// </summary>
+ protected string Name
+ {
+ get;
+ private set;
+ }
+
+ /// <summary>
+ /// Retrieves the source for the image with the specified URI from the cache, downloading it
+ /// if needed.
+ /// </summary>
+ /// <param name="imageUri">The URI of the image. Must be an absolute URI.</param>
+ /// <returns>An ImageSource object, or <c>null</c> if <paramref name="imageUri"/> is <c>null</c> or not an absolute URI.</returns>
+ /// <exception cref="UnauthorizedAccessException">The method is not called in the UI thread.</exception>
+ public ImageSource Get(Uri imageUri)
+ {
+ if (!Deployment.Current.Dispatcher.CheckAccess())
+ {
+ throw new UnauthorizedAccessException("invalid cross-thread access");
+ }
+ if (imageUri == null || !imageUri.IsAbsoluteUri)
+ {
+ return null;
+ }
+ return GetInternal(imageUri);
+ }
+
+ /// <summary>
+ /// Retrieves the source for the image with the specified URI from the cache, downloading it
+ /// if needed.
+ /// </summary>
+ /// <param name="imageUriString">The URI of the image. Must be an absolute URI.</param>
+ /// <returns>An ImageSource object, or <c>null</c> if <paramref name="imageUriString"/> is <c>null</c>,
+ /// the empty string, or not an absolute URI.</returns>
+ /// <exception cref="UnauthorizedAccessException">The method is not called in the UI thread.</exception>
+ public ImageSource Get(string imageUriString)
+ {
+ if (!Deployment.Current.Dispatcher.CheckAccess())
+ {
+ throw new UnauthorizedAccessException("invalid cross-thread access");
+ }
+ if (string.IsNullOrEmpty(imageUriString))
+ {
+ return null;
+ }
+ Uri imageUri;
+ try
+ {
+ imageUri = new Uri(imageUriString, UriKind.Absolute);
+ }
+ catch (Exception)
+ {
+ return null;
+ }
+ return Get(imageUri);
+ }
+
+ /// <summary>
+ /// The actual implementation of <see cref="ImageCache.Get"/>.
+ /// </summary>
+ protected abstract ImageSource GetInternal(Uri imageUri);
+
+ /// <summary>
+ /// Deletes all the images from the cache.
+ /// This method can block the current thread for a long time; it is advised to call it from
+ /// a background thread.
+ /// </summary>
+ public abstract void Clear();
+
+ /// <summary>
+ /// Overrides object.ToString().
+ /// </summary>
+ /// <returns></returns>
+ public override string ToString()
+ {
+ return string.Format("ImageCache:{0}", Name);
+ }
+ }
+}
diff --git a/Juick/Storage/PersistentImageCache.cs b/Juick/Storage/PersistentImageCache.cs
new file mode 100644
index 0000000..1472148
--- /dev/null
+++ b/Juick/Storage/PersistentImageCache.cs
@@ -0,0 +1,880 @@
+// Copyright 2010 Andreas Saudemont (andreas.saudemont@gmail.com)
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.IO.IsolatedStorage;
+using System.Net;
+using System.Security.Cryptography;
+using System.Text;
+using System.Threading;
+using System.Windows;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+using Kawagoe.Threading;
+
+namespace Kawagoe.Storage
+{
+ /// <summary>
+ /// Implements an <see cref="ImageCache"/> on top of the isolated storage.
+ /// </summary>
+ public class PersistentImageCache : ImageCache
+ {
+ /// <summary>
+ /// The default value of <see cref="PersistentImageCache.ExpirationDelay"/>.
+ /// </summary>
+ public static readonly TimeSpan DefaultExpirationDelay = TimeSpan.FromDays(1);
+
+ /// <summary>
+ /// The default value of <see cref="PersistentImageCache.MemoryCacheCapacity"/>.
+ /// </summary>
+ public const int DefaultMemoryCacheCapacity = 100;
+
+ private const string ImageDataExtension = "data";
+ private const string ImageTimestampExtension = "tstamp";
+
+ private TimeSpan _expirationDelay = DefaultExpirationDelay;
+
+ /// <summary>
+ /// Initializes a new <see cref="PersistentImageCache"/> instance with the specified name.
+ /// </summary>
+ public PersistentImageCache(string name)
+ : base(name)
+ {
+ }
+
+ /// <summary>
+ /// The delay after which an image, once downloaded, is consider expired and is deleted
+ /// from the cache.
+ /// </summary>
+ public TimeSpan ExpirationDelay
+ {
+ get
+ {
+ return _expirationDelay;
+ }
+ set
+ {
+ if (value.TotalMinutes < 1)
+ {
+ throw new ArgumentOutOfRangeException();
+ }
+ _expirationDelay = value;
+ RequestCachePruning();
+ }
+ }
+
+ /// <summary>
+ /// Implements <see cref="ImageCache.GetInternal"/>.
+ /// </summary>
+ protected override ImageSource GetInternal(Uri imageUri)
+ {
+ BitmapImage imageSource = new BitmapImage();
+
+ string imageKey = GetImageKey(imageUri);
+ Stream imageDataStream = LoadImageFromMemoryCache(imageKey);
+ if (imageDataStream != null)
+ {
+ imageSource.SetSource(imageDataStream);
+ return imageSource;
+ }
+
+ WeakReference imageSourceRef = new WeakReference(imageSource);
+ ThreadPool.QueueUserWorkItem((state) =>
+ {
+ LoadImageSource(imageUri, imageSourceRef);
+ });
+ return imageSource;
+ }
+
+ private void LoadImageSource(Uri imageUri, WeakReference imageSourceRef)
+ {
+ BitmapImage imageSource = imageSourceRef.Target as BitmapImage;
+ if (imageSource == null)
+ {
+ return;
+ }
+
+ string imageKey = GetImageKey(imageUri);
+ Stream imageDataStream = LoadImageFromMemoryCache(imageKey);
+ if (imageDataStream == null)
+ {
+ imageDataStream = ReadImageDataFromCache(imageKey);
+ }
+ if (imageDataStream != null)
+ {
+ Deployment.Current.Dispatcher.BeginInvoke(() =>
+ {
+ imageSource.SetSource(imageDataStream);
+ });
+ }
+ else
+ {
+ RequestImageDownload(imageUri, imageSourceRef);
+ }
+ }
+
+ /// <summary>
+ /// Implements <see cref="ImageCache.Clear"/>.
+ /// </summary>
+ public override void Clear()
+ {
+ lock (_storeLock)
+ lock (_memoryCacheLock)
+ {
+ ClearMemoryCache();
+ DeleteAllImagesFromStore();
+ }
+ }
+
+ #region Image Downloads
+
+ private readonly Dictionary<Uri, ImageRequest> _pendingRequests = new Dictionary<Uri, ImageRequest>();
+
+ private void RequestImageDownload(Uri imageUri, WeakReference imageSourceRef)
+ {
+ if (imageUri == null || imageSourceRef == null || imageSourceRef.Target == null)
+ {
+ return;
+ }
+
+ lock (_pendingRequests)
+ {
+ PrunePendingRequests();
+
+ if (_pendingRequests.ContainsKey(imageUri))
+ {
+ ImageRequest request = _pendingRequests[imageUri];
+ lock (request)
+ {
+ _pendingRequests[imageUri].SourceRefs.Add(imageSourceRef);
+ }
+ }
+ else
+ {
+ ImageRequest request = new ImageRequest(imageUri);
+ request.Completed += OnImageRequestCompleted;
+ request.SourceRefs.Add(imageSourceRef);
+ _pendingRequests[imageUri] = request;
+ try
+ {
+ request.Start();
+ }
+ catch (Exception)
+ {
+ _pendingRequests.Remove(imageUri);
+ }
+ }
+ }
+ }
+
+ private void OnImageRequestCompleted(object sender, EventArgs e)
+ {
+ ImageRequest request = sender as ImageRequest;
+ if (request == null)
+ {
+ return;
+ }
+
+ lock (_pendingRequests)
+ {
+ PrunePendingRequests();
+
+ if (!_pendingRequests.ContainsKey(request.ImageUri))
+ {
+ return;
+ }
+ _pendingRequests.Remove(request.ImageUri);
+
+ if (request.ImageData == null || request.ImageData.Length == 0)
+ {
+ return;
+ }
+
+ string imageKey = GetImageKey(request.ImageUri);
+ WriteImageToCache(imageKey, request.ImageData);
+ WriteImageToMemoryCache(imageKey, request.ImageData);
+
+ foreach (WeakReference sourceRef in request.SourceRefs)
+ {
+ BitmapSource imageSource = sourceRef.Target as BitmapSource;
+ if (imageSource != null)
+ {
+ Stream imageDataStream = new MemoryStream(request.ImageData);
+ Deployment.Current.Dispatcher.BeginInvoke(() =>
+ {
+ imageSource.SetSource(imageDataStream);
+ });
+ }
+ }
+ }
+ }
+
+ private void PrunePendingRequests()
+ {
+ lock (_pendingRequests)
+ {
+ List<Uri> obsoleteUris = null;
+
+ foreach (Uri imageUri in _pendingRequests.Keys)
+ {
+ ImageRequest request = _pendingRequests[imageUri];
+ bool hasSources = false;
+ foreach (WeakReference sourceRef in request.SourceRefs)
+ {
+ if (sourceRef.Target != null)
+ {
+ hasSources = true;
+ break;
+ }
+ }
+ if (!hasSources)
+ {
+ if (obsoleteUris == null)
+ {
+ obsoleteUris = new List<Uri>();
+ }
+ obsoleteUris.Add(imageUri);
+ }
+ }
+
+ if (obsoleteUris != null)
+ {
+ foreach (Uri obsoleteUri in obsoleteUris)
+ {
+ ImageRequest request = _pendingRequests[obsoleteUri];
+ _pendingRequests.Remove(obsoleteUri);
+ request.Cancel();
+ }
+ }
+ }
+ }
+
+ private class ImageRequest
+ {
+ private bool _started = false;
+ private HttpWebRequest _webRequest = null;
+ private Stream _responseInputStream = null;
+ private byte[] _responseBuffer = new byte[4096];
+ private MemoryStream _responseDataStream = new MemoryStream();
+
+ public ImageRequest(Uri imageUri)
+ {
+ ImageUri = imageUri;
+ ImageData = null;
+ SourceRefs = new List<WeakReference>();
+ }
+
+ public Uri ImageUri
+ {
+ get;
+ private set;
+ }
+
+ public byte[] ImageData
+ {
+ get;
+ private set;
+ }
+
+ public IList<WeakReference> SourceRefs
+ {
+ get;
+ private set;
+ }
+
+ public void Start()
+ {
+ lock (this)
+ {
+ if (_started)
+ {
+ return;
+ }
+ _started = true;
+
+ _webRequest = (HttpWebRequest)HttpWebRequest.Create(ImageUri);
+ _webRequest.BeginGetResponse(OnGotResponse, null);
+ }
+ }
+
+ public void Cancel()
+ {
+ lock (this)
+ {
+ if (!_started)
+ {
+ return;
+ }
+ HttpWebRequest webRequest = _webRequest;
+ ReleaseResources();
+ if (webRequest != null)
+ {
+ try
+ {
+ webRequest.Abort();
+ }
+ catch (Exception) { }
+ }
+ }
+ }
+
+ public event EventHandler Completed;
+
+ private void OnGotResponse(IAsyncResult asyncResult)
+ {
+ lock (this)
+ {
+ if (_webRequest == null)
+ {
+ return;
+ }
+ try
+ {
+ HttpWebResponse webResponse = (HttpWebResponse)_webRequest.EndGetResponse(asyncResult);
+ _responseInputStream = webResponse.GetResponseStream();
+ _responseInputStream.BeginRead(_responseBuffer, 0, _responseBuffer.Length, OnReadResponseCompleted, null);
+ }
+ catch (Exception)
+ {
+ NotifyCompletion();
+ }
+ }
+ }
+
+ private void OnReadResponseCompleted(IAsyncResult asyncResult)
+ {
+ lock (this)
+ {
+ if (_responseInputStream == null)
+ {
+ return;
+ }
+ try
+ {
+ int readCount = _responseInputStream.EndRead(asyncResult);
+ if (readCount > 0)
+ {
+ _responseDataStream.Write(_responseBuffer, 0, readCount);
+ _responseInputStream.BeginRead(_responseBuffer, 0, _responseBuffer.Length, OnReadResponseCompleted, null);
+ }
+ else
+ {
+ if (_responseDataStream.Length > 0)
+ {
+ ImageData = _responseDataStream.ToArray();
+ }
+ NotifyCompletion();
+ }
+ }
+ catch (Exception)
+ {
+ NotifyCompletion();
+ }
+ }
+ }
+
+ private void NotifyCompletion()
+ {
+ lock (this)
+ {
+ ReleaseResources();
+
+ ThreadPool.QueueUserWorkItem((state) =>
+ {
+ if (Completed == null)
+ {
+ return;
+ }
+ try
+ {
+ Completed(this, EventArgs.Empty);
+ }
+ catch (Exception) { }
+ });
+ }
+ }
+
+ private void ReleaseResources()
+ {
+ lock (this)
+ {
+ _responseBuffer = null;
+ _responseDataStream = null;
+ if (_responseInputStream != null)
+ {
+ try { _responseInputStream.Dispose(); }
+ catch (Exception) { }
+ _responseInputStream = null;
+ }
+ _webRequest = null;
+ }
+ }
+ }
+
+ #endregion
+
+ #region Store Access
+
+ private readonly object _storeLock = new object();
+ private IsolatedStorageFile _store = null;
+ private readonly SHA1 _hasher = new SHA1Managed();
+
+ /// <summary>
+ /// The name of directory in isolated storage that contains the files of this image cache.
+ /// </summary>
+ private string StoreDirectoryName
+ {
+ get
+ {
+ return "ImageCache_" + Name;
+ }
+ }
+
+ /// <summary>
+ /// The isolated storage file used by the cache.
+ /// </summary>
+ private IsolatedStorageFile Store
+ {
+ get
+ {
+ lock (_storeLock)
+ {
+ if (_store == null)
+ {
+ _store = IsolatedStorageFile.GetUserStoreForApplication();
+ if (!_store.DirectoryExists(StoreDirectoryName))
+ {
+ _store.CreateDirectory(StoreDirectoryName);
+ }
+ }
+ return _store;
+ }
+ }
+ }
+
+ private string GetImageKey(Uri imageUri)
+ {
+ byte[] imageUriBytes = Encoding.UTF8.GetBytes(imageUri.ToString());
+ byte[] hash;
+ lock (_hasher)
+ {
+ hash = _hasher.ComputeHash(imageUriBytes);
+ }
+ return BitConverter.ToString(hash).Replace("-", "");
+ }
+
+ private string GetImageFilePath(string imageKey)
+ {
+ return Path.Combine(StoreDirectoryName, imageKey) + "." + ImageDataExtension;
+ }
+
+ private string GetTimestampFilePath(string imageKey)
+ {
+ return Path.Combine(StoreDirectoryName, imageKey) + "." + ImageTimestampExtension;
+ }
+
+ private Stream ReadImageDataFromCache(string imageKey)
+ {
+ RequestCachePruning();
+
+ MemoryStream dataStream = null;
+ try
+ {
+ string imageFilePath = GetImageFilePath(imageKey);
+ lock (_storeLock)
+ {
+ if (!Store.FileExists(imageFilePath))
+ {
+ return null;
+ }
+ if (GetImageTimestamp(imageKey).Add(ExpirationDelay) < DateTime.UtcNow)
+ {
+ DeleteImageFromCache(imageKey);
+ return null;
+ }
+ using (IsolatedStorageFileStream fileStream = Store.OpenFile(imageFilePath, FileMode.Open, FileAccess.Read))
+ {
+ if (fileStream.Length > int.MaxValue)
+ {
+ return null;
+ }
+ dataStream = new MemoryStream((int)fileStream.Length);
+ byte[] buffer = new byte[4096];
+ while (dataStream.Length < fileStream.Length)
+ {
+ int readCount = fileStream.Read(buffer, 0, Math.Min(buffer.Length, (int)(fileStream.Length - dataStream.Length)));
+ if (readCount <= 0)
+ {
+ throw new NotSupportedException();
+ }
+ dataStream.Write(buffer, 0, readCount);
+ }
+ }
+ WriteImageToMemoryCache(imageKey, dataStream.ToArray());
+ return dataStream;
+ }
+ }
+ catch (Exception)
+ {
+ if (dataStream != null)
+ {
+ try { dataStream.Dispose(); }
+ catch (Exception) { }
+ }
+ }
+ return null;
+ }
+
+ private void WriteImageToCache(string imageKey, byte[] imageData)
+ {
+ RequestCachePruning();
+
+ string imageFilePath = GetImageFilePath(imageKey);
+ try
+ {
+ lock (_storeLock)
+ {
+ IsolatedStorageFileStream fileStream;
+ if (Store.FileExists(imageFilePath))
+ {
+ fileStream = Store.OpenFile(imageFilePath, FileMode.Create, FileAccess.Write);
+ }
+ else
+ {
+ fileStream = Store.OpenFile(imageFilePath, FileMode.CreateNew, FileAccess.Write);
+ }
+ using (fileStream)
+ {
+ fileStream.Seek(0, SeekOrigin.Begin);
+ while (fileStream.Position < imageData.Length)
+ {
+ fileStream.Write(imageData, (int)fileStream.Position, (int)(imageData.Length - fileStream.Position));
+ }
+ }
+ SetImageTimestamp(imageKey, DateTime.UtcNow);
+ }
+ }
+ catch (Exception)
+ {
+ try
+ {
+ Store.DeleteFile(imageFilePath);
+ }
+ catch (Exception) { }
+ }
+ }
+
+ private void PrunePersistentCache()
+ {
+ try
+ {
+ lock (_storeLock)
+ {
+ string searchPattern = Path.Combine(StoreDirectoryName, string.Format("*.{0}", ImageDataExtension));
+ string[] fileNames = Store.GetFileNames(searchPattern);
+ foreach (string fileName in fileNames)
+ {
+ if (!fileName.EndsWith("." + ImageDataExtension))
+ {
+ continue;
+ }
+ string imageKey = fileName.Remove(Math.Max(fileName.Length - ImageDataExtension.Length - 1, 0));
+ if (GetImageTimestamp(imageKey).Add(ExpirationDelay) < DateTime.UtcNow)
+ {
+ DeleteImageFromCache(imageKey);
+ }
+ }
+ }
+ }
+ catch (Exception) { }
+ }
+
+ private void DeleteImageFromCache(string imageKey)
+ {
+ string imageFilePath = GetImageFilePath(imageKey);
+ string timestampFilePath = GetTimestampFilePath(imageKey);
+ lock (_storeLock)
+ {
+ try
+ {
+ if (Store.FileExists(imageFilePath))
+ {
+ Store.DeleteFile(imageFilePath);
+ }
+ }
+ catch (Exception) { }
+ try
+ {
+ if (Store.FileExists(timestampFilePath))
+ {
+ Store.DeleteFile(timestampFilePath);
+ }
+ }
+ catch (Exception) { }
+ }
+ }
+
+ private void DeleteAllImagesFromStore()
+ {
+ lock (_storeLock)
+ {
+ string searchPattern = Path.Combine(StoreDirectoryName, "*.*");
+ try
+ {
+ string[] fileNames = Store.GetFileNames(searchPattern);
+ foreach (string fileName in fileNames)
+ {
+ string filePath = Path.Combine(StoreDirectoryName, fileName);
+ try
+ {
+ Store.DeleteFile(filePath);
+ }
+ catch (Exception) { }
+ }
+ }
+ catch (Exception) { }
+ }
+ }
+
+ private DateTime GetImageTimestamp(string imageKey)
+ {
+ string timestampFilePath = GetTimestampFilePath(imageKey);
+ try
+ {
+ lock (_storeLock)
+ {
+ if (!Store.FileExists(timestampFilePath))
+ {
+ return DateTime.MinValue;
+ }
+ using (IsolatedStorageFileStream fileStream = Store.OpenFile(timestampFilePath, FileMode.Open, FileAccess.Read))
+ using (StreamReader fileStreamReader = new StreamReader(fileStream, Encoding.UTF8))
+ {
+ string timestampString = fileStreamReader.ReadToEnd();
+ return DateTime.Parse(timestampString).ToUniversalTime();
+ }
+ }
+ }
+ catch (Exception)
+ {
+ return DateTime.MinValue;
+ }
+ }
+
+ private void SetImageTimestamp(string imageKey, DateTime timestamp)
+ {
+ string timestampFilePath = GetTimestampFilePath(imageKey);
+ try
+ {
+ lock (_storeLock)
+ {
+ IsolatedStorageFileStream fileStream;
+ if (Store.FileExists(timestampFilePath))
+ {
+ fileStream = Store.OpenFile(timestampFilePath, FileMode.Create, FileAccess.Write);
+ }
+ else
+ {
+ fileStream = Store.OpenFile(timestampFilePath, FileMode.CreateNew, FileAccess.Write);
+ }
+ using (fileStream)
+ using (StreamWriter fileStreamWriter = new StreamWriter(fileStream, Encoding.UTF8))
+ {
+ fileStreamWriter.Write(timestamp.ToUniversalTime().ToString("u"));
+ }
+ }
+ }
+ catch (Exception) { }
+ }
+
+ #endregion
+
+ #region Cache Pruning
+
+ private static readonly TimeSpan CachePruningInterval = TimeSpan.FromMinutes(1);
+ private static readonly TimeSpan CachePruningTimerDuration = TimeSpan.FromSeconds(5);
+
+ private DateTime _cachePruningTimestamp = DateTime.MinValue;
+ private OneShotDispatcherTimer _cachePruningTimer = null;
+
+ private void RequestCachePruning()
+ {
+ lock (this)
+ {
+ if (_cachePruningTimer != null || _cachePruningTimestamp.Add(CachePruningInterval) >= DateTime.UtcNow)
+ {
+ return;
+ }
+ Deployment.Current.Dispatcher.BeginInvoke(() =>
+ {
+ if (_cachePruningTimer != null)
+ {
+ return;
+ }
+ _cachePruningTimer = OneShotDispatcherTimer.CreateAndStart(CachePruningTimerDuration, OnCachePruningTimerFired);
+ });
+ }
+ }
+
+ private void OnCachePruningTimerFired(object sender, EventArgs e)
+ {
+ if (sender != _cachePruningTimer)
+ {
+ return;
+ }
+ _cachePruningTimer = null;
+ _cachePruningTimestamp = DateTime.UtcNow;
+ ThreadPool.QueueUserWorkItem((state) => { PruneCache(); });
+ }
+
+ private void PruneCache()
+ {
+ PrunePersistentCache();
+ PruneMemoryCache();
+ }
+
+ #endregion
+
+ #region Memory Cache
+
+ private readonly object _memoryCacheLock = new object();
+ private int _memoryCacheCapacity = DefaultMemoryCacheCapacity;
+ private Dictionary<string, LinkedListNode<byte[]>> _memoryCacheNodes = new Dictionary<string, LinkedListNode<byte[]>>(DefaultMemoryCacheCapacity);
+ private LinkedList<byte[]> _memoryCacheList = new LinkedList<byte[]>();
+
+ /// <summary>
+ /// The capacity of the in-memory cache.
+ /// If set to zero, the in-memory cache is disabled.
+ /// </summary>
+ public int MemoryCacheCapacity
+ {
+ get
+ {
+ return _memoryCacheCapacity;
+ }
+ set
+ {
+ if (value < 0)
+ {
+ throw new ArgumentOutOfRangeException();
+ }
+ lock (_memoryCacheLock)
+ {
+ _memoryCacheCapacity = value;
+ PruneMemoryCache();
+ }
+ }
+ }
+
+ private Stream LoadImageFromMemoryCache(string imageKey)
+ {
+ lock (_memoryCacheLock)
+ {
+ if (_memoryCacheCapacity == 0)
+ {
+ return null;
+ }
+ if (!_memoryCacheNodes.ContainsKey(imageKey))
+ {
+ return null;
+ }
+ LinkedListNode<byte[]> node = _memoryCacheNodes[imageKey];
+ if (node.List == _memoryCacheList)
+ {
+ _memoryCacheList.Remove(node);
+ }
+ _memoryCacheList.AddLast(node.Value);
+ PruneMemoryCache();
+ return new MemoryStream(node.Value);
+ }
+ }
+
+ private void WriteImageToMemoryCache(string imageKey, byte[] imageData)
+ {
+ if (string.IsNullOrEmpty(imageKey) || imageData == null || imageData.Length == 0)
+ {
+ return;
+ }
+ lock (_memoryCacheLock)
+ {
+ if (_memoryCacheCapacity == 0)
+ {
+ return;
+ }
+ if (_memoryCacheNodes.ContainsKey(imageKey))
+ {
+ _memoryCacheList.Remove(_memoryCacheNodes[imageKey]);
+ }
+ LinkedListNode<byte[]> newNode = _memoryCacheList.AddLast(imageData);
+ PruneMemoryCache();
+ _memoryCacheNodes[imageKey] = newNode;
+ }
+ }
+
+ private void PruneMemoryCache()
+ {
+ lock (_memoryCacheLock)
+ {
+ if (_memoryCacheCapacity == 0)
+ {
+ ClearMemoryCache();
+ return;
+ }
+ while (_memoryCacheList.Count > _memoryCacheCapacity)
+ {
+ DeleteFirstMemoryCacheNode();
+ }
+ }
+ }
+
+ private void DeleteFirstMemoryCacheNode()
+ {
+ lock (_memoryCacheLock)
+ {
+ LinkedListNode<byte[]> node = _memoryCacheList.First;
+ if (node == null)
+ {
+ return;
+ }
+ _memoryCacheList.Remove(node);
+ foreach (string imageKey in _memoryCacheNodes.Keys)
+ {
+ if (_memoryCacheNodes[imageKey] == node)
+ {
+ _memoryCacheNodes.Remove(imageKey);
+ break;
+ }
+ }
+ }
+ }
+
+ private void ClearMemoryCache()
+ {
+ lock (_memoryCacheLock)
+ {
+ _memoryCacheNodes.Clear();
+ _memoryCacheList.Clear();
+ }
+ }
+
+ #endregion
+
+ public override string ToString()
+ {
+ return string.Format("PersistentImageCache({0})", Name);
+ }
+ }
+}
diff --git a/Juick/Storage/SystemImageCache.cs b/Juick/Storage/SystemImageCache.cs
new file mode 100644
index 0000000..0f62f6b
--- /dev/null
+++ b/Juick/Storage/SystemImageCache.cs
@@ -0,0 +1,50 @@
+// Copyright 2010 Andreas Saudemont (andreas.saudemont@gmail.com)
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using System;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+
+namespace Kawagoe.Storage
+{
+ /// <summary>
+ /// Implements an <see cref="ImageCache"/> using the cache mechanism provided by the system.
+ /// </summary>
+ public class SystemImageCache : ImageCache
+ {
+ /// <summary>
+ /// Initializes a new <see cref="SystemImageCache"/> instance with the specified name.
+ /// </summary>
+ public SystemImageCache(string name)
+ : base(name)
+ {
+ }
+
+ /// <summary>
+ /// Implements <see cref="ImageCache.GetInternal"/>.
+ /// </summary>
+ protected override ImageSource GetInternal(Uri imageUri)
+ {
+ return new BitmapImage(imageUri);
+ }
+
+ /// <summary>
+ /// Implements <see cref="ImageCache.Clear"/>.
+ /// </summary>
+ public override void Clear()
+ {
+ // do nothing
+ }
+ }
+}
diff --git a/Juick/Threading/OneShotDispatcherTimer.cs b/Juick/Threading/OneShotDispatcherTimer.cs
new file mode 100644
index 0000000..4b2854d
--- /dev/null
+++ b/Juick/Threading/OneShotDispatcherTimer.cs
@@ -0,0 +1,160 @@
+// Copyright 2010 Andreas Saudemont (andreas.saudemont@gmail.com)
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using System;
+using System.Windows.Threading;
+
+namespace Kawagoe.Threading
+{
+ /// <summary>
+ /// Provides a one-shot timer integrated to the Dispatcher queue.
+ /// </summary>
+ public class OneShotDispatcherTimer
+ {
+ /// <summary>
+ /// Creates a new <see cref="OneShotDispatcherTimer"/> and starts it.
+ /// </summary>
+ /// <param name="duration">The duration of the timer.</param>
+ /// <param name="callback">The delegate that will be called when the timer fires.</param>
+ /// <returns>The newly created timer.</returns>
+ public static OneShotDispatcherTimer CreateAndStart(TimeSpan duration, EventHandler callback)
+ {
+ OneShotDispatcherTimer timer = new OneShotDispatcherTimer();
+ timer.Duration = duration;
+ timer.Fired += callback;
+ timer.Start();
+ return timer;
+ }
+
+ private TimeSpan _duration = TimeSpan.Zero;
+ private DispatcherTimer _timer = null;
+
+ /// <summary>
+ /// Initializes a new <see cref="OneShotDispatcherTimer"/> instance.
+ /// </summary>
+ public OneShotDispatcherTimer()
+ {
+ }
+
+ /// <summary>
+ /// The duration of the timer. The default is 00:00:00.
+ /// </summary>
+ /// <remarks>
+ /// Setting the value of this property takes effect the next time the timer is started.
+ /// </remarks>
+ /// <exception cref="ArgumentOutOfRangeException">The specified value when setting this property represents
+ /// a negative time internal.</exception>
+ public TimeSpan Duration
+ {
+ get
+ {
+ return _duration;
+ }
+ set
+ {
+ if (value.TotalMilliseconds < 0)
+ {
+ throw new ArgumentOutOfRangeException();
+ }
+ _duration = value;
+ }
+ }
+
+ /// <summary>
+ /// Indicates whether the timer is currently started.
+ /// </summary>
+ public bool IsStarted
+ {
+ get
+ {
+ return (_timer != null);
+ }
+ }
+
+ /// <summary>
+ /// Occurs when the one-shot timer fires.
+ /// </summary>
+ public event EventHandler Fired;
+
+ /// <summary>
+ /// Raises the <see cref="Fired"/> event.
+ /// </summary>
+ private void RaiseFired()
+ {
+ if (Fired != null)
+ {
+ try
+ {
+ Fired(this, EventArgs.Empty);
+ }
+ catch (Exception) { }
+ }
+ }
+
+ /// <summary>
+ /// Starts the timer.
+ /// This method has no effect if the timer is already started.
+ /// </summary>
+ /// <remarks>
+ /// The same <see cref="OneShotDispatcherTimer"/> instance can be started and stopped multiple times.
+ /// </remarks>
+ public void Start()
+ {
+ if (_timer != null)
+ {
+ return;
+ }
+
+ _timer = new DispatcherTimer();
+ _timer.Interval = _duration;
+ _timer.Tick += OnTimerTick;
+ _timer.Start();
+ }
+
+ /// <summary>
+ /// Stops the timer.
+ /// This method has no effect if the timer is not started.
+ /// </summary>
+ /// <remarks>
+ /// The <see cref="Fired"/> event is guaranteed not to be raised once this method has been invoked
+ /// and until the timer is started again.
+ /// </remarks>
+ public void Stop()
+ {
+ if (_timer == null)
+ {
+ return;
+ }
+ try
+ {
+ _timer.Stop();
+ }
+ catch (Exception) { }
+ _timer = null;
+ }
+
+ /// <summary>
+ /// Listens to Tick events on the underlying timer.
+ /// </summary>
+ private void OnTimerTick(object sender, EventArgs e)
+ {
+ if (sender != _timer)
+ {
+ return;
+ }
+ Stop();
+ RaiseFired();
+ }
+ }
+}
diff --git a/Juick/ViewModels/PostItem.cs b/Juick/ViewModels/PostItem.cs
index 2072cf5..e88f6f1 100644
--- a/Juick/ViewModels/PostItem.cs
+++ b/Juick/ViewModels/PostItem.cs
@@ -31,14 +31,14 @@ namespace Juick.ViewModels
public int MID {get;set;}
public int RID {get;set;}
-
- public string Username {get;set;}
-
- public Uri AvatarUri {get;set;}
-
- public Uri Attachment {get;set;}
- public string Status {get;set;}
+ public string Username { get; set; }
+
+ public Uri AvatarUri { get; set; }
+
+ public Uri Attachment { get; set; }
+
+ public string Status { get; set; }
public string MessageText { get; set; }
diff --git a/Juick/ViewModels/ViewModelBase.cs b/Juick/ViewModels/ViewModelBase.cs
index 2427a80..27fdc42 100644
--- a/Juick/ViewModels/ViewModelBase.cs
+++ b/Juick/ViewModels/ViewModelBase.cs
@@ -9,11 +9,13 @@ using System.Windows.Media.Imaging;
using Juick.Classes;
using JuickApi;
using RestSharp;
+using Kawagoe.Storage;
namespace Juick.ViewModels
{
public class ViewModelBase : INotifyPropertyChanged
{
+ ImageCache _cache = new PersistentImageCache("avatars");
static readonly string IsDataLoadingPropertyName = ExpressionHelper.GetPropertyName<ViewModelBase>(x => x.IsDataLoading);
bool isDataLoading;