diff options
author | Vitaly Takmazov | 2013-03-29 14:58:12 +0400 |
---|---|---|
committer | Vitaly Takmazov | 2013-03-29 14:58:12 +0400 |
commit | 9f19cd09bfad13715bb4eda46e7782f56674e26c (patch) | |
tree | f2d7f8be17c0ab9a00056d372a364bb6938f2429 | |
parent | 32fc287a28c24ca980f4fe67ccab178c04cd9159 (diff) |
using PersistentImageCache for avatars
-rw-r--r-- | Juick/Controls/MessageList.xaml | 3 | ||||
-rw-r--r-- | Juick/Converters/UriToImageSourceConverter.cs | 31 | ||||
-rw-r--r-- | Juick/Juick.csproj | 5 | ||||
-rw-r--r-- | Juick/Storage/ImageCache.cs | 160 | ||||
-rw-r--r-- | Juick/Storage/PersistentImageCache.cs | 880 | ||||
-rw-r--r-- | Juick/Storage/SystemImageCache.cs | 50 | ||||
-rw-r--r-- | Juick/Threading/OneShotDispatcherTimer.cs | 160 | ||||
-rw-r--r-- | Juick/ViewModels/PostItem.cs | 14 | ||||
-rw-r--r-- | Juick/ViewModels/ViewModelBase.cs | 2 |
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; |