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

来源:https://www.cnblogs.com/h82258652/archive/2018/04/13/8820725.html
-Advertisement-
Play Games

在 Web 開發中,img 標簽用來呈現圖片,而且一般來說,瀏覽器是會對這些圖片進行緩存的。 比如訪問百度,我們可以發現,圖片、腳本這種都是從緩存(記憶體緩存/磁碟緩存)中載入的,而不是再去訪問一次百度的伺服器,這樣一方面改善了響應速度,另一方面也減輕了服務端的壓力。 但是,對於 WPF 和 UWP ...


在 Web 開發中,img 標簽用來呈現圖片,而且一般來說,瀏覽器是會對這些圖片進行緩存的。

QQ截圖20180412110412

比如訪問百度,我們可以發現,圖片、腳本這種都是從緩存(記憶體緩存/磁碟緩存)中載入的,而不是再去訪問一次百度的伺服器,這樣一方面改善了響應速度,另一方面也減輕了服務端的壓力。

 

但是,對於 WPF 和 UWP 開發來說,原生的 Image 控制項是只有記憶體緩存的,並沒有磁碟緩存的,所以一旦程式退出了,下次再重新啟動程式的話,那還是得從伺服器上面取圖片的。因此,打造一個具備緩存(尤其是磁碟緩存)的 Image 控制項還是有必要的。

在 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 的轉換。再仔細分析的話,我們大概可以得出下麵的結論:

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

其中 Uri 到 byte[] 就是相當於從 Uri 對應的地方載入圖片數據,常見的就是 web、磁碟和程式內嵌資源。

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

 

經過整理,基本可以得出如下的流程圖。

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 的服務註入(弄了一個很簡單的)這些可以參考源代碼中的實現。

另外源碼中值得改進的地方應該是有的,希望大家能給出一些好的想法和意見,畢竟個人能力有限。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 1. 操作系統介面 os 模塊提供很多函數與操作系統進行交互︰ 確保使用import os而不是from os import *。這樣可以防止函數os.open()覆蓋內建函數open(),兩者之間的操作是很不同的。內建函數dir()和help()對os這樣的大型模塊提供互動式的幫助是很有用的: 對 ...
  • 前言 在這裡主要分析下ThreadLocal類的結構,與set(),get(),remove()方法的源碼 1.ThreadLocal類的結構 ThreadLocal是java.lang下麵的類。 這個類下麵有幾個靜態內部類。比如: 在ThreadLocalMap類下麵還有一個靜態內部類 在上圖中可 ...
  • sqlalchemy中使用query查詢,而flask-sqlalchemy中使用basequery查詢,他們是子類與父類的關係 假設 page_index=1,page_size=10;所有分頁查詢不可以再跟first(),all()等 1.用offset()設置索引偏移量,limit()限制取出 ...
  • web api與webservice以及wcf的區別? Web Service 1、它是基於SOAP協議的,數據格式是XML 2、只支持HTTP協議 3、它不是開源的,但可以被任意一個瞭解XML的人使用 4、它只能部署在IIS上 WCF 1、這個也是基於SOAP的,數據格式是XML 2、這個是Web ...
  • 概述 UWP Community Toolkit Extensions 中有一個為 WebView 提供的擴展 - WebViewExtensions,本篇我們結合代碼詳細講解 WebView Extensions 的實現。 WebView Extensions 允許使用附加屬性,在 XAML 中指 ...
  • C#情懷與未來,怨天尤人還是抓住機會,能否跟上dnc新時代浪潮? 經常看到有.NET圈子在討論是否應該轉其它語言 C#情懷是一方面,如果覺得C#未來沒前途,光靠情懷是撐不住的, 建議對C#未來沒信心的朋友,轉go、rust、py、TS、JS、java …… 常常看到有人抱怨.NET,抱怨好幾年了,卻 ...
  • IoC框架最本質的東西:反射或者EMIT來實例化對象。然後我們可以加上緩存,或者一些策略來控制對象的生命周期,比如是否是單例對象還是每次都生成一個新的對象。 之前對DI註入與控制器擴展竟然用依賴性解析器來實現,兩個方面深感疑惑,由於越學越不懂,越學越頭暈,因此就暫且放下了,接著學習,誰知道今天寫程式 ...
  • 游戲規則說明: 由系統生成一個隨機數,玩家有三次猜數字的機會,如果在三次內猜出數字反饋玩家猜對了,否則Game Over! 代碼設計說明: 1.首先設計一個簡易的歡迎界面,並提示玩家是否開始游戲; 2.由系統自動生成一個隨機數; 3.由玩家輸入一個數值與系統生成隨機數進行比較,判斷是否相同,如果相同 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...