DEV Community

Cover image for Implement the "download file" feature on a Blazor WebAssembly app
jsakamoto
jsakamoto

Posted on

Implement the "download file" feature on a Blazor WebAssembly app

If we want to implement the "download file" feature (please imagine a picture that was posted on Facebook can be downloadable) on your Blazor WebAssembly app project, how can we implement that feature?

Method 1 - make <a> tag with download attribute, simply - but it doesn't work as expected.

At first, I came up with a simple solution that is making <a> tag linked with the URL of the file, and add download attribute to that tag.

<a href="api/pictures/1" download="foo.jpeg">
  download
</a>

Unfortunately, this solution doesn't work as expected.

Please remind that the runtime of Blazor intercepts all of URL navigations, and transfer it to rendering a component which has a matching route URL.

If there is no matching route URL, the URL navigation is passed through to the web browser.

However, in this scenario, Blazor doesn't consider the download attribute!

Therefore, if the user clicks this <a> tag, the user will see the picture file inside the browser window, downloading the file is not started.

fig.1

To be exact, if the content type of the linked file is not well known for the web browser, it will be started the download.

But even if this scenario, this behavior will also give us a bad experience, because the URL of the address bar will be changed from the URL of the current page to the URL of the file.

In addtion, the download file name that is specified in download attribute is ignored.

Method 2 - make <a> tag with download attribute, and ... target="_top" attribute!

To avoid the interception of URL navigation by Blazor, we can use a hack.

The hack is, adding target="_top" attribute to the <a> tag.

<a href="api/pictures/1" download="foo.jpeg" target="_top">
  download
</a>

Blazor doesn't intercept URL navigation in some cases, and adding target="_top" attribute is one of those cases.

This solution is very simple, and works fine as expected, as perfect.

Method 3 - use JavaScript helper function

By the way, in some cases, you may want to confirm to the user before downloading started.

To implement this, we have to use the JavaScript helper function.

At first, implement the JavaScript function that receives the download URL and file name, and that downloads form the URL to a file with a specified file name, like this.

(Aside: I always use TypeScript to implement JavaScript code.)

// helper.ts 
// (this TypeScript source file will be trancepiled to "helper.js")

function downloadFromUrl(options: { url: string, fileName?: string }): void {
  const anchorElement = document.createElement('a');
  anchorElement.href = options.url;
  anchorElement.download = options.fileName ?? '';
  anchorElement.click();
  anchorElement.remove();
}

This JavaScript code is a well known and classical technic.

After implementing this JavaScript code, don't forget to include it from the HTML document.

  ...
  <script src="_framework/blazor.webassembly.js"></script>
  <script src="script/helper.js"></script>
</body>
</html>

At last, just invoke that JavaScript helper function from C# code in Blazor app.

@inject IJSRuntime JSRuntime
...
<button @onclick="OnClickDownloadButton">
  download
</button>
...
@code {
  private async Task OnClickDownloadButton()
  {
    var res = await JSRuntime.InvokeAsync<bool>("confirm", "Are you sure?");
    if (res == false) return;

    await JSRuntime.InvokeVoidAsync(
      "downloadFromUrl",
      new {Url = "api/pictures/1", FileName = "foo.jpeg"});
  }
}

Method 4 - download from in-memory byte array inside of Blazor app

In some cases, the web browser can not download from the resource URL directly.

For example:

  • The resource URL is protected by token-based (non cookie-based) authorization, therefore it can be retrieved only from HttpClient inside the Blazor app.
  • The contents to download is generated by computing in C# code in Blazor app.

In these cases, we have to make in-memory byte array contents to be downloadable.

To do this, we can use "object URL" feature of Web browser and JavaScript engine on it.

