在 Web 开发中,img 标签用来呈现图片,而且一般来说,浏览器是会对这些图片进行缓存的。

比如访问百度,我们可以发现,图片、脚本这种都是从缓存(内存缓存/磁盘缓存)中加载的,而不是再去访问一次百度的服务器,这样一方面改善了响应速度,另一方面也减轻了服务端的压力。

但是,对于 WPF 和 UWP 开发来说,原生的 Image 控件是只有内存缓存的,并没有磁盘缓存的,所以一旦程序退出了,下次再重新启动程序的话,那还是得从服务器上面取图片的。因此,打造一个具备缓存(尤其是磁盘缓存)的 Image 控件还是有必要的。

在 WPF 和 UWP 中,我们都知道 Image 控件 Source 属性的类型是 ImageSource,但是,如果我们使用数据绑定的话,是可以绑定一个字符串的,在运行的时候,我们会发现 Source 属性变成了一个 BitmapImage 类型的对象。那么可以推论出,是框架给我们做了一些转换。经过查阅 WPF 的相关资料,发现是 ImageSource 这个类型上有一个 TypeConverterAttribute:

查看 ImageSourceConverter 的源码(https://referencesource.microsoft.com/#PresentationCore/Core/CSharp/System/Windows/Media/ImageSourceConverter.cs,0f008db560b688fe),我们可以看到这么一段

因此,在对 Source 属性进行绑定的时候,我们的数据源是可以使用:string、Stream、Uri、byte[] 这些类型的,当然还有它自身 ImageSource(BitmapImage 是 ImageSource 的子类)。

虽然有 5 种这么多,然而最终我们需要的是 ImageSource。另外 Uri 就相当于 string 的转换。再仔细分析的话,我们大概可以得出下面的结论:

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

其中 Uri 到 byte[] 就是相当于从 Uri 对应的地方加载图片数据,常见的就是 web、磁盘和程序内嵌资源。

在某些节点我们是可以加上缓存的,如碰到一个 http/https 的地址,那可以先检查本地是否有缓存文件,有就直接加载不去访问服务器了。

经过整理,基本可以得出如下的流程图。

可以看出,流程是一个自上而下,再自下而上的流程。这里就相当于是一个管道处理模型。每一行等价于一个管道,然后整个流程相当于整个管道串联起来。

在代码的实现过程中,我借鉴了 asp.net core 中的 middleware 的处理过程。https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/middleware/?view=aspnetcore-2.1&tabs=aspnetcore2x

在 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 = ;
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

先声明,如果你在真实项目中使用出了问题,本人一概不负责的说。

本文只是介绍了一下具体关键点的实现思路,诸如磁盘缓存、Pipe 的服务注入(弄了一个很简单的)这些可以参考源代码中的实现。

另外源码中值得改进的地方应该是有的,希望大家能给出一些好的想法和意见,毕竟个人能力有限。

【WPF】【UWP】借鉴 asp.net core 管道处理模型打造图片缓存控件 ImageEx的更多相关文章

  1. ASP.NET Core MVC TagHelper实践HighchartsNET快速图表控件-开源

    ASP.NET Core MVC TagHelper最佳实践HighchartsNET快速图表控件支持ASP.NET Core. 曾经在WebForms上写过 HighchartsNET快速图表控件- ...

  2. 通过重建Hosting系统理解HTTP请求在ASP.NET Core管道中的处理流程[下]:管道是如何构建起来的?

    在<中篇>中,我们对管道的构成以及它对请求的处理流程进行了详细介绍,接下来我们需要了解的是这样一个管道是如何被构建起来的.总的来说,管道由一个服务器和一个HttpApplication构成 ...

  3. ASP.NET Core管道深度剖析(4):管道是如何建立起来的?

    在<管道是如何处理HTTP请求的?>中,我们对ASP.NET Core的请求处理管道的构成以及它对请求的处理流程进行了详细介绍,接下来我们需要了解的是这样一个管道是如何被构建起来的.这样一 ...

  4. ASP.NET Core管道深度剖析(2):创建一个“迷你版”的管道来模拟真实管道请求处理流程

    从<ASP.NET Core管道深度剖析(1):采用管道处理HTTP请求>我们知道ASP.NET Core请求处理管道由一个服务器和一组有序的中间件组成,所以从总体设计来讲是非常简单的,但 ...

  5. win10 uwp 使用 asp dotnet core 做图床服务器客户端

    原文 win10 uwp 使用 asp dotnet core 做图床服务器客户端 本文告诉大家如何在 UWP 做客户端和 asp dotnet core 做服务器端来做一个图床工具   服务器端 从 ...

  6. ASP.NET CORE 管道模型及中间件使用解读

    说到ASP.NET CORE 管道模型不得不先来看看之前的ASP.NET 的管道模型,两者差异很大,.NET CORE 3.1 后完全重新设计了框架的底层,.net core 3.1 的管道模型更加灵 ...

  7. ASP.Net 管道模型 VS Asp.Net Core 管道 总结

    1 管道模型  1 Asp.Net Web Form管道 请求进入Asp.Net工作进程后,由进程创建HttpWorkRequest对象,封装此次请求有关的所有信息,然后进入HttpRuntime类进 ...

  8. Asp.Net Core 轻松学-正确使用分布式缓存

    前言     本来昨天应该更新的,但是由于各种原因,抱歉,让追这个系列的朋友久等了.上一篇文章 在.Net Core 使用缓存和配置依赖策略 讲的是如何使用本地缓存,那么本篇文章就来了解一下如何使用分 ...

  9. ASP.NET Core 简单实现七牛图片上传(FormData 和 Base64)

    ASP.NET Core 简单实现七牛图片上传(FormData 和 Base64) 七牛图片上传 SDK(.NET 版本):https://developer.qiniu.com/kodo/sdk/ ...

随机推荐

  1. 学习excel的使用技巧二批量复制

    1 选中要操作的部分 2 CTRL+G 打开定位 3 点击 定位条件 4 选择空值 5 输入=号  然后键盘的 方向键  向上 6 按住CTRL+回车 即可实现  批量复制

  2. ApacheTraffic Server 使用ssd 以及裸盘

    使用裸设备后可以使用ATS自身的文件子系统,可以获得更好的IO性能,也是官方推荐的方式.下面为例 删除分区,不使用操作系统自带分区 `fdisk -l /dev/sde` 修改相关设备权限并创新相关设 ...

  3. python的相对导入

    最近断断续续学习flask,学到蓝本时候有点小问题卡住了,问题如下 导入包的时候py文件里使用了相对路径导入,但是这种导入方法不是很明白,就自己搜索加实验了终于有点眉目了 先定义一个包 adb包 这个 ...

  4. Sql Server数据库之约束

    一.约束的分类 实体约束:关于行的约束,比如某一行出现的值就不允许别的行出现,如主键 域约束:关于列的约束,对表中所有行的某些列进行约束,如check约束 参照完整性约束:如果某列的值必须与其他列的值 ...

  5. cdnbest自定义错误显示节点名教程

    在自定义错误里选择js选项,输入: document.write("error!" + hostname); 这是最简单的写法,只显示节点名,如果要显示其他效果,可自已修改js

  6. Node2.js

    Node.js简单爬虫的爬取,也是跟着慕课网上抄的,网站有一点点改动,粘上来好复习嘛 var http = require('http') var cheerio = require('cheerio ...

  7. JDBC缺点分析

    * JDBC代码繁琐,每一次JDBC都需要编写“同样”的六步. * sql不能配置,在JDBC编程中sql语句是写在java源程序当中的,sql语句经常会发生改变(业务发生了改变),sql改变之后,需 ...

  8. Java 运行时常量池

    运行时常量池是方法区的一部分.class中除了有类的版本,字段,方法,接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放在方法区的运行时常量池 ...

  9. linux环境下安装oracle步骤和自启动oracle

    oracle安装步骤 一.创建用户 --注释-- /etc/passwd 用户配置文件 /etc/shadow 用户密码文件 /etc/group 组 组用户文件/etc/gshadow 组密码文件 ...

  10. Python中使用%还是format来格式化字符串?

    Python中应该使用%还是format来格式化字符串?   %还是format Python中格式化字符串目前有两种阵营:%和format,我们应该选择哪种呢? 自从Python2.6引入了form ...