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. | ||
|  | 
 | ||
|  |  | ||
|  | 
 | ||
|  | #### 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: | ||
|  | 
 | ||
|  |  | ||
|  | 
 | ||
|  | 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. | ||
|  | 
 | ||
|  |  | ||
|  | 
 | ||
|  | 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. | ||
|  | 
 | ||
|  |  | ||
|  | 
 | ||
|  | :::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) |