In this article, we will wrap up the JSON to CSV Converter, which allows users to enter text in a simplified JSON format and converts it into a comma-separated value (CSV) format. This app-idea has more complex requirements, so we will cover this application in 3 parts:
- In part 1, we focused on the basic conversion functionality that allowed users to type in JSON text and convert it to CSV format.
- In part 2, we added functionality to copy the converted text to the clipboard, and load the JSON input from a local file.
- In part 3 (this article), we will add functionality to save the converted text as a local CSV file.
In this article, we will add the ability to save the converted text to a local CSV file. This includes C# code to perform the operation and then create the file download via JavaScript interoperability.
We will continue to use the Json-Csv converter page and classes from Part 2 (please read that article first). You can find the source for the Blazor.AppIdeas.Converters on GitHub. And the running sample of the Blazor app online.
We will start by loading Blazor.AppIdeas.Converters solution from Part 2 into Visual Studio.
File Download JavaScript
To create a file as a download, we need to use JavaScript to do this via the browser. So, let’s create a new JavaScript file called ‘File.js’ in the Blazor.AppIdeas.Converters project and ‘./wwwroot/js’ folder.
window.file = {
saveAsFile: function (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);
}
}
The saveAsFile
function takes a filename and the data to be saved in base64 encoding. This script:
- Creates a link with the download filename.
- The link href will contain the data for the file.
- Then, we simulate a click on the link to start the file download process.
- The browser will then show its download UI and save the file to the Downloads folder.
- When this process completes, we remove the link we just created.
Next, we need to add a script link to this JavaScript file (line #27) to the main index.html file (in the wwwroot folder), so that the JavaScript function is available to our application.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title>Blazor AppIdeas - Converters</title>
<base href="/" />
<link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
<link href="css/app.css" rel="stylesheet" />
<link href="Blazor.AppIdeas.Converters.styles.css" rel="stylesheet" />
<link href="manifest.json" rel="manifest" />
<link rel="apple-touch-icon" sizes="512x512" href="icon-512.png" />
</head>
<body>
<div id="app">Loading...</div>
<div id="blazor-error-ui">
An unhandled error has occurred.
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>
<script src="_framework/blazor.webassembly.js"></script>
<script>navigator.serviceWorker.register('service-worker.js');</script>
<script src="js/Clipboard.js"></script>
<script src="js/File.js"></script>
</body>
</html>
Update BrowserFileAdapter
Since we already have the BrowserFileAdapter
class that loads data from local files, this class is a great place to our file save and download functionality as well. Let’s start by updating the IBrowserFileAdapter
interface (in the Services folder).
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.JSInterop;
using System.Threading.Tasks;
namespace Blazor.AppIdeas.Converters.Services
{
public interface IBrowserFileAdapter
{
public enum FileType
{
JSON = 0,
CSV = 1
}
Task<string> ReadTextAsync(IBrowserFile file);
Task SaveTextAsAsync(IJSRuntime js, string filename, FileType fileType, string data);
}
}
First, we define a FileType
enum for the two file types that we will support: JSON and CSV. Then, we define the SaveTextAsAsync
method to manage the save and download operation.
Then, we need to implement that method in the BrowserFileAdapter
class.
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.JSInterop;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Blazor.AppIdeas.Converters.Services
{
public class BrowserFileAdapter : IBrowserFileAdapter
{
private const string _jsSaveAsFileMethod = "file.saveAsFile";
private static readonly IList<string> _supportedMimeTypes = new List<string>
{
"application/json",
"text/csv"
};
public async Task<string> ReadTextAsync(IBrowserFile browserFile)
{
_ = browserFile ?? throw new ArgumentNullException(nameof(browserFile));
if (!_supportedMimeTypes.Contains(browserFile.ContentType))
{
throw new NotSupportedException(
$"File type ({browserFile.ContentType}) is not supported.");
}
using var file = browserFile.OpenReadStream();
using var reader = new StreamReader(file, Encoding.UTF8);
return await reader.ReadToEndAsync().ConfigureAwait(false);
}
public async Task SaveTextAsAsync(
IJSRuntime jsRuntime,
string filename,
IBrowserFileAdapter.FileType fileType,
string data)
{
_ = jsRuntime ?? throw new ArgumentNullException(nameof(jsRuntime));
if (string.IsNullOrEmpty(filename))
throw new ArgumentNullException(nameof(filename));
if (string.IsNullOrEmpty(data))
throw new ArgumentNullException(nameof(data));
var buffer = Encoding.UTF8.GetBytes(data);
await jsRuntime.InvokeVoidAsync(
_jsSaveAsFileMethod,
CalculateFullFileName(filename, fileType),
Convert.ToBase64String(buffer));
}
private string CalculateFullFileName(
string filename,
IBrowserFileAdapter.FileType fileType)
{
var mimeType = _supportedMimeTypes[(int)fileType];
var extension = mimeType.Split("/").Last();
return $"{filename}.{extension}";
}
}
}
First, we define the saveAsFile
method name as a constant (line #14).
Then, we implement the SaveTextAsAsync
method (lines #37-54) to:
- Validate the input parameters and throw exceptions for unexpected values.
- Convert the
data
parameter from string to byte array, which is then converted to a base64 string. This is the format that the JavaScript function is expecting for the output file. - Calculate the full filename based on the specified base name and the
fileType
. - Finally, we call the
IJSRuntime.InvokeVoidAsync
method with all of this data to invoke the ‘file.saveAsFile’ function in our JavaScript above.
Finally, the CalculateFullFileName
helper method creates the full filename with the appropriate extension based on the specified fileType
and defined mime types.
JsonCsvConverter View Model
With our service method in place, we can add the DownloadConvertedText
operation to the JsonCsvConverterViewModel
. Let’s change the JsonCsvConverterViewModel
(in the Blazor.AppIdeas.Converters project and ViewModels folder) to the following:
using Blazor.AppIdeas.Converters.Models;
using Blazor.AppIdeas.Converters.Services;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.JSInterop;
using System;
using System.Threading.Tasks;
namespace Blazor.AppIdeas.Converters.ViewModels
{
public class JsonCsvConverterViewModel
{
private const string _jsCopyTextMethod = "clipboardCopy.copyText";
private const string _convertedTextFilename = "ConvertedText";
private readonly IJSRuntime _jsRuntime;
private readonly IBrowserFileAdapter _browserFileAdapter;
public JsonCsvConverterViewModel(
IJSRuntime jsRuntime, IBrowserFileAdapter fileAdapter)
{
_jsRuntime = jsRuntime ??
throw new ArgumentNullException(nameof(jsRuntime));
_browserFileAdapter = fileAdapter ??
throw new ArgumentNullException(nameof(fileAdapter));
}
public string SourceText { get; set; }
public string ConvertedText { get; set; }
public bool IsConvertedTextEmpty => string.IsNullOrEmpty(ConvertedText);
public string ErrorMessage { get; private set; }
public bool HasError => !string.IsNullOrEmpty(ErrorMessage);
public void ConvertToCsv()
{
try
{
ErrorMessage = null;
var converter = new TextConverter(SourceText);
ConvertedText = converter.JsonToCsv();
}
catch (Exception ex)
{
ErrorMessage = $"Cannot parse source text. {ex.Message}";
ConvertedText = null;
}
}
public void ClearAll()
{
SourceText = null;
ConvertedText = null;
ErrorMessage = null;
}
public async Task Copy() => await _jsRuntime.InvokeVoidAsync(
_jsCopyTextMethod,
ConvertedText);
public async Task OpenInputFile(InputFileChangeEventArgs e)
{
try
{
ErrorMessage = null;
_ = e ?? throw new ArgumentNullException(nameof(e));
if (e.FileCount > 1)
{
ErrorMessage = "Application does not support multiple file selection.";
}
else
{
SourceText = await _browserFileAdapter.ReadTextAsync(e.File)
.ConfigureAwait(false);
}
}
catch (Exception ex)
{
ErrorMessage = $"Cannot read file '{e?.File.Name}'. {ex.Message}";
}
}
public async Task DownloadConvertedText()
{
try
{
ErrorMessage = null;
await _browserFileAdapter.SaveTextAsAsync(
_jsRuntime,
_convertedTextFilename,
IBrowserFileAdapter.FileType.CSV,
ConvertedText)
.ConfigureAwait(false);
}
catch (Exception ex)
{
ErrorMessage = $"Cannot download converted text file. {ex.Message}";
}
}
}
}
As with our other view model operations, the DownloadConvertedText
method (lines #88-105) is responsible for:
- Calling the
IBrowserFileAdatper.SaveTextAsAsync
method with the appropriate inputs (the global instance of theIJSRuntime
, the base name for the converted text file, the desired file type, and the converted text). - And, handling any exceptions that happen throughout the process to show the user an appropriate error message.
JsonCsvConverter Page
Finally, it’s just a minor change to the JsonCsvConvert
page (in the Pages folder) to bind the ‘Download File’ button’s onclick
event to the JsonCsvConverterViewModel.DownloadConvertedText
operation to hook up our UI element with our view model operation.
@page "/json-csv-convert"
@inject JsonCsvConverterViewModel vm
<div class="col-12 mt-3 pb-2 mb-3 container">
<h3 class="mt-3">JSON-CSV Converter</h3>
<hr />
<EditForm class="form-row" Model=@vm>
<div class="form-group col-lg-5 col-md-12">
<InputTextArea id="convert-from-value" class="form-control"
placeholder="Type JSON or CSV..." rows="8"
@bind-Value=vm.SourceText />
<div class="button-wrap">
<label class="btn btn-secondary" for="convert-open-file">
<span class="oi oi-cloud-upload" aria-hidden="true" /> Load File
</label>
<InputFile id="convert-open-file" style="width: 100%"
@ref="fileRef" OnChange="vm.OpenInputFile"/>
</div>
</div>
<div class="col-lg-2 col-md-12">
<button id="btn-convert-csv"
class="btn btn-primary col-lg-10 offset-lg-1 col-6 mb-lg-1 mb-3"
@onclick="vm.ConvertToCsv">
To CSV
</button>
<button id="btn-clear"
class="btn btn-primary col-lg-10 offset-lg-1 col-5 mb-lg-1 mb-3"
@onclick="vm.ClearAll">
Clear
</button>
</div>
<div class="form-group col-lg-5 col-12">
<InputTextArea id="converted-value" class="form-control" rows="8"
readonly @bind-Value=vm.ConvertedText />
<div>
<button id="download-file" class="btn btn-outline-secondary text-nowrap"
style="width: 52%" disabled="@vm.IsConvertedTextEmpty"
@onclick="vm.DownloadConvertedText">
<span class="oi oi-cloud-download" aria-hidden="true" />
Download File
</button>
<button id="clipboard-copy" class="btn btn-outline-secondary"
style="width: 46.5%; margin-left: -2px"
disabled="@vm.IsConvertedTextEmpty" @onclick="vm.Copy">
<span class="oi oi-clipboard" aria-hidden="true" />
Copy
</button>
</div>
</div>
@if (vm.HasError)
{
<div class="alert alert-danger col-12 ml-1">
<strong>Error:</strong> @vm.ErrorMessage
</div>
}
</EditForm>
</div>
@code {
public InputFile fileRef;
}
All of the required changes are done, so we can build and run (Ctrl + F5) our converter application again to see the results. If we convert some simple JSON and click the ‘Download File’ button, we should see the following download UI (in the Edge browser).

And by saving this file with the CSV extension, when we open the downloaded file, it will open with the app registered for that extension. In my case, it opens the CSV file in Excel.

In conclusion, we built a fully functional JSON to CSV converter with a lot of helpful functionality to load input files, copy the converted results to the clipboard, and save those same results to a CSV file. In this particular lesson, we learned to:
- Create and register JavaScript in our project.
- Use JavaScript to build a download URL to save client-side data.
- Use the
IJSRuntime
object to call JavaScript code in our project. - Convert string encoding to UTF8 and base64.
One thought on “App-Idea 6: JSON to CSV Converter (Part 3)”