From 9f19cd09bfad13715bb4eda46e7782f56674e26c Mon Sep 17 00:00:00 2001 From: Vitaly Takmazov Date: Fri, 29 Mar 2013 14:58:12 +0400 Subject: using PersistentImageCache for avatars --- Juick/Controls/MessageList.xaml | 3 +- Juick/Converters/UriToImageSourceConverter.cs | 31 + Juick/Juick.csproj | 5 + Juick/Storage/ImageCache.cs | 160 +++++ Juick/Storage/PersistentImageCache.cs | 880 ++++++++++++++++++++++++++ Juick/Storage/SystemImageCache.cs | 50 ++ Juick/Threading/OneShotDispatcherTimer.cs | 160 +++++ Juick/ViewModels/PostItem.cs | 14 +- Juick/ViewModels/ViewModelBase.cs | 2 + 9 files changed, 1297 insertions(+), 8 deletions(-) create mode 100644 Juick/Converters/UriToImageSourceConverter.cs create mode 100644 Juick/Storage/ImageCache.cs create mode 100644 Juick/Storage/PersistentImageCache.cs create mode 100644 Juick/Storage/SystemImageCache.cs create mode 100644 Juick/Threading/OneShotDispatcherTimer.cs 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"> + @@ -37,7 +38,7 @@ - + MessageList.xaml + LoginView.xaml @@ -98,6 +99,10 @@ NewPostView.xaml + + + + ThreadView.xaml 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 +{ + /// + /// Defines the base clase for image cache implementations. + /// + public abstract class ImageCache + { + /// + /// The name of the default image cache. + /// + public const string DefaultImageCacheName = "default"; + + private static ImageCache _defaultImageCache = null; + private static object _defaultImageCacheLock = new object(); + + /// + /// The default image cache. + /// If not set explicitely, a instance is used by default. + /// + 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; + } + } + } + + /// + /// Initializes a new instance. + /// + protected ImageCache(string name) + { + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentException(); + } + Name = name; + } + + /// + /// The name of the image cache. + /// + protected string Name + { + get; + private set; + } + + /// + /// Retrieves the source for the image with the specified URI from the cache, downloading it + /// if needed. + /// + /// The URI of the image. Must be an absolute URI. + /// An ImageSource object, or null if is null or not an absolute URI. + /// The method is not called in the UI thread. + 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); + } + + /// + /// Retrieves the source for the image with the specified URI from the cache, downloading it + /// if needed. + /// + /// The URI of the image. Must be an absolute URI. + /// An ImageSource object, or null if is null, + /// the empty string, or not an absolute URI. + /// The method is not called in the UI thread. + 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); + } + + /// + /// The actual implementation of . + /// + protected abstract ImageSource GetInternal(Uri imageUri); + + /// + /// 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. + /// + public abstract void Clear(); + + /// + /// Overrides object.ToString(). + /// + /// + 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 +{ + /// + /// Implements an on top of the isolated storage. + /// + public class PersistentImageCache : ImageCache + { + /// + /// The default value of . + /// + public static readonly TimeSpan DefaultExpirationDelay = TimeSpan.FromDays(1); + + /// + /// The default value of . + /// + public const int DefaultMemoryCacheCapacity = 100; + + private const string ImageDataExtension = "data"; + private const string ImageTimestampExtension = "tstamp"; + + private TimeSpan _expirationDelay = DefaultExpirationDelay; + + /// + /// Initializes a new instance with the specified name. + /// + public PersistentImageCache(string name) + : base(name) + { + } + + /// + /// The delay after which an image, once downloaded, is consider expired and is deleted + /// from the cache. + /// + public TimeSpan ExpirationDelay + { + get + { + return _expirationDelay; + } + set + { + if (value.TotalMinutes < 1) + { + throw new ArgumentOutOfRangeException(); + } + _expirationDelay = value; + RequestCachePruning(); + } + } + + /// + /// Implements . + /// + 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); + } + } + + /// + /// Implements . + /// + public override void Clear() + { + lock (_storeLock) + lock (_memoryCacheLock) + { + ClearMemoryCache(); + DeleteAllImagesFromStore(); + } + } + + #region Image Downloads + + private readonly Dictionary _pendingRequests = new Dictionary(); + + 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 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(); + } + 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(); + } + + public Uri ImageUri + { + get; + private set; + } + + public byte[] ImageData + { + get; + private set; + } + + public IList 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(); + + /// + /// The name of directory in isolated storage that contains the files of this image cache. + /// + private string StoreDirectoryName + { + get + { + return "ImageCache_" + Name; + } + } + + /// + /// The isolated storage file used by the cache. + /// + 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> _memoryCacheNodes = new Dictionary>(DefaultMemoryCacheCapacity); + private LinkedList _memoryCacheList = new LinkedList(); + + /// + /// The capacity of the in-memory cache. + /// If set to zero, the in-memory cache is disabled. + /// + 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 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 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 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 +{ + /// + /// Implements an using the cache mechanism provided by the system. + /// + public class SystemImageCache : ImageCache + { + /// + /// Initializes a new instance with the specified name. + /// + public SystemImageCache(string name) + : base(name) + { + } + + /// + /// Implements . + /// + protected override ImageSource GetInternal(Uri imageUri) + { + return new BitmapImage(imageUri); + } + + /// + /// Implements . + /// + 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 +{ + /// + /// Provides a one-shot timer integrated to the Dispatcher queue. + /// + public class OneShotDispatcherTimer + { + /// + /// Creates a new and starts it. + /// + /// The duration of the timer. + /// The delegate that will be called when the timer fires. + /// The newly created timer. + 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; + + /// + /// Initializes a new instance. + /// + public OneShotDispatcherTimer() + { + } + + /// + /// The duration of the timer. The default is 00:00:00. + /// + /// + /// Setting the value of this property takes effect the next time the timer is started. + /// + /// The specified value when setting this property represents + /// a negative time internal. + public TimeSpan Duration + { + get + { + return _duration; + } + set + { + if (value.TotalMilliseconds < 0) + { + throw new ArgumentOutOfRangeException(); + } + _duration = value; + } + } + + /// + /// Indicates whether the timer is currently started. + /// + public bool IsStarted + { + get + { + return (_timer != null); + } + } + + /// + /// Occurs when the one-shot timer fires. + /// + public event EventHandler Fired; + + /// + /// Raises the event. + /// + private void RaiseFired() + { + if (Fired != null) + { + try + { + Fired(this, EventArgs.Empty); + } + catch (Exception) { } + } + } + + /// + /// Starts the timer. + /// This method has no effect if the timer is already started. + /// + /// + /// The same instance can be started and stopped multiple times. + /// + public void Start() + { + if (_timer != null) + { + return; + } + + _timer = new DispatcherTimer(); + _timer.Interval = _duration; + _timer.Tick += OnTimerTick; + _timer.Start(); + } + + /// + /// Stops the timer. + /// This method has no effect if the timer is not started. + /// + /// + /// The event is guaranteed not to be raised once this method has been invoked + /// and until the timer is started again. + /// + public void Stop() + { + if (_timer == null) + { + return; + } + try + { + _timer.Stop(); + } + catch (Exception) { } + _timer = null; + } + + /// + /// Listens to Tick events on the underlying timer. + /// + 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(x => x.IsDataLoading); bool isDataLoading; -- cgit v1.2.3