如何使用 Blazor 在客户端生成和保存文件?

How can one generate and save a file client side using Blazor?

我想要一个 SPA 来完成客户端的所有工作,甚至生成一些 graphs/visuals。

我希望能够让用户单击按钮并保存页面中的视觉效果、表格和其他内容(可见和不可见,所以右键单击保存或 copy/paste并不总是一个选项)。

如何调用 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..?)
}

}

Blazor 的创建者史蒂夫桑德森在他最近的一次演讲中使用 JavaScript 互操作来完成类似的任务。

您可以在 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. 添加一个link

<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);
    }
}

我创建了一个存储库和 nuget 包来解决和简化这个问题请看一看:https://github.com/arivera12/BlazorDownloadFile

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

在服务器配置中添加:

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

这会将静态 link 添加到系统某处的下载文件夹中。将您希望可供下载的任何文件放在那里。

Next 您可以使用 http://pathToYourApplication/downloads/yourFileName 对该文件使用 link 或使用主要示例中的简化保存 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);
    }

这将为您将其推送到用户浏览器。

我是这样做的:

向名为 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 响应一样运行,因此您可以从您喜欢的任何来源发回任何文件。使用中间件服务在视图和下载器控制器之间保存临时数据可能会有所帮助,因此客户端正在下载相同的数据。

Eugene 的回答出于某种原因对我不起作用,但现在 official documentation 介绍了如何执行此操作,这非常相似并且在我的 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);
    }
}