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

C#WebWebassemblyBlazor

C# Problem Overview


I want to have a SPA that's doing all the work client side and even generating some graphs/visuals.

I'd like to be able to have the user click buttons and save the visuals, tables, and other things from the page (that are both seen and not seen, so right click save or copy/paste is not always an option).

How do I call a function from the webassembly/blazor library, get it's results and save it as a file on the client side?

the idea is something like this...?

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..?)
}

}

C# Solutions


Solution 1 - C#

Creator of Blazor Steve Sanderson used JavaScript interop for similar task during one of his last presentations.

You can find example on BlazorExcelSpreadsheet

Solution includes three parts:

  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# interop wrapper

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

  3. Call from your component

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

  4. You can see it an action in Blazor Fiddle

Solution 2 - C#

  1. Add a link

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

  1. Add Razor Page with a route
    2.1. Create Razor page 'Download.cshtml' or another name... 'PewPew.cshtml'... does not matter
    2.2. Put the next code in the created page
    @page "/download"
    @model MyNamespace.DownloadModel
  2. Edit Download.cshtml.cs file
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);
    }
}

Solution 3 - C#

I created a repository and nuget package which solves and simplifies this issue please take a look: https://github.com/arivera12/BlazorDownloadFile

Solution 4 - C#

The solution proposed by Eugene work, but have one drawback. If you try to do it with large files the browser hangs while downloading the blob to the client side. Solution I have found is to change that code a bit and store files in temporary directory and let the server use its mechanics for serving files instead of pushing it as a blob.

In the server configuration add :

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

This will add static link to a download folder somewhere on your system. Put any files you want to be available for download in there.

Next You can use either link to that file using http://pathToYourApplication/downloads/yourFileName or use simplified save javascript from main example:

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

which will push it to user browser for you.

Solution 5 - C#

I did it thus:

Added a new DownloadController.cs to a folder called Controllers

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

Strictly speaking async isn't required here as it doesn't need to process anything else, but that method is used to display the same results on screen when it is.

Then inside Startup.cs

app.UseEndpoints(endpoints =>

add:

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

    endpoints.MapControllers();

Again the defaults isn't strictly speaking required, it's a standard MVC Controller.

This then functions just like a classic MVC response, so you can send back any files, from any source you like. It may be helpful to have a middleware service to hold temporary data between the view and the downloader controller so the client is downloading the same data.

Solution 6 - C#

Eugene's answer didn't work for me for some reason, but there is now official documentation on how to do this, which is very similar and works well in my Blazor Server app.

Add these JavaScript methods to your _Host.cshtml file:

<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>

In your .razor page file, add:

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

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
Questionshawn.mekView Question on Stackoverflow
Solution 1 - C#Eugene ShmakovView Answer on Stackoverflow
Solution 2 - C#DenView Answer on Stackoverflow
Solution 3 - C#revobtzView Answer on Stackoverflow
Solution 4 - C#Aleksander WisniewskiView Answer on Stackoverflow
Solution 5 - C#TodView Answer on Stackoverflow
Solution 6 - C#CocolateIcecreamView Answer on Stackoverflow