69

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

}
General Grievance
  • 4,555
  • 31
  • 31
  • 45
shawn.mek
  • 1,195
  • 1
  • 11
  • 17
  • When saving files, do you mean to tell browsers to "download/save as" the file you've generated? – InDieTasten Oct 06 '18 at 22:33
  • Yes. That's what I mean. – shawn.mek Oct 07 '18 at 01:00
  • 2
    I'm not too sure how much of it can be done using blazor right now, but the JS version of saving files can be seen [here](https://stackoverflow.com/a/18197341/3919195) – InDieTasten Oct 07 '18 at 09:17
  • I think that's a useful workaround to saving files (if it can't currently be done in Blazor), but the difficult part that I have (even when I go the JS route) is generating information client side using Blazor and getting it into some file for download. – shawn.mek Oct 07 '18 at 15:27
  • byte as base64 string is not the same as a binary file. – MPinello Aug 01 '20 at 01:01

6 Answers6

84

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

You can see it an action in Blazor Fiddle

hultqvist
  • 17,451
  • 15
  • 64
  • 101
Eugene Shmakov
  • 942
  • 1
  • 7
  • 6
19
  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);
    }
}
Den
  • 397
  • 3
  • 4
  • Alas, this didn't work for me. I get `Sorry, there's nothing at this address.`. – JohnyL Jan 22 '20 at 10:14
  • @JohnyL, I've udpated the post. Plz try to use steps 2.1 and 2.2. Probably, you need to edit the route – Den Jan 23 '20 at 15:20
  • 1
    @JohnyL Did you change the Build Action of the new file to Content? Default is None the you will get this error. Took me a couple of hours to find that this was the issue. – Eugene Niemand Jul 29 '20 at 23:10
  • 4
    `@model` only applies to MVC views and Razor Pages (not Blazor pages). See [doc'n](https://learn.microsoft.com/en-us/aspnet/core/mvc/views/razor?view=aspnetcore-5.0#model) – Peter L Apr 28 '21 at 05:08
  • Extending what @PeterL mentioned, the `Microsoft.AspNetCore.Mvc.Core` NuGet package is required for this to function. That package is not included in Blazor WebAssembly projects by default. – The Thirsty Ape Jun 22 '22 at 17:38
8

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

revobtz
  • 606
  • 10
  • 12
  • 2
    the documentation is rather poor sadly, maybe you could add an example to your answer. – OuttaSpaceTime May 20 '21 at 06:01
  • I know documentation is not the best but I don't consider it poorly. The methods are there and the source code is documented. I also have a sample project you can take a look in the repo. – revobtz May 20 '21 at 14:40
  • @revobtz, if I have to look in the source code to understand what a method does, then your documentation isn't just "poorly", it is nonexistent. Summaries aren't "documentation" they are the bare minimum for a programmer to NOT be considered a glorified code monkey. But summaries which are just "the method or parameter name copy-pasted" aren't a summary they are an annoyance. Telling me the parameter "timeout" is "the timeout" is telling me "I shouldn't be allowed within 100 feet of a computer". – Tessaract Nov 04 '21 at 14:17
  • @Tessaract Well it is what it is, you can come and contribute if you want to improve the repo docs, I am doing a newer version to net 6 at this moment to fix some existent bugs, in a near future I will improve docs. I have been very busy working for very long years, sorry for disappoint you and everyone else in terms of the docs. – revobtz Nov 04 '21 at 17:29
3

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.

  • This is just serving files on a public endpoint. It is of no use when trying to generate content on the fly. – Stijn Van Antwerpen Jun 24 '21 at 18:07
  • @StijnVanAntwerpen Not true, after you save the file to the static files, you use the javascript he provided to make the browser download it. – Chris Bordeman Jul 12 '21 at 09:33
  • @StijnVanAntwerpen It works for generated content. You create an action that generates the content, writes it into file in the pre-defined directory that was configured in first half of the sample code. Once that is done, you push it to the client using "saveAsFile" code which uses javascript to start the download process. I am using it for generated file content, I know it works. – Aleksander Wisniewski Jul 13 '21 at 10:13
0

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.

Tod
  • 2,070
  • 21
  • 27
  • How do you download a file client side? It comes from the server, this is a solution to allow you to download a file to the client-side. I literally did the same thing as the accepted answer, just in a different way. – Tod Feb 04 '22 at 23:46
  • This is not a client-side solution. Please re-read the question. – Andrew Rondeau Jun 25 '22 at 19:11
  • The question specifically states "SPA" (Single Page Application) and "webassembly". The author wants to 100% generate the file in the browser, not download it. (IE, like how https://canvaspaint.org/ lets you save a png file without making a call to the server. Push F12, select the network tab, and then go file->save. No png is downloaded, it's all generated in the browser.) – Andrew Rondeau Jun 30 '22 at 01:56
0

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);
    }
}
  • @AndrewRondeau In what way is this not client side? This works perfectly in Blazor WebAssembly. If you read the documentation linked on this post you will see this is for Blazor regardless of server or client side. – The Thirsty Ape Jun 22 '22 at 19:20
  • Apologies, I moved my comment to: https://stackoverflow.com/a/68471903/1711103 – Andrew Rondeau Jun 25 '22 at 19:11