URL.createObjectURL() API allows us to make a valid URL (it's called "object URL") that is linked to the "Blob" object.

By the way, there is a big problem.

No way transfers the byte array from inside of the Blazor app to JavaScript natively. Instead, the byte array inside of the Blazor app is converted base64 encoded string by JavaScript interop feature of the Blazor runtime, before pass it to a JavaScript function.

After thinking these considerations points, I implemented the JavaScript helper function, like this:

// helper.ts
...

function downloadFromByteArray(options: { 
  byteArray: string, 
  fileName: string, 
  contentType: string
}): void {

  // Convert base64 string to numbers array.
  const numArray = atob(options.byteArray).split('').map(c => c.charCodeAt(0));

  // Convert numbers array to Uint8Array object.
  const uint8Array = new Uint8Array(numArray);

  // Wrap it by Blob object.
  const blob = new Blob([uint8Array], { type: options.contentType });

  // Create "object URL" that is linked to the Blob object.
  const url = URL.createObjectURL(blob);

  // Invoke download helper function that implemented in 
  // the earlier section of this article.
  downloadFromUrl({ url: url, fileName: options.fileName });

  // At last, release unused resources.
  URL.revokeObjectURL(url);
}

After implemented this JavaScript helper function, we can invoke it from C# code in the Blazor app, like this:

@inject HttpClient HttpClient
@inject IJSRuntime JSRuntime
...
<button @onclick="OnClickDownloadButton">
  download
</button>
...
@code {
  private async Task OnClickDownloadButton()
  {
    // Please imagine the situation that the API is protected by
    // token-based authorization (non cookie-based authorization).
    var bytes = await HttpClient.GetByteArrayAsync("api/pictures/1");

    await JSRuntime.InvokeVoidAsync(
      "downloadFromByteArray",
      new
      {
          ByteArray = bytes,
          FileName = "foo.jpeg",
          ContentType = "image/jpeg"
      });
  }
}

By using these implementations, we can make a byte array to downloadable.

However, please remember these implementations are very inefficient.

Because these implementations cause duplicating source byte array many times.

Please remind that:

  1. source byte array is encoded to base64 string. (source byte array is duplicated as another representation.)
  2. The base64 string is copied from Blazor runtime to JavaScript runtime. (source byte array is duplicated, again.)
  3. The base64 string is decoded to number array. (source byte array is duplicated as another representation, again.)
  4. (etc...)

Conclusion

I showed you how to implement the "download file" feature in a Blazor app in this article.

All of the sample code is published at the GitHub repository below.

If you know more efficiently implementation, please feel free to get in touch with me, and let's share the technic.

Happy coding! :)

Top comments (10)

Collapse
 
intermundos profile image
intermundos

Great list of hacks!

One thing I don't get - why would you want to use Blazor when at the end you write helpers in JavaScript and consume them inside Blazor? IMHO, Pointless technology that will never offer a thing beyond JavaScript.

Collapse
 
j_sakamoto profile image
jsakamoto

Are you saying that "Blazor is a pointless technology"!?

If you ask me, that's too short-sighted.

This article is not describing the whole of Blazor.
This article is only about "getting users to download content", which is a very limited scoped topic.

Blazor is undoubtedly one of the strongest options for implementing SPAs for those who benefit from the development environment surrounding C# and C#.
The advantages of Blazor need to be understood as an overall application development ecosystem, including robust and easily understood package dependency management, reuse of existing .NET library assets, etc.

It's true that in some cases, as discussed in this article, we may need the help of JavaScript to implement the "let the content download" feature.

But that doesn't detract from the value of Blazor.

The power of Blazor benefits us when implementing the main body of the application, other than the "make the content download" feature.

Writing a little bit of JavaScript to implement the "let the content download" feature is trivial compared to the development load of the entire application and the benefits that can be gained by using Blazor for this purpose.

Of course, for those not relying on C# and the development environment surrounding C#, there is no reason to use Blazor, nor is there any appeal.

Yes, Blazor is not beyond JavaScript overall.

