如何在Blazor中客户端生成并保存文件?

70

我想要一个单页应用程序,所有工作都在客户端完成,甚至生成一些图表/可视化效果。

我希望能够让用户点击按钮并保存页面上的可视化效果、表格和其他内容(包括可见和不可见的内容,因此右键保存或复制/粘贴并不总是可行的)。

如何从 WebAssembly/Blazor 库中调用函数,获取其结果并将其保存为客户端文件?

这个想法是这样的...?

cshtml

<input type="file" onchange="@ReadFile">

<input type="file" onchange="@SaveFile">

@functions{
object blazorObject = new blazorLibrary.SomeObject();

void ReadFile(){
    blazorObject.someFunction(...selectedFile?...);

}
void SaveFile(){
    saveFile(...selectedFile..?)
}

}

保存文件时,您是要告诉浏览器“下载/另存为”您生成的文件吗? - InDieTasten
是的,那就是我的意思。 - shawn.mek
2
我不太确定目前使用Blazor可以完成多少工作,但是保存文件的JS版本可以在这里看到。 - InDieTasten
我认为这是一个有用的解决方法来保存文件(如果当前在Blazor中不能实现),但即使使用JS的方式,我所遇到的困难是如何使用Blazor在客户端生成信息并将其下载到某个文件中。 - shawn.mek
将字节作为Base64字符串与二进制文件不同。 - MPinello
6个回答

87

Blazor 创始人 Steve Sanderson 在他最近的其中一次演示中使用了 JavaScript Interop 来完成类似的任务。

你可以在 BlazorExcelSpreadsheet 找到该示例。

解决方案包括三个部分:

1)JavaScript

function saveAsFile(filename, bytesBase64) {
        var link = document.createElement('a');
        link.download = filename;
        link.href = "data:application/octet-stream;base64," + bytesBase64;
        document.body.appendChild(link); // Needed for Firefox
        link.click();
        document.body.removeChild(link);
    }

2) C# 互操作封装

public static class FileUtil
{
    public async static Task SaveAs(IJSRuntime js, string filename, byte[] data)
    {
        await js.InvokeAsync<object>(
            "saveAsFile",
            filename,
            Convert.ToBase64String(data));
    }            
}

3) 从您的组件调用

@inject IJSRuntime js
@functions {
    void DownloadFile() {
        var text = "Hello, world!";
        var bytes = System.Text.Encoding.UTF8.GetBytes(text);
        FileUtil.SaveAs(js, "HelloWorld.txt", bytes);
    }
}

您可以在Blazor Fiddle中看到它的实际应用。


1
还没有其他方法来做这件事吗? - FranzHuber23
2
即使使用JS,我仍然相信这是最好的方式,因为它是纯Blazor。混合Blazor和MVC并不是很好的选择。 - Guilherme
3
下载一个很大的文件时,这种解决方案不会消耗太多内存吗?将其转换为Base64似乎是个难题。 - Tim Friesen
1
在 MS Edge 中似乎无法正常工作。在 fiddle 中,我在开发工具中收到错误消息“错误:电路已因错误而关闭。” - StefanFFM
2
Blazor是否有支持此功能的GitHub问题?谢谢! - Jan Paolo Go
显示剩余6条评论

19
  1. 添加一个链接

<a class="form-control btn btn-primary" href="/download?name=test.txt" target="_blank">Download</a>

  1. 添加具有路由的Razor页面
    2.1. 创建Razor页面 'Download.cshtml' 或其他名称... 'PewPew.cshtml'... 都可以
    2.2. 在创建的页面中放置以下代码
    @page "/download"
    @model MyNamespace.DownloadModel
  2. 编辑 Download.cshtml.cs 文件
public class DownloadModel : PageModel
{
    public IActionResult OnGet(string name) {
        // do your magic here
        var content = new byte[] { 1, 2, 3 };
        return File(content, "application/octet-stream", name);
    }
}

