blazor maui hybrid app显示本地图片

啊… …
一通操作下来感觉就是两个字 折磨
跨平台有跨平台的好处 但框架本身支持的有限 很多东西做起来很曲折 哎

这里总结一下笔者为了折腾本地图片显示的尝试
为什么要做本地图片展示呢 如果是做需要网络连接的app 这个一般是不需要的(要做上传前预览/编辑的话还是要的)
但对于离线的app肯定是要的 总会有场景用户导入图片/文件之类的吧

笔者只测试了windows和安卓这两个平台,mac和iOS因为没有设备和开发者账号所以调试不了😀

复制到wwwroot

这是最简单直接的方法,什么都不用改,把图片复制到wwwroot下然后直接使用图片地址就行了
代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public async Task PickAndShow(PickOptions options)
{
try
{
var result = await FilePicker.Default.PickAsync(options);
if (result != null)
{
await using var stream = await result.OpenReadAsync();
var originalFileName = result.FileName;
var extension = Path.GetExtension(originalFileName);
var targetFileName = Guid.NewGuid() + extension;

var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "wwwroot", "images", targetFileName);
Directory.CreateDirectory(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "wwwroot", "images"));
await using var targetStream = new FileStream(path, FileMode.Create);
Debug.WriteLine($"copy path:{path}");
await stream.CopyToAsync(targetStream);
_objUrl = $"images/{targetFileName}";
}
}
catch (Exception ex)
{
Debug.WriteLine(ex);
}
}

这个的优点就是简单直接
缺点就是安卓不支持… 安卓的wwwroot在app bundle里 只能读不能写
适合只需要windows的使用
但你只需要windows 还用maui是不是有🍪

base64/objectURL

这两个我一起说 因为原理是一样的
存在sqlite appdata或者其他什么地方
需要用的时候读取转化为base64和objectURL

但是使用base64的时候我发现了很严重的内存泄漏,传了很多图片测试之后应用的内存直接炸了.
见: https://stackoverflow.com/questions/77513507/how-to-avoid-memory-leaks-when-using-base64-images-in-blazor

可能是我用的png有关,但他的确会造成内存较大的消耗,并且有不必要的开销,你转成base64,浏览器还得给他转回来.

用objectURL是个更好的选择
在index.html中放入这段代码:

1
2
3
4
5
6
7
8
9
10
11
<script>
window.createObjectURL = async (imageStream) => {
const arrayBuffer = await imageStream.arrayBuffer();
const blob = new Blob([arrayBuffer]);
return URL.createObjectURL(blob);
}

window.revokeObjectURL = (url) => {
URL.revokeObjectURL(url);
}
</script>

扩展一下IJSRuntime,这是为了方便使用,也可以直接调,都是一样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static class IJSRuntimeExtensions
{
public static ValueTask<string> CreateObjectUrl(this IJSRuntime js, Stream stream, bool leaveOpen = false)
{
var dotNetStreamReference = new DotNetStreamReference(stream, leaveOpen);
return js.InvokeAsync<string>("createObjectURL", dotNetStreamReference);
}

public static async Task RevokeObjectUrl(this IJSRuntime js, string? url)
{
Debug.WriteLine($"Revoke call url {url}");
if (url is not null)
{
await js.InvokeVoidAsync("revokeObjectURL", url);
}
}
}

哪里需要用调用一下获得objectURL即可

复制到appdata下 覆盖CreateFileProvider

从爆栈上发现的,测试了下windows和安卓上都可用,意外的简单直接.
见:
https://stackoverflow.com/a/75282680/2078863

1
2
3
4
5
6
7
8
public class CustomFilesBlazorWebView : BlazorWebView
{
public override IFileProvider CreateFileProvider(string contentRootDir)
{
var lPhysicalFiles = new PhysicalFileProvider(FileSystem.Current.AppDataDirectory);
return new CompositeFileProvider(lPhysicalFiles, base.CreateFileProvider(contentRootDir));
}
}

然后用这个替换原本的BlazorWebView即可.
之后就将文件复制到Appdata下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static readonly string ImageDir = "image";

public async Task<string> CopyToImageDir(byte[] bytes, string originalFileName)
{
var appFilePath = Path.Combine(GetImageDirPath(), CreateNewName(originalFileName));
Directory.CreateDirectory(GetImageDirPath());
await using var fileStream = File.Create(appFilePath);
await fileStream.WriteAsync(bytes);
return appFilePath;
}

public string GetImageDirPath()
{
return Path.Combine(FileSystem.AppDataDirectory, ImageDir);
}

private string CreateNewName(string originalFileName)
{
var extension = Path.GetExtension(originalFileName);
var uuid = Guid.NewGuid().ToString();
var newFileName = $"{Path.GetFileNameWithoutExtension(originalFileName)}_{uuid}{extension}";
return newFileName;
}

url使用image/filename即可


这些方案都不可避免要将文件复制都某个地方.
其实优化下可以直接读原始文件, 但是不同平台限制不太一样, 比如在安卓上FilePicker返回的fullPath是cache地址不是实际地址, 需要自己写native代码用安卓原生的文件选择.
其他还有不少大佬的方案,这里只做几个实现比较简单的总结.
市面上有许多由资深开发者提出的复杂方案,但在这里,我只总结了几个实现起来相对简单的方法.
唉,MAUI本身的使用确实颇具挑战性.虽然将Blazor集成进UI在某种程度上缓解了这一问题,但由于其跨平台的特性,很多功能似乎都不尽如人意.
初看上去,基础的使用似乎没什么问题,但一旦动手实现更复杂的功能,就会发现受到了各种限制.想要突破这些限制,就必须编写与平台相关的代码,而这又要求你必须深入了解Windows、安卓和iOS等系统的原生代码是如何编写的.

这种局面确实令人尴尬,这或许也是跨平台框架普遍存在的问题.

微软的努力将决定未来的发展,但从他们那边来看,情况似乎并不乐观.
就以本文读取本地图片的问题为例:
https://github.com/dotnet/maui/issues/2907
https://github.com/dotnet/aspnetcore/issues/25274
早在2021年甚至2020年,就有人提出了这个问题.
经过多个相关问题的讨论,认为在.NET 7中实现这一功能风险太大,决定推迟到.NET 8.

mkArtakMSFT modified the milestones: Backlog, .NET 7 Planning on Nov 6, 2021
danroth27 modified the milestones: .NET 7 Planning, .NET 8 Planning on Aug 24, 2022
While we have made progress on this feature, at this point we think the risk is too high to include this in the .NET 7 release. Moving to .NET 8.

2023-06-30 bot: We’ve moved this issue to the Backlog milestone. This means that it is not going to be worked on for the coming release. We will reassess the backlog following the current release and consider this item at that time. To learn more about our issue management process and to have better expectation regarding different types of issues you can read our Triage Process.

然而,即便.NET 8已经发布,这个功能依旧没有得到官方的实现, 只有各种workaround……这实在令人沮丧.

MAUI给人的感觉好像是一个被遗弃的项目.随着.NET 9或.NET 10的推出,微软可能会宣布停止支持MAUI,并推出另一个全新的框架.
毕竟微软的作风一贯如此.

相关参考链接:
Work with images in ASP.NET Core Blazor

[Bug] Not allowed to load local resource for Android

Blazor Image component to display images that are not accessible through HTTP endpoints

How to display local image as well as resources image in .Net MAUI Blazor

Overriding CreateFileProvider for BlazorWebView throws exception