We will continue to enhance 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 (this article), we will add functionality to copy the converted text to the clipboard, and load the JSON input from a local file.
- In part 3, we will add functionality to save the converted text as a local CSV file.
In this article, we will learn how to call JavaScript code from our C# view model class to access the browser’s Clipboard integration. Then, we will use the InputFile
component to find a local JSON file and load it into the SourceText
property.
We will continue to use the JSON-CSV converter page and classes from Part 1 (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 1 into Visual Studio.
Copy To Clipboard
Copy to clipboard is useful functionality in many web applications. in our scenarios, we will add a copy function that takes the ConvertedText
and copies it to the Clipboard.
However, in Blazor we cannot directly call the .NET Clipboard methods because we are running in the context of a browser host. So we need to use some JavaScript to communicate with the browser’s clipboard integration. Blazor allows us to call JavaScript from our C# code using its JavaScript interop layer.
1. Create the ./wwwroot/js folder in the Blazor.AppIdeas.Converters project.
2. Let’s create the Clipboard.js file in that new folder.
window.clipboardCopy = {
copyText: function (text) {
navigator.clipboard.writeText(text).then(function () {
alert("Copied to clipboard!");
})
.catch(function (error) {
alert(error);
});
},
copyElement: function (codeElement) {
navigator.clipboard.writeText(codeElement.textContent).then(function () {
alert("Copied to clipboard!");
})
.catch(function (error) {
alert(error);
});
}
};
We defined two functions: copyText
and copyElement
in this JavaScript, though we will only use copyText
for this article. Both methods make use of the cross-browser Clipboard API. The Clipboard API provides the ability to respond to clipboard commands (cut, copy, and paste) as well as to asynchronously read from and write to the system clipboard. It supports text as well as object representations of clipboard items. For our example, we will only use the write methods.
copyText
takes a string as input and calls the browser’sclipboard.writeText
method with the text we wish to copy, which writes the specified text to the system clipboard. Finally, it shows an alert stating whether the copy succeeded or failed.copyElement
takes an element parameter. It then retrieves that element’s textual representation, using the element’stextContent
attribute. And, it uses that in theclipboard.writeText
method call. This method is helpful when trying to copy some HTML content and markup, like a table.
3. We need to include this JavaScript file in our index.html file, so that it is accessible in our Blazor code. We do that by adding a script link (line #26):
<!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>
</body>
</html>
4. We make several changes to the JsonCsvConverterViewModel
to provide a Copy operation (in the ViewModels folder).
using Blazor.AppIdeas.Converters.Models;
using Blazor.AppIdeas.Converters.Services;
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.JSInterop;
using System;
using System.Threading.Tasks;
namespace Blazor.AppIdeas.Converters.ViewModels
{
public class JsonCsvConverterViewModel
{
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 = string.Empty;
ConvertedText = string.Empty;
ErrorMessage = null;
}
public async Task Copy() => await _jsRuntime.InvokeVoidAsync(
"clipboardCopy.copyText",
ConvertedText);
}
- First, we define two new external dependencies (lines #12-13):
IJSRuntime
andIBrowserFileAdapter
(we will discuss this interface in the next section). TheIJSRuntime
interface is an abstraction that Blazor provides to call into JavaScript code from .NET. This interface is already registered in the app’s dependency injection container, so we can just inject it into our view model constructor. - Then, we define a constructor that accepts those two dependencies (lines #15-23). We use constructor injection to pass these objects in. And we save these objects in our member variables to use later.
- Finally, we implement the
Copy
method (lines #58-60) that theJsonCsvConverter
page will bind to. We call theIJSRuntime.InvokeVoidAsync
method to call a JavaScript method with no return parameter.InvokeVoidAsync
‘s first parameter is a string that represents the function name to call. “clipboardCopy.copyText” is the fully qualified name for the JavaScript method we wrote earlier.- The second parameter (the
ConvertedText
) is the only parameter passed into thecopyText
Javascript function.
- That’s all we need to call the JavaScript function and copy our results.
5. Update the JsonCsvConverter
page to bind the ‘Copy’ button onclick
event to the JsonCsvConverterViewModel.Copy
method.
<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>
Those are all of the changes that we need to support to copy our conversion results to the system clipboard.
Load JSON File
Typing long strings of JSON by hand can be boring and error-prone, so we want to allow users to load a JSON file into our SourceText
property in order to use it for conversion. We will do this by using the Blazor InputFile
component to display the system ‘File Open’ dialog. Then, when the user selects a file, we will load that local file as text into the view model’s SourceText
.
If we review the InputFile
and the event arguments it uses (InputFileChangeEventArgs
), we will quickly see that we need to work with an IBrowserFile
object that represents the file for us to read. After thinking through the design, we will want to abstract the system interface using the Adapter pattern, so that we insulate ourselves a bit from changes to IBrowserFile
and make it easier to test our application by mocking the adapter.
1. We will create the IBrowseFileAdapter
interface in the Blazor.AppIdeas.Converters project and Services folder. For this article the adapter interface has a single ReadTextAsync
method.
using Microsoft.AspNetCore.Components.Forms;
using System.Threading.Tasks;
namespace Blazor.AppIdeas.Converters.Services
{
public interface IBrowserFileAdapter
{
Task<string> ReadTextAsync(IBrowserFile file);
}
}
2. Create the BrowserFileAdapter
class also in the Blazor.AppIdeas.Converters project and Services folder.
using Microsoft.AspNetCore.Components.Forms;
using System;
using System.IO;
using System.Text;
using System.Threading.Tasks;
namespace Blazor.AppIdeas.Converters.Services
{
public class BrowserFileAdapter : IBrowserFileAdapter
{
public async Task<string> ReadTextAsync(IBrowserFile browserFile)
{
_ = browserFile ?? throw new ArgumentNullException(nameof(browserFile));
using var file = browserFile.OpenReadStream();
using var reader = new StreamReader(file, Encoding.UTF8);
return await reader.ReadToEndAsync().ConfigureAwait(false);
}
}
}
- First, we ensure the specified
browserFile
is not null (line #13). - Then, we open a readable stream from the
IBrowserFile
. - Followed by creating a
StreamReader
, which is able to read through a text file with UTF-8 encoding. If the file does not conform to this format, then the read operation will fail. - Then, we read the whole file as a string and return it from this method.
- Finally, we surface all exceptions from this class and allow the view model to handle them.
3. Update the JsonCsvConverterViewModel
class (in the ViewModels folder) to expose the OpenInputFile
operation. The OpenInputFile
method is similar to other view model methods and provides the target for our page’s event binding.
using Blazor.AppIdeas.Converters.Models;
using Blazor.AppIdeas.Converters.Services;
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.JSInterop;
using System;
using System.Threading.Tasks;
namespace Blazor.AppIdeas.Converters.ViewModels
{
public class JsonCsvConverterViewModel
{
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 = string.Empty;
ConvertedText = string.Empty;
ErrorMessage = null;
}
public async Task Copy() => await _jsRuntime.InvokeVoidAsync(
"clipboardCopy.copyText",
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}";
}
}
}
}
The OpenInputFile
method (lines #62-83) ensures that there is only a single file selected by the user, handles any errors that occur along they way, and calls the IBrowserFileAdapter.ReadTextAsync
method (described above) to load the data from the selected file.
4. Update the JsonCsvConverter
page (in the Pages folder) to use the InputFile
component.
@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%"
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">
<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>
We changed the original button on this page to actually use the InputFile
component instead (lines #12-18). To help customize this component, we wrap the elements in a div
with the button-wrap
BootStrap class. Then, the label
that gets styled as a custom button. And, we bind the component’s OnChange
event to the OpenInputFile
method above.
5. Add the IBrowserFileAdapter
service to the app’s DI container (line #29) with the following changes to the Program.cs file, so that it can be injected into our view model constructor.
using Blazor.AppIdeas.Converters.Services;
using Blazor.AppIdeas.Converters.ViewModels;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Diagnostics.CodeAnalysis;
using System.Net.Http;
using System.Threading.Tasks;
namespace Blazor.AppIdeas.Converters
{
[ExcludeFromCodeCoverage]
public class Program
{
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
var httpClient = new HttpClient
{
BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)
};
builder.Services.AddScoped(sp => httpClient);
// add services and view models to DI container.
builder.Services.AddSingleton<ICurrencyServiceClient>(
sp => new CurrencyServiceClient(httpClient));
builder.Services.AddSingleton<IBrowserFileAdapter, BrowserFileAdapter>();
builder.Services.AddTransient<RomanDecimalConverter>();
builder.Services.AddTransient<NumberConverterViewModel>();
builder.Services.AddTransient<DollarCentsConverterViewModel>();
builder.Services.AddTransient<CurrencyConverterViewModel>();
builder.Services.AddTransient<JsonCsvConverterViewModel>();
await builder.Build().RunAsync();
}
}
}
6. We want to customize the look of the InputFile
element, so let’s make some styling changes to the app.css file (in the ./wwwroot/css folder).
@import url('open-iconic/font/css/open-iconic-bootstrap.min.css');
html, body {
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
}
a, .btn-link {
color: #0366d6;
}
.btn-primary {
color: #fff;
background-color: #1b6ec2;
border-color: #1861ac;
}
.content {
padding-top: 1.1rem;
}
.valid.modified:not([type=checkbox]) {
outline: 1px solid #26b050;
}
.invalid {
outline: 1px solid red;
}
.validation-message {
color: red;
}
#blazor-error-ui {
background: lightyellow;
bottom: 0;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
display: none;
left: 0;
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
position: fixed;
width: 100%;
z-index: 1000;
}
#blazor-error-ui .dismiss {
cursor: pointer;
position: absolute;
right: 0.75rem;
top: 0.5rem;
}
.container {
border-style: solid;
border-radius: 15px;
border-width: 1px;
border-color: darkgreen;
}
hr {
border-top: 1px solid darkgreen;
}
select {
border: 1px solid lightgray;
border-radius: 5px;
height: 36px;
}
/*----------------------- Input File styles --------------------------*/
input[type="file"] {
position: absolute;
z-index: -1;
top: 4px;
left: 4px;
font-size: 17px;
color: #b8b8b8;
}
.button-wrap {
position: relative;
}
.button {
display: inline-block;
padding: 8px 12px;
cursor: pointer;
border-radius: 5px;
background-color: gray;
font-size: 16px;
font-weight: bold;
color: #fff;
}
We define these custom styles (lines #70-94) for the InputFile
element to make it a flat button with an icon and text to match the other buttons in our app.
Finally, with all of these code changes, we can build and run our application again (Ctrl + F5). We can test drive our file load functionality by clicking the ‘Load File’ button.

Then, selecting a JSON file on your local drive to open (create a simple test JSON file, if you don’t have any handy).

Next, click the ‘To CSV’ button to perform the conversion.

Finally, click the ‘Copy’ button to copy the converted text to the system clipboard and show all of the features added in this article.

In this article, we learned:
- To use
IJSRuntime
to invoke JavaScript functions from within our .NET code. - How to inject the
IJSRuntime
service into our view model constructor. - About the Clipboard API supported by most major browser versions.
- To use the
InputFile
component to load the system’s ‘File Open’ dialog. - To use the
IBrowserFile
service to open and read local system files. - How to customize the look of the
<input type="file>
element.
One thought on “App-Idea 6: JSON to CSV Converter (Part 2)”