唉,这对我没用。我得到了“抱歉,此地址上没有任何内容。” - JohnyL
@JohnyL,我已经更新了帖子。请尝试使用2.1和2.2步骤。可能需要编辑路由。 - Den
1
@JohnyL 你把新文件的构建操作(Build Action)改成了Content吗?默认是None,如果不改就会出现这个错误。我花了几个小时才发现这是问题所在。 - Eugene Niemand
4
@model 只适用于 MVC 视图和 Razor 页面(不适用于 Blazor 页面)。参见 文档 - Peter L
1
扩展@PeterL所提到的,需要Microsoft.AspNetCore.Mvc.Core NuGet包才能使其正常运行。该包在Blazor WebAssembly项目中默认不包含。 - The Thirsty Ape

8

2
文档非常不好,不幸的是,也许你可以在回答中添加一个示例。 - OuttaSpaceTime
我知道文档可能不是最好的,但我并不认为它很差。方法都在那里,源代码也有注释。我还有一个示例项目,你可以在仓库中查看。 - revobtz
1
@revobtz,如果我必须查看源代码才能理解一个方法的作用,那么你的文档不仅是“糟糕”,而是根本不存在。摘要不是“文档”,它们只是程序员不被视为华而不实的代码猴子的最低要求。但是,那些只是“复制粘贴”方法或参数名称的摘要并不是摘要,而是一种恼人的东西。告诉我参数“timeout”是“超时”就等于告诉我“我不应该在100英尺内接近计算机”。 - Tessaract
@Tessaract,事情就是这样,如果你想改进存储库文档,可以来贡献。我正在进行一个新版本的net 6,以修复一些现有的错误,在不久的将来我会改进文档。我已经忙于工作很多年了,对于文档方面让你和其他人失望,我感到非常抱歉。 - revobtz

5

Eugene提出的解决方案有效,但有一个缺点。如果你尝试使用大文件进行操作,浏览器会在将blob下载到客户端时挂起。我发现的解决方案是稍微改变代码,并将文件存储在临时目录中,让服务器使用其机制来提供文件,而不是将其作为blob推送。

在服务器配置中添加:

app.UseStaticFiles();
app.UseStaticFiles(new StaticFileOptions
{
    FileProvider = new PhysicalFileProvider(
        Path.Combine(___someTempDirectoryLocation___, "downloads")),
    RequestPath = "/downloads"
});

这将在您的系统中的某个下载文件夹中添加静态链接。把您想要提供下载的任何文件放在那里。
接下来,您可以使用以下任一链接来访问该文件:http://pathToYourApplication/downloads/yourFileName 或使用主示例中的简化保存JavaScript。
function saveAsFile(filename) {
        var link = document.createElement('a');
        link.download = filename;
        link.href = "/downloads/" + filename;
        document.body.appendChild(link); // Needed for Firefox
        link.click();
        document.body.removeChild(link);
    }

这将把它推送到用户的浏览器中。

这只是在公共端点上提供文件。当试图动态生成内容时,它没有任何用处。 - Stijn Van Antwerpen
@StijnVanAntwerpen 不是这样的,当你将文件保存到静态文件后,你可以使用他提供的JavaScript代码使浏览器下载它。 - Chris Bordeman
@StijnVanAntwerpen 它适用于生成的内容。您可以创建一个操作来生成内容,将其写入预定义目录中的文件中,该目录在示例代码的前半部分中进行了配置。完成后,您可以使用“saveAsFile”代码将其推送到客户端,该代码使用JavaScript启动下载过程。我正在使用它来生成文件内容,我知道它有效。 - Aleksander Wisniewski

0

Eugene的答案出了一些问题,对我没有起作用。但是现在有官方文档可以告诉你如何做到这一点,非常相似,并且在我的Blazor Server应用程序中运行良好。

将以下JavaScript方法添加到您的_Host.cshtml文件中:

<script type="text/javascript">
    async function downloadFileFromStream(fileName, contentStreamReference) {
        const arrayBuffer = await contentStreamReference.arrayBuffer();
        const blob = new Blob([arrayBuffer]);
        const url = URL.createObjectURL(blob);
        triggerFileDownload(fileName, url);
        URL.revokeObjectURL(url);
    }

    function triggerFileDownload(fileName, url) {
        const anchorElement = document.createElement('a');
        anchorElement.href = url;

        if (fileName) {
            anchorElement.download = fileName;
        }

        anchorElement.click();
        anchorElement.remove();
    }
