forked from sheetjs/docs.sheetjs.com
440 lines
13 KiB
Markdown
440 lines
13 KiB
Markdown
|
---
|
||
|
title: Sheets in Blazor Sites
|
||
|
sidebar_label: Blazor
|
||
|
pagination_prev: demos/index
|
||
|
pagination_next: demos/grid/index
|
||
|
sidebar_position: 9
|
||
|
---
|
||
|
|
||
|
import current from '/version.js';
|
||
|
import Tabs from '@theme/Tabs';
|
||
|
import TabItem from '@theme/TabItem';
|
||
|
import CodeBlock from '@theme/CodeBlock';
|
||
|
|
||
|
[Blazor](https://dotnet.microsoft.com/en-us/apps/aspnet/web-apps/blazor) is a
|
||
|
framework for building user interfaces using C#, HTML, JS and CSS.
|
||
|
|
||
|
[SheetJS](https://sheetjs.com) is a JavaScript library for reading and writing
|
||
|
data from spreadsheets.
|
||
|
|
||
|
This demo uses Blazor and SheetJS to process and generate spreadsheets. We'll
|
||
|
explore how to load SheetJS in Razor components and compare common state models
|
||
|
and data flow strategies.
|
||
|
|
||
|
:::caution Blazor support is considered experimental.
|
||
|
|
||
|
Great open source software grows with user tests and reports. Any issues should
|
||
|
be reported to the Blazor project for further diagnosis.
|
||
|
|
||
|
:::
|
||
|
|
||
|
:::danger Telemetry
|
||
|
|
||
|
**The `dotnet` command embeds telemetry.**
|
||
|
|
||
|
The `DOTNET_CLI_TELEMETRY_OPTOUT` environment variable should be set to `1`.
|
||
|
|
||
|
["Platform Configuration"](#platform-configuration) includes instructions for
|
||
|
setting the environment variable on supported platforms.
|
||
|
|
||
|
:::
|
||
|
|
||
|
## Integration Details
|
||
|
|
||
|
### Installation
|
||
|
|
||
|
The SheetJS library can be loaded when the page is loaded or imported whenever
|
||
|
the library functionality is used.
|
||
|
|
||
|
#### Standalone Script
|
||
|
|
||
|
The [SheetJS Standalone scripts](/docs/getting-started/installation/standalone)
|
||
|
can be loaded in the root HTML page (typically `wwwroot/index.html`):
|
||
|
|
||
|
<CodeBlock language="html">{`\
|
||
|
<!-- use version ${current} -->
|
||
|
<script lang="javascript" src="https://cdn.sheetjs.com/xlsx-${current}/package/dist/xlsx.full.min.js"></script>`}
|
||
|
</CodeBlock>
|
||
|
|
||
|
#### ECMAScript Module
|
||
|
|
||
|
The SheetJS ECMAScript module script can be dynamically imported from functions.
|
||
|
This ensures the library is only loaded when necessary. The following example
|
||
|
loads the library and returns a Promise that resolves to the version string:
|
||
|
|
||
|
<CodeBlock language="js">{`\
|
||
|
async function sheetjs_version(id) {
|
||
|
/* dynamically import the script in the event listener */
|
||
|
// highlight-next-line
|
||
|
const XLSX = await import("https://cdn.sheetjs.com/xlsx-${current}/package/xlsx.mjs");
|
||
|
\n\
|
||
|
/* use the library */
|
||
|
return XLSX.version;
|
||
|
}`}
|
||
|
</CodeBlock>
|
||
|
|
||
|
### Calling JS from C#
|
||
|
|
||
|
#### Setup
|
||
|
|
||
|
The primary mechanism for invoking JS functions from Blazor is `IJSRuntime`[^1].
|
||
|
It should be injected at the top of relevant Razor component scripts:
|
||
|
|
||
|
```csharp title="Injecting IJSRuntime"
|
||
|
@inject IJSRuntime JS
|
||
|
```
|
||
|
|
||
|
#### Fire and Forget
|
||
|
|
||
|
When exporting a file with the SheetJS `writeFile` method[^2], browser APIs do
|
||
|
not provide success or error feedback. As a result, this demo invokes functions
|
||
|
using the `InvokeVoidAsync` static method[^3]:
|
||
|
|
||
|
```csharp title="Invoking JS functions from C#"
|
||
|
private async Task ExportDataset() {
|
||
|
await JS.InvokeVoidAsync("export_method", data);
|
||
|
}
|
||
|
```
|
||
|
|
||
|
Methods are commonly bound to buttons in the Razor template using `@onclick`:
|
||
|
|
||
|
```html title="Binding callback to a HTML button"
|
||
|
<button @onclick="ExportDataset">Export Dataset</button>
|
||
|
```
|
||
|
|
||
|
### State in Blazor
|
||
|
|
||
|
The example [presidents sheet](https://docs.sheetjs.com/pres.xlsx) has one
|
||
|
header row with "Name" and "Index" columns.
|
||
|
|
||
|
![`pres.xlsx` data](pathname:///pres.png)
|
||
|
|
||
|
#### C# Representation
|
||
|
|
||
|
The natural C# representation of a single row is a class object:
|
||
|
|
||
|
```csharp title="President class"
|
||
|
public class President {
|
||
|
public string Name { get; set; }
|
||
|
public int Index { get; set; }
|
||
|
}
|
||
|
|
||
|
var PrezClinton = new President() { Name = "Bill Clinton", Index = 42 };
|
||
|
```
|
||
|
|
||
|
The entire dataset is typically stored in an array of class objects:
|
||
|
|
||
|
```csharp title="President dataset"
|
||
|
private President[] data;
|
||
|
```
|
||
|
|
||
|
#### Data Interchange
|
||
|
|
||
|
`InvokeVoidAsync` can pass data from the C# state to a JS function:
|
||
|
|
||
|
```csharp
|
||
|
await JS.InvokeVoidAsync("export_method", data);
|
||
|
```
|
||
|
|
||
|
Each row in the dataset will be passed as a separate argument to the JavaScript
|
||
|
method, so the JavaScript code should collect the arguments:
|
||
|
|
||
|
```js title="Collecting rows in a JS callback"
|
||
|
/* NOTE: blazor spreads the C# array, so the ... spread syntax is required */
|
||
|
async function export_method(...rows) {
|
||
|
/* display the array of objects */
|
||
|
console.log(rows);
|
||
|
}
|
||
|
```
|
||
|
|
||
|
Each row is a simple JavaScript object.
|
||
|
|
||
|
:::caution pass
|
||
|
|
||
|
Blazor automatically spreads arrays. Each row is passed as a separate argument
|
||
|
to the JavaScript method.
|
||
|
|
||
|
The example method uses the JavaScript spread syntax to collect the arguments.
|
||
|
|
||
|
:::
|
||
|
|
||
|
#### Exporting Data
|
||
|
|
||
|
With the collected array of objects, the SheetJS `json_to_sheet` method[^4] will
|
||
|
generate a SheetJS worksheet[^5] from the dataset. After creating a workbook[^6]
|
||
|
object with the `book_new` method[^7], the file is written with `writeFile`[^2]:
|
||
|
|
||
|
<CodeBlock title="JS Callback for exporting datasets" language="javascript">{`\
|
||
|
/* NOTE: blazor spreads the C# array, so the spread is required */
|
||
|
async function export_method(...rows) {
|
||
|
const XLSX = await import("https://cdn.sheetjs.com/xlsx-${current}/package/xlsx.mjs");
|
||
|
const ws = XLSX.utils.json_to_sheet(rows);
|
||
|
const wb = XLSX.utils.book_new(ws, "Data");
|
||
|
XLSX.writeFile(wb, "SheetJSBlazor.xlsx");
|
||
|
}`}
|
||
|
</CodeBlock>
|
||
|
|
||
|
|
||
|
### HTML Tables
|
||
|
|
||
|
When displaying datasets, Razor components typically generate HTML tables:
|
||
|
|
||
|
```html title="Razor template from official starter"
|
||
|
<table class="table" id="weather-table">
|
||
|
<thead>
|
||
|
<tr><th>Date</th><th>Temp. (C)</th><th>Temp. (F)</th><th>Summary</th></tr>
|
||
|
</thead>
|
||
|
<tbody>
|
||
|
@foreach (var forecast in forecasts)
|
||
|
{
|
||
|
<tr>
|
||
|
<td>@forecast.Date.ToShortDateString()</td>
|
||
|
<td>@forecast.TemperatureC</td>
|
||
|
<td>@forecast.TemperatureF</td>
|
||
|
<td>@forecast.Summary</td>
|
||
|
</tr>
|
||
|
}
|
||
|
</tbody>
|
||
|
</table>
|
||
|
```
|
||
|
|
||
|
If it has an `id`, JS code on the frontend can find the table element using the
|
||
|
`document.getElementById` DOM method. A SheetJS workbook object can be generated
|
||
|
using the `table_to_book` method[^8] and exported with `writeFile`[^2]:
|
||
|
|
||
|
<CodeBlock title="JS Callback for exporting HTML TABLE elements" language="javascript">{`\
|
||
|
/* NOTE: blazor spreads the C# array, so the spread is required */
|
||
|
async function export_method() {
|
||
|
const XLSX = await import("https://cdn.sheetjs.com/xlsx-${current}/package/xlsx.mjs");
|
||
|
const wb = XLSX.utils.table_to_book(document.getElementById("weather-table"));
|
||
|
XLSX.writeFile(wb, "SheetJSBlazor.xlsx");
|
||
|
}`}
|
||
|
</CodeBlock>
|
||
|
|
||
|
This approach uses data that already exists in the document, so no additional
|
||
|
data is passed from C# to JavaScript.
|
||
|
|
||
|
## Complete Demo
|
||
|
|
||
|
The Blazor + WASM starter app includes a "Weather" component that displays data
|
||
|
from a C#-managed dataset. This demo uses SheetJS to export data in two ways:
|
||
|
|
||
|
- "Export Dataset" will send row objects from the underlying C# data store to
|
||
|
the frontend. The SheetJS `json_to_sheet` method[^4] builds the worksheet.
|
||
|
|
||
|
- "Export HTML Table" will scrape the table using the SheetJS `table_to_book`
|
||
|
method[^8]. No extra data will be sent to the frontend.
|
||
|
|
||
|
:::note Tested Deployments
|
||
|
|
||
|
This demo was tested in the following deployments:
|
||
|
|
||
|
| Architecture | Date |
|
||
|
|:-------------|:-----------|
|
||
|
| `darwin-arm` | 2024-10-15 |
|
||
|
|
||
|
:::
|
||
|
|
||
|
### Platform Configuration
|
||
|
|
||
|
0) Set the `DOTNET_CLI_TELEMETRY_OPTOUT` environment variable to `1`.
|
||
|
|
||
|
<details open>
|
||
|
<summary><b>How to disable telemetry</b> (click to hide)</summary>
|
||
|
|
||
|
<Tabs groupId="os">
|
||
|
<TabItem value="unix" label="Linux/MacOS">
|
||
|
|
||
|
Add the following line to `.profile`, `.bashrc` and `.zshrc`:
|
||
|
|
||
|
```bash title="(add to .profile , .bashrc , and .zshrc)"
|
||
|
export DOTNET_CLI_TELEMETRY_OPTOUT=1
|
||
|
```
|
||
|
|
||
|
Close and restart the Terminal to load the changes.
|
||
|
|
||
|
</TabItem>
|
||
|
<TabItem value="win" label="Windows">
|
||
|
|
||
|
Type `env` in the search bar and select "Edit the system environment variables".
|
||
|
|
||
|
In the new window, click the "Environment Variables..." button.
|
||
|
|
||
|
In the new window, look for the "System variables" section and click "New..."
|
||
|
|
||
|
Set the "Variable name" to `DOTNET_CLI_TELEMETRY_OPTOUT` and the value to `1`.
|
||
|
|
||
|
Click "OK" in each window (3 windows) and restart your computer.
|
||
|
|
||
|
</TabItem>
|
||
|
</Tabs>
|
||
|
|
||
|
</details>
|
||
|
|
||
|
1) Install .NET
|
||
|
|
||
|
<details>
|
||
|
<summary><b>Installation Notes</b> (click to show)</summary>
|
||
|
|
||
|
For macOS x64 and ARM64, install the `dotnet-sdk` Cask with Homebrew:
|
||
|
|
||
|
```bash
|
||
|
brew install --cask dotnet-sdk
|
||
|
```
|
||
|
|
||
|
For Steam Deck Holo and other Arch Linux x64 distributions, the `dotnet-sdk` and
|
||
|
`dotnet-runtime` packages should be installed using `pacman`:
|
||
|
|
||
|
```bash
|
||
|
sudo pacman -Syu dotnet-sdk dotnet-runtime
|
||
|
```
|
||
|
|
||
|
https://dotnet.microsoft.com/en-us/download/dotnet/6.0 is the official source
|
||
|
for Windows and ARM64 Linux versions.
|
||
|
|
||
|
</details>
|
||
|
|
||
|
2) Open a new Terminal window in macOS or PowerShell window in Windows.
|
||
|
|
||
|
### App Creation
|
||
|
|
||
|
3) Create a new `blazorwasm` app:
|
||
|
|
||
|
```bash
|
||
|
dotnet new blazorwasm -o SheetJSBlazorWasm
|
||
|
cd SheetJSBlazorWasm
|
||
|
dotnet run
|
||
|
```
|
||
|
|
||
|
When the Blazor service runs, the terminal will display a URL:
|
||
|
|
||
|
```text
|
||
|
info: Microsoft.Hosting.Lifetime[14]
|
||
|
// highlight-next-line
|
||
|
Now listening on: http://localhost:6969
|
||
|
```
|
||
|
|
||
|
4) In a new browser window, open the displayed URL from Step 3.
|
||
|
|
||
|
5) Click the "Weather" link and confirm the page includes a data table.
|
||
|
|
||
|
6) Stop the server (press <kbd>CTRL</kbd>+<kbd>C</kbd> in the terminal window).
|
||
|
|
||
|
### SheetJS Integration
|
||
|
|
||
|
7) Add the following script tag to `wwwroot/index.html` in the `HEAD` block:
|
||
|
|
||
|
<CodeBlock title="wwwroot/index.html (add within the HEAD block)" language="html">{`\
|
||
|
<script>
|
||
|
/* NOTE: blazor spreads the C# array, so the spread is required */
|
||
|
async function export_dataset(...rows) {
|
||
|
const XLSX = await import("https://cdn.sheetjs.com/xlsx-${current}/package/xlsx.mjs");
|
||
|
const ws = XLSX.utils.json_to_sheet(rows);
|
||
|
const wb = XLSX.utils.book_new(ws, "Data");
|
||
|
XLSX.writeFile(wb, "SheetJSBlazorDataset.xlsx");
|
||
|
}
|
||
|
\n\
|
||
|
async function export_html(id) {
|
||
|
const XLSX = await import("https://cdn.sheetjs.com/xlsx-${current}/package/xlsx.mjs");
|
||
|
const wb = XLSX.utils.table_to_book(document.getElementById(id));
|
||
|
XLSX.writeFile(wb, "SheetJSBlazorHTML.xlsx");
|
||
|
}
|
||
|
</script>`}
|
||
|
</CodeBlock>
|
||
|
|
||
|
8) Inject the `IJSRuntime` dependency near the top of `Pages/Weather.razor`:
|
||
|
|
||
|
```csharp title="Pages/Weather.razor (add highlighted lines)"
|
||
|
@page "/weather"
|
||
|
@inject HttpClient Http
|
||
|
// highlight-next-line
|
||
|
@inject IJSRuntime JS
|
||
|
```
|
||
|
|
||
|
9) Add an ID to the `TABLE` element in `Pages/Weather.razor`:
|
||
|
|
||
|
```html title="Pages/Weather.razor (add id to TABLE element)"
|
||
|
{
|
||
|
<!-- highlight-next-line -->
|
||
|
<table class="table" id="weather-table">
|
||
|
<thead>
|
||
|
<tr>
|
||
|
```
|
||
|
|
||
|
10) Add callbacks to the `@code` section in `Pages/Weather.razor`:
|
||
|
|
||
|
```csharp title="Pages/Weather.razor (add within the @code section)"
|
||
|
private async Task ExportDataset()
|
||
|
{
|
||
|
await JS.InvokeVoidAsync("export_dataset", forecasts);
|
||
|
}
|
||
|
|
||
|
private async Task ExportHTML()
|
||
|
{
|
||
|
await JS.InvokeVoidAsync("export_html", "weather-table");
|
||
|
}
|
||
|
```
|
||
|
|
||
|
11) Add Export buttons to the template in `Pages/Weather.razor`:
|
||
|
|
||
|
```csharp title="Pages/Weather.razor (add highlighted lines)"
|
||
|
<p>This component demonstrates fetching data from the server.</p>
|
||
|
|
||
|
<!-- highlight-start -->
|
||
|
<button @onclick="ExportDataset">Export Dataset</button>
|
||
|
<button @onclick="ExportHTML">Export HTML TABLE</button>
|
||
|
<!-- highlight-end -->
|
||
|
```
|
||
|
|
||
|
|
||
|
### Testing
|
||
|
|
||
|
12) Launch the `dotnet` process again:
|
||
|
|
||
|
```bash
|
||
|
dotnet run
|
||
|
```
|
||
|
|
||
|
When the Blazor service runs, the terminal will display a URL:
|
||
|
|
||
|
```text
|
||
|
info: Microsoft.Hosting.Lifetime[14]
|
||
|
Now listening on: http://localhost:6969
|
||
|
```
|
||
|
|
||
|
13) In a new browser window, open the displayed URL from Step 12.
|
||
|
|
||
|
14) Click the "Weather" link. The page should match the following screenshot:
|
||
|
|
||
|
![SheetJSBlazorWasm with Exports](pathname:///blazor/weather.png)
|
||
|
|
||
|
15) Click the "Export Dataset" button and save the generated file to
|
||
|
`SheetJSBlazorDataset.xlsx`. Open the file in a spreadsheet editor and confirm
|
||
|
the data matches the table. The column labels will differ since the underlying
|
||
|
dataset uses different labels.
|
||
|
|
||
|
![SheetJSBlazorDataset.xlsx](pathname:///blazor/dataset.png)
|
||
|
|
||
|
16) Click the "Export HTML TABLE" button and save the generated file to
|
||
|
`SheetJSBlazorHTML.xlsx`. Open the file in a spreadsheet editor and confirm the
|
||
|
data matches the table. The column labels will match the HTML table.
|
||
|
|
||
|
![SheetJSBlazorHTML.xlsx](pathname:///blazor/html.png)
|
||
|
|
||
|
:::note pass
|
||
|
|
||
|
It is somewhat curious that the official `dotnet` Blazor sample dataset marks
|
||
|
`1 C` and `-13 C` as "freezing" but marks `-2 C` as "chilly". It stands to
|
||
|
reason that `-2 C` should also be freezing.
|
||
|
|
||
|
:::
|
||
|
|
||
|
[^1]: See ["Microsoft.JSInterop.IJSRuntime"](https://learn.microsoft.com/en-us/dotnet/api/microsoft.jsinterop.ijsruntime) in the `dotnet` documentation.
|
||
|
[^2]: See [`writeFile` in "Writing Files"](/docs/api/write-options)
|
||
|
[^3]: See ["Microsoft.JSInterop.JSRuntimeExtensions.InvokeVoidAsync"](https://learn.microsoft.com/en-us/dotnet/api/microsoft.jsinterop.jsruntimeextensions.invokevoidasync) in the `dotnet` documentation.
|
||
|
[^4]: See [`json_to_sheet` in "Utilities"](/docs/api/utilities/array#array-of-objects-input)
|
||
|
[^5]: See ["Sheet Objects"](/docs/csf/sheet)
|
||
|
[^6]: See ["Workbook Object"](/docs/csf/book)
|
||
|
[^7]: See [`book_new` in "Utilities"](/docs/api/utilities/wb)
|
||
|
[^8]: See [`table_to_book` in "HTML" Utilities](/docs/api/utilities/html#create-new-sheet)
|