【WPF】【UWP】借鑑 asp.net core 管道處理模型打造圖片緩存控件 ImageEx

在 Web 開發中,img 標籤用來呈現圖片,並且通常來講,瀏覽器是會對這些圖片進行緩存的。git

QQ截圖20180412110412

好比訪問百度,咱們能夠發現,圖片、腳本這種都是從緩存(內存緩存/磁盤緩存)中加載的,而不是再去訪問一次百度的服務器,這樣一方面改善了響應速度,另外一方面也減輕了服務端的壓力。github

 

可是,對於 WPF 和 UWP 開發來講,原生的 Image 控件是隻有內存緩存的,並無磁盤緩存的,因此一旦程序退出了,下次再從新啓動程序的話,那仍是得從服務器上面取圖片的。所以,打造一個具有緩存(尤爲是磁盤緩存)的 Image 控件仍是有必要的。web

在 WPF 和 UWP 中,咱們都知道 Image 控件 Source 屬性的類型是 ImageSource,可是,若是咱們使用數據綁定的話,是能夠綁定一個字符串的,在運行的時候,咱們會發現 Source 屬性變成了一個 BitmapImage 類型的對象。那麼能夠推論出,是框架給咱們作了一些轉換。通過查閱 WPF 的相關資料,發現是 ImageSource 這個類型上有一個 TypeConverterAttribute:瀏覽器

QQ截圖20180413130857

查看 ImageSourceConverter 的源碼(https://referencesource.microsoft.com/#PresentationCore/Core/CSharp/System/Windows/Media/ImageSourceConverter.cs,0f008db560b688fe),咱們能夠看到這麼一段緩存

QQ截圖20180413131203

所以,在對 Source 屬性進行綁定的時候,咱們的數據源是可使用:string、Stream、Uri、byte[] 這些類型的,固然還有它自身 ImageSource(BitmapImage 是 ImageSource 的子類)。服務器

雖然有 5 種這麼多,然而最終咱們須要的是 ImageSource。另外 Uri 就至關於 string 的轉換。再仔細分析的話,咱們大概能夠得出下面的結論:app

string –> Uri –> byte[] –> Stream –> ImageSource框架

其中 Uri 到 byte[] 就是至關於從 Uri 對應的地方加載圖片數據,常見的就是 web、磁盤和程序內嵌資源。asp.net

在某些節點咱們是能夠加上緩存的,如碰到一個 http/https 的地址,那能夠先檢查本地是否有緩存文件,有就直接加載不去訪問服務器了。async

 

通過整理,基本能夠得出以下的流程圖。

ImageEx流程圖

能夠看出,流程是一個自上而下,再自下而上的流程。這裏就至關因而一個管道處理模型。每一行等價於一個管道,而後整個流程至關於整個管道串聯起來。

 

在代碼的實現過程當中,我借鑑了 asp.net core 中的 middleware 的處理過程。https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/middleware/?view=aspnetcore-2.1&tabs=aspnetcore2x

request-delegate-pipeline

在 asp.net core 中,middleware 的其中一種寫法以下:

public class AspNetCoreMiddleware
{
    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        // before
        await next(context);
        // after
    }
}

先創建一個相似 HttpContext 的上下文,用於在這個管道模型中處理,我就叫 LoadingContext:

public class LoadingContext<TResult> where TResult : class
{
    private byte[] _httpResponseBytes;
    private TResult _result;

    public LoadingContext(object source)
    {
        if (source == null)
        {
            throw new ArgumentNullException(nameof(source));
        }

        OriginSource = source;
        Current = source;
    }

    public object Current { get; set; }

    public byte[] HttpResponseBytes
    {
        get => _httpResponseBytes;
        set
        {
            if (_httpResponseBytes != null)
            {
                throw new InvalidOperationException("value has been set.");
            }

            _httpResponseBytes = value;
        }
    }

    public object OriginSource { get; }

    public TResult Result
    {
        get => _result;
        set
        {
            if (_result != null)
            {
                throw new InvalidOperationException("value has been set.");
            }

            _result = value;
        }
    }
}

這裏有四個屬性,OriginSource 表明輸入的原始 Source,Current 表明當前的 Source 值,在一開始是與 OriginSource 一致的。Result 表明了最終的輸出,通常不須要用戶手動設置,只須要到達管道底部的話,若是 Result 仍然爲空,那麼將 Current 賦值給 Result 就是了。HttpResponseBytes 一旦設置了就不可再設置。

可能大家會問,爲啥要單獨弄 HttpResponseBytes 這個屬性呢,不能在下載完成的時候緩存到磁盤嗎?這裏考慮到下載回來的不必定是一幅圖片,等到後面成功了,獲得一個 ImageSource 對象了,那才能認爲這是一個圖片,這時候才緩存。

另外爲啥是泛型,這裏考慮到擴展性,搞很差某個 Image 的 Source 類型就不是 ImageSource 呢(*^_^*)

而 RequestDelegate 是一個委託,簽名以下:

public delegate System.Threading.Tasks.Task RequestDelegate(HttpContext context);

所以我仿照,代碼裏就建一個 PipeDelegate 的委託。

public delegate Task PipeDelegate<TResult>([NotNull]LoadingContext<TResult> context, CancellationToken cancellationToken = default(CancellationToken)) where TResult : class;

NotNullAttribute 是來自 JetBrains.Annotations 這個 nuget 包的。

另外微軟爸爸說,支持取消的話,那是好作法,要表揚的,所以加上了 CancellationToken 參數。

 

接下來那就能夠準備咱們本身的 middleware 了,代碼以下:

public abstract class PipeBase<TResult> : IDisposable where TResult : class
{
    protected bool IsInDesignMode => (bool)DesignerProperties.IsInDesignModeProperty.GetMetadata(typeof(DependencyObject)).DefaultValue;

