Windows Phone App基于文件系统的图片缓存方案

最近在做一个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; }

        }

    }

你可能感兴趣的:(windows phone)