But the value of Blazor is providing a powerful alternative for a part of a group of developers (such as depend on C# strongly) for whom SPA development in only JavaScript (TypeScript) would be inefficient.
For these SPA developer groups, Blazor may be beyond JavaScript.

Collapse
 
intermundos profile image
intermundos

Shortsighted or not, yet, if you have to "go back" to JavaScript land and use it to complement Blazor for the missing functionality, in my shortsighted view, this is pretty inconvenient and does not provide much benefit. After all you can't write full SPA with Blazor alone and will have to mix and match things.

Again, I am not arguing that Blazor is a bad piece of technology, I am trying to understand why would one use it to build client side solutions knowing that, want it or not, you are bound to JS and other limitations coming along.

Thread Thread
 
j_sakamoto profile image
jsakamoto

you can't write full SPA with Blazor alone and will have to mix and match things.

That's right at all, and I agree.

But, I'm not sticking to using Blazor alone for implementing SPA, at all.

Again, (it looks like a point you are not trying to understand from previous my post, and it looks like a point we still disagree) the inconvenience I have to mix in with some JavaScript to implement a practical Blazor SPA is very small for me, compared to the benefits I get from using Blazor.

Regardless of how you feel about it, using Blazor in my work has allowed me to achieve higher quality and shorter development times than I've ever had with Angular for SPA development.

It is not a truth for everyone, but at least, it is true for me.

Of course, please don't forget that I'm developing with C# not only client-side but also server-side, and not an only web application but also console app and Windows GUI app.

This is a big reason why I can get big benefit by using Blazor when I implement SPA.

It provides high-level integration by using C# from database entities, server-side business logic, to client-side SPA.
Once I had annotated to a field of a model class to it should be "required field", it makes the database field is made "NOT NULL", server-side web API endpoint will check it has any values, and client-side Blazor will check the input field is not empty.

I can also write unit tests for client-side Blazor using the same unit test framework, test runner, technic, with what I always use for server-side.

Due to these backgrounds, I agree that the benefits are small if somebody using C# for the client-side only.

Again, I am not arguing that Blazor is a bad piece of technology

Sorry, I could not understand well the finer nuances you may have wanted to convey, at that time.
I felt what you said "pointless" is too harsh words for me.

I am trying to understand why would one use it to build client-side solutions knowing that, ...

To talk about that theme, using this comment field of this article is not appropriate, I think.
I hope you find any answers to that question in some days.

Collapse
 
serj profile image
Sergey-PrudentDev • Edited

This code no longer works as of .NET 6, to cover all versions, here's a quick and dirty fix, change these 2 lines (this is only tested in .NET 6):

// Convert base64 string to numbers array.
const numArray = atob(options.byteArray).split('').map(c => c.charCodeAt(0));

// Convert numbers array to Uint8Array object.
const uint8Array = new Uint8Array(numArray);
Enter fullscreen mode Exit fullscreen mode

to

var uint8Array;

try {
    // sometimes it comes across to JS as a byteArray already... a .NET 6 framework change
    uint8Array = new Uint8Array(options.byteArray);
} catch {
    // Convert base64 string to numbers array.
    var numArray = atob(options.byteArray).split('').map(c => c.charCodeAt(0));
    uint8Array = new Uint8Array(numArray);
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
abc_wendsss profile image
Wendy Wong

Thanks Sergey for sharing your thoughts and with the DEV community :)

Collapse
 
esben2000 profile image
esben2000

Method 4 will be faster in .NET 6. In .NET 6 the byte array is no longer converted to a base64 encoded string inside the Blazor app before passing it to a JavaScript function :-)
docs.microsoft.com/en-us/dotnet/co...

Collapse
 
Sloan, the sloth mascot
Comment deleted
Collapse
 
stevebroshar profile image
Steve Broshar

Wish I knew what that meant.

Collapse
 
adopilot profile image
Admir

Thank you on your post.
Any idea how to push browser to ask user where to save file, beside automatic start download