最近在做一個Windows Phone 8.1的應用,因為應用里面使用了很多圖片,所以打算將圖片文件緩存到文件系統里面,以便節省流量和提升體驗。
一般顯示圖片的做法大多只需要將對應的Uri地址綁定到對應控件的ImageSource屬性上即可,或者將Uri傳入BitmapImage對象,其會自動將資源異步下載好然后加載。
為了不將緩存的邏輯侵入實體模型層,所以打算在Uri->BitmapImage綁定上做文章,也就是利用IValueConverter接口構造值轉換器。
這里有一個問題就是,因為綁定的原因,IValueConverter接口只能是同步的方法,而圖片的下載的過程是一個異步的過程,所以我們需要一種解決方案實現一個異步的ValueConverter。
這個解決方案我思考了很久,最后在這里受到了啟發:http://stackoverflow.com/questions/15003827/async-implementation-of-ivalueconverter。
Update: 順藤摸瓜找到了這個問題的答主的一個開源庫,主要是對async和await的拓展,推薦:https://github.com/StephenCleary/AsyncEx
具體而言,就是在同步的ValueConverter中構造一個Task-like的對象,然后將對應的屬性綁定到這個Task-like對象的一個屬性中去,然后Task-like待Task完成后更新對應的狀態。
這個解決方案實現很巧妙,同時也沒有對Model和ViewModel做任何的侵入,下面是我修改后用於圖片緩存的相關代碼:
首先是界面綁定所做的改動,大概如下,就是通過構造一個新的DataContext來實現綁定:
<Image DataContext="{Binding Image, Converter={StaticResource ImageConverter}}" Stretch="UniformToFill" Source="{Binding Result}" />
對應的ValueConverter類:
public class CacheImageValueConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, string language) { var image = value as Image; if (image == null || string.IsNullOrEmpty(image.Url)) { var bitmap = new BitmapImage(new Uri("ms-appx:///assets/default.png")); var notifier = new TaskCompletionNotifier<BitmapImage>(); notifier.SetTask(Task.FromResult(bitmap)); return notifier; } else { var task = Task.Run(async () => { var cache = HiwedoContainer.Current.Resolve<ImageCache>(); var uri = await cache.GetImageSourceFromUrlAsync(image.Url); return uri; }); var notifier = new TaskCompletionNotifier<BitmapImage>(); notifier.SetTask(task, c => new BitmapImage(c)); return notifier; } } public object ConvertBack(object value, Type targetType, object parameter, string language) { throw new NotImplementedException(); } }
其次是Task-like的類。
public sealed class TaskCompletionNotifier<TResult> : INotifyPropertyChanged { private TResult _result; private IAsyncResult _task; public TaskCompletionNotifier() { this._result = default(TResult); } public void SetTask<T>(Task<T> task, Func<T, TResult> factoryFunc) { this._task = task; if (!task.IsCompleted) { var scheduler = (SynchronizationContext.Current == null) ? TaskScheduler.Current : TaskScheduler.FromCurrentSynchronizationContext(); task.ContinueWith(t => { var propertyChanged = PropertyChanged; if (propertyChanged != null) { this.OnPropertyChanged("IsCompleted"); if (t.IsFaulted) { InnerException = t.Exception; this.OnPropertyChanged("ErrorMessage"); } else { try { this._result = factoryFunc(task.Result); } catch (Exception ex) { Debug.WriteLine("Factory error: " + ex.Message); this.InnerException = ex; this.OnPropertyChanged("ErrorMessage"); } this.OnPropertyChanged("Result"); } } }, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, scheduler); } else { this._result = factoryFunc(task.Result); } } public void SetTask(Task<TResult> task) { this._task = task; if (!task.IsCompleted) { var scheduler = (SynchronizationContext.Current == null) ? TaskScheduler.Current : TaskScheduler.FromCurrentSynchronizationContext(); task.ContinueWith(t => { var propertyChanged = PropertyChanged; if (propertyChanged != null) { this.OnPropertyChanged("IsCompleted"); if (t.IsFaulted) { InnerException = t.Exception; this.OnPropertyChanged("ErrorMessage"); } else { this._result = task.Result; this.OnPropertyChanged("Result"); } } }, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, scheduler); } else { this._result = task.Result; } } public TResult Result { get { return this._result; } } public bool IsCompleted { get { return _task.IsCompleted; } } public Exception InnerException { get; set; } public string ErrorMessage { get { return (InnerException == null) ? null : InnerException.Message; } } public event PropertyChangedEventHandler PropertyChanged; [NotifyPropertyChangedInvocator] private void OnPropertyChanged([CallerMemberName] string propertyName = null) { PropertyChangedEventHandler handler = PropertyChanged; if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName)); } }
提供一個Task和Factory的函數重載是必須的,因為對於像BitmapImage這樣的類,其初始化需要在對應的UI線程中,不能直接將BitmapImage作為結果返回,所以在下面也可以看到,實際上Task返回的只是Uri,然后構造BitmapImage是在ContinueWith的方法中完成的。
最后就是用於圖片緩存的類,這個類比較簡單,就是如果判斷如果圖片已經存在,就直接從文件系統返回,如果不存在就先下載再返回對應的Uri。然后會有一個變量緩存所有已經緩存的文件名。
public class ImageCache { private const string ImageCacheFolder = "ImageCaches"; private StorageFolder _cacheFolder; private IList<string> _cachedFileNames; public async Task<Uri> GetImageSourceFromUrlAsync(string url) { string fileName = url.Substring(url.LastIndexOf('/') + 1); if (this._cachedFileNames.Contains(fileName)) { return new Uri("ms-appdata:///local/ImageCaches/" + fileName); } if (await DownloadAndSaveAsync(url, fileName)) { _cachedFileNames.Add(fileName); return new Uri("ms-appdata:///local/ImageCaches/" + fileName); } Debug.WriteLine("Download image failed. " + url); return new Uri(url); } private async Task<bool> DownloadAndSaveAsync(string url, string filename) { try { var request = WebRequest.CreateHttp(url); request.Method = "GET"; using (var response = await request.GetResponseAsync()) { using (var responseStream = response.GetResponseStream()) { var file = await this._cacheFolder.CreateFileAsync(filename, CreationCollisionOption.ReplaceExisting); using (var fs = await file.OpenStreamForWriteAsync()) { await responseStream.CopyToAsync(fs); Debug.WriteLine("Downloaded: " + url); return true; } } } } catch (Exception ex) { Debug.WriteLine("Error: " + ex.Message); return false; } } public async Task LoadCache() { var folders = await ApplicationData.Current.LocalFolder.GetFoldersAsync(); foreach (var folder in folders) { if (folder.Name == ImageCacheFolder) { this._cacheFolder = folder; break; } } if (this._cacheFolder == null) { this._cacheFolder = await ApplicationData.Current.LocalFolder.CreateFolderAsync(ImageCacheFolder); } this._cachedFileNames = (await this._cacheFolder.GetFilesAsync()).Select(c => c.Name).ToList(); } public IList<string> CachedFileNames { get { return _cachedFileNames; } set { _cachedFileNames = value; } } }