</script>

在您的 .razor 页面文件中添加以下内容:
@using System.IO
@inject IJSRuntime JS

<button @onclick="DownloadFileFromStream">
    Download File From Stream
</button>

@code {
    private Stream GetFileStream()
    {
        var randomBinaryData = new byte[50 * 1024];
        var fileStream = new MemoryStream(randomBinaryData);
        return fileStream;
    }
    
    private async Task DownloadFileFromStream()
    {
        var fileStream = GetFileStream();
        var fileName = "log.bin";
        using var streamRef = new DotNetStreamReference(stream: fileStream);
        await JS.InvokeVoidAsync("downloadFileFromStream", fileName, streamRef);
    }
}

@AndrewRondeau 这不是客户端吗?在 Blazor WebAssembly 中,这个工作非常完美。如果您阅读此帖子中链接的文档,您将看到无论是服务器端还是客户端,这都适用于 Blazor。 - The Thirsty Ape
抱歉,我把我的评论移到了这里:https://dev59.com/m1QJ5IYBdhLWcg3w_7AN#68471903 - Andrew Rondeau

0

我是这样做的:

在一个名为Controllers的文件夹中添加了一个名为DownloadController.cs的新控制器

[Controller, Microsoft.AspNetCore.Mvc.Route("/[controller]/[action]")]
public class DownloadController : Controller
{
    private readonly IDataCombinerService DataCombinerService;
    private readonly IDataLocatorService DataLocatorService;

    public DownloadController(IDataCombinerService dataCombinerService, IDataLocatorService dataLocatorService)
    {
        DataCombinerService = dataCombinerService;
        DataLocatorService = dataLocatorService;

    }

    [HttpGet]
    [ActionName("Accounts")]
    public async Task<IActionResult> Accounts()
    {
        var cts = new CancellationTokenSource();
        var Accounts = await DataCombinerService.CombineAccounts(await DataLocatorService.GetDataLocationsAsync(cts.Token), cts.Token);

        var json = JsonSerializer.SerializeToUtf8Bytes(Accounts, Accounts.GetType(), new JsonSerializerOptions(JsonSerializerDefaults.Web) { WriteIndented = true });
        var stream = new MemoryStream(json);

        var fResult = new FileStreamResult(stream, MediaTypeNames.Application.Json)
        {
            FileDownloadName = $"Account Export {DateTime.Now.ToString("yyyyMMdd")}.json"
        };

        return fResult;
    }

    [HttpGet]
    public IActionResult Index()
    {
        return View();
    }
}

严格来说,在这里并不需要异步操作,因为它不需要处理其他任何内容,但是当需要在屏幕上显示相同的结果时,该方法被使用。

然后在 Startup.cs 文件中。

app.UseEndpoints(endpoints =>

添加:

endpoints.MapControllerRoute(
    name: "default",
    defaults: new { action = "Index" },
    pattern: "{controller}/{action}");

    endpoints.MapControllers();

再次强调,默认值并非必需,它是标准的MVC控制器。

这样就可以像经典的MVC响应一样运行,因此您可以从任何喜欢的来源发送任何文件。最好有一个中间件服务来在视图和下载控制器之间保存临时数据,以便客户端下载相同的数据。


你如何在客户端下载文件?它来自服务器,这是一个允许你将文件下载到客户端的解决方案。我实际上做了与被接受的答案相同的事情,只是用了不同的方法。 - Tod
这不是客户端解决方案。请重新阅读问题。 - Andrew Rondeau
问题明确指出了"SPA"(单页应用程序)和"webassembly"。作者希望在浏览器中完全生成文件,而不是下载它。(即像 https://canvaspaint.org/ 这样让您在不调用服务器的情况下保存 PNG 文件。按 F12,选择网络选项卡,然后转到文件->保存。没有 PNG 被下载,全部在浏览器中生成。) - Andrew Rondeau

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接