    public virtual void Dispose()
    {
    }

    public abstract Task InvokeAsync([NotNull]LoadingContext<TResult> context, [NotNull]PipeDelegate<TResult> next, CancellationToken cancellationToken = default(CancellationToken));
}

跟 asp.net core 的 middleware 很像,這裏我加了一個 IsInDesignMode 屬性,畢竟在設計器模式下面,就不必跑緩存相關的分支了。

 

那麼,咱們本身的 middleware,也就是 Pipe 有了,該怎麼串聯起來呢,這裏咱們能夠看 asp.net core 的源碼

https://github.com/aspnet/HttpAbstractions/blob/a78b194a84cfbc560a56d6d951eb71c8367d17bb/src/Microsoft.AspNetCore.Http/Internal/ApplicationBuilder.cs

        public RequestDelegate Build()
        {
            RequestDelegate app = context =>
            {
                context.Response.StatusCode = 404;
                return Task.CompletedTask;
            };

            foreach (var component in _components.Reverse())
            {
                app = component(app);
            }

            return app;
        }

其中 _components 的定義以下:

private readonly IList<Func<RequestDelegate, RequestDelegate>> _components = new List<Func<RequestDelegate, RequestDelegate>>();

Func<RequestDelegate, RequestDelegate> 表明輸入了一個委託,返回了一個委託。而上面 app 就至關於管道的最底部了,由於沒法處理了,所以就賦值爲 404 了。至於爲啥要反轉一下列表,這個你們能夠本身手動試試,這裏也很差解析。

所以,我編寫出以下的代碼來組裝咱們的 Pipe。

internal static PipeDelegate<TResult> Build<TResult>(IEnumerable<Type> pipes) where TResult : class
{
    PipeDelegate<TResult> end = (context, cancellationToken) =>
    {
        if (context.Result == null)
        {
            context.Result = context.Current as TResult;
        }
        if (context.Result == null)
        {
            throw new NotSupportedException();
        }

        return Task.CompletedTask;
    };

    foreach (var pipeType in pipes.Reverse())
    {
        Func<PipeDelegate<TResult>, PipeDelegate<TResult>> handler = next =>
        {
            return (context, cancellationToken) =>
            {
                using (var pipe = CreatePipe<TResult>(pipeType))
                {
                    return pipe.InvokeAsync(context, next, cancellationToken);
                }
            };
        };
        end = handler(end);
    }

    return end;
}

代碼比 asp.net core  的複雜一點,先看上面 end 的初始化。由於到達了管道的底部,若是 Result 仍然是空的話,那麼嘗試將 Current 賦值給 Result,若是執行後仍是空,那說明輸入的 Source 是不支持的類型,就直接拋出異常好了。

在下面的循環體中,handler 等價於上面 asp.net core 的 component,接受了一個委託,返回了一個委託。

委託體中,根據當前管道的類型建立了一個實例,並執行 InvokeAsync 方法。

 

構建管道的代碼也有了,所以加載邏輯也沒啥難的了。

        private async Task SetSourceAsync(object source)
        {
            if (_image == null)
            {
                return;
            }

            _lastLoadCts?.Cancel();
            if (source == null)
            {
                _image.Source = null;
                VisualStateManager.GoToState(this, NormalStateName, true);
                return;
            }

            _lastLoadCts = new CancellationTokenSource();
            try
            {
                VisualStateManager.GoToState(this, LoadingStateName, true);

                var context = new LoadingContext<ImageSource>(source);

                var pipeDelegate = PipeBuilder.Build<ImageSource>(Pipes);
                var retryDelay = RetryDelay;
                var policy = Policy.Handle<Exception>().WaitAndRetryAsync(RetryCount, count => retryDelay, (ex, delay) =>
                {
                    context.Reset();
                });
                await policy.ExecuteAsync(() => pipeDelegate.Invoke(context, _lastLoadCts.Token));

                if (!_lastLoadCts.IsCancellationRequested)
                {
                    _image.Source = context.Result;
                    VisualStateManager.GoToState(this, OpenedStateName, true);
                    ImageOpened?.Invoke(this, EventArgs.Empty);
                }
            }
            catch (Exception ex)
            {
                if (!_lastLoadCts.IsCancellationRequested)
                {
                    _image.Source = null;
                    VisualStateManager.GoToState(this, FailedStateName, true);
                    ImageFailed?.Invoke(this, new ImageExFailedEventArgs(source, ex));
                }
            }
        }

咱們的 ImageEx 控件裏面必然須要有一個原生的 Image 控件進行承載(否則咋顯示)。

這裏我定義了 4 個 VisualState:

Normal:未加載,Source 爲 null 的狀況。

Opened:加載成功,並引起 ImageOpened 事件。

Failed:加載失敗,並引起 ImageFailed 事件。

Loading:正在加載。

在這段代碼中,我引入了 Polly 這個庫,用於重試,一旦出現異常,就重置 context 到初始狀態,再從新執行管道。

而 _lastLoadCts 的類型是 CancellationTokenSource,由於若是 Source 發生快速變化的話,那麼先前還在執行的就須要放棄掉了。

 

 

最後奉上源代碼(含 WPF 和 UWP demo):

https://github.com/h82258652/HN.Controls.ImageEx

先聲明,若是你在真實項目中使用出了問題,本人一律不負責的說。2018new_doge_thumb

本文只是介紹了一下具體關鍵點的實現思路,諸如磁盤緩存、Pipe 的服務注入(弄了一個很簡單的)這些能夠參考源代碼中的實現。

另外源碼中值得改進的地方應該是有的,但願你們能給出一些好的想法和意見,畢竟我的能力有限。

相關文章
相關標籤/搜索