How to Build a Razor Class Library for JavaScript Barcode Reader
Tags: webdev, blazor, dotnet, csharp
Published: 2023-11-14 09:18:58

Previously, we published a GitHub repository named [blazor-barcode-qrcode-reader-scanner](https://github.com/yushulx/blazor-barcode-qrcode-reader-scanner), showcasing the development of a web barcode reader using **Blazor WebAssembly**. In this project, **C#** accounted for only **2.0%** of the entire codebase, mainly tasked with invoking JavaScript code. The core functionality was primarily implemented in **JavaScript**. With an aim to enhance code reusability and streamline .NET development, we are going to build a **Razor Class Library (RCL)**, which can include static assets like JavaScript files, export JavaScript API through C# interop, and be distributed via **NuGet**.


## NuGet Package
[https://www.nuget.org/packages/RazorBarcodeLibrary](https://www.nuget.org/packages/RazorBarcodeLibrary)

## Initiate a Razor Class Library Project
In Visual Studio, when you're working with a **Razor Class Library (RCL)**, creating a separate Blazor WebAssembly project within the same solution is a practical approach to debug and test the library. Here's how you can set it up:

1. **Create a New Project**: Start by creating a new project in Visual Studio and select the **Razor Class Library** option. 

2. **Add a Blazor WebAssembly Project**: To facilitate debugging, add a new Blazor WebAssembly project to your solution. 

3. **Reference the Library**: In your Blazor WebAssembly project, add a reference to the Razor Class Library project. 

4. **Maintain Separate Project Folders**: It's crucial to keep the two project folders independent within the solution. Placing the example (Blazor WebAssembly) project under the library project's folder hierarchy can lead to compilation errors due to the way Visual Studio and MSBuild handle project dependencies and paths.
    
    ```bash
    - Razor-Barcode-Library
      - example
      - RazorBarcodeLibrary
    ```

5. **Run and Debug**: Set the Blazor WebAssembly project as the startup project. You can then run and debug it just like any other Blazor application. 

## Adding a JavaScript Barcode Library to the Razor Class Library
Adding a JavaScript Barcode Library to a Razor Class Library involves several steps. 
1. Download the [Dynamsoft JavaScript Barcode SDK](https://www.dynamsoft.com/Downloads/Dynamic-Barcode-Reader-Download.aspx) via the npm command:

    ```bash
    npm install dynamsoft-javascript-barcode
    ```

    All the JavaScript and wasm files are under the `node_modules/dynamsoft-javascript-barcode/dist` folder. 

    ![dynamsoft-javascript-barcode-sdk](https://www.dynamsoft.com/codepool/img/2023/11/dynamsoft-javascript-barcode-sdk.png)

2. Copy the resource files to the `wwwroot` folder of the Razor Class Library project.
3. Create a `barcodeJsInterop.js` file for exporting the JavaScript APIs.

## Loading JavaScript Files in the Razor Class Library
In a Razor Class Library (RCL) project, unlike a Blazor project, there is no `index.html` file for loading JavaScript files. To address this, we can create a `BarcodeJsInterop.cs` file in C#. This file will be responsible for handling the JavaScript interop, which includes loading and interacting with the JavaScript files required for barcode functionality.

```csharp

using Microsoft.JSInterop;
using System.Text.Json;

namespace RazorBarcodeLibrary
{
    public class BarcodeJsInterop : IAsyncDisposable
    {
        private readonly Lazy<Task<IJSObjectReference>> moduleTask;

        public BarcodeJsInterop(IJSRuntime jsRuntime)
        {
            moduleTask = new(() => jsRuntime.InvokeAsync<IJSObjectReference>(
                "import", "./_content/RazorBarcodeLibrary/barcodeJsInterop.js").AsTask());
        }

        public async ValueTask DisposeAsync()
        {
            if (moduleTask.IsValueCreated)
            {
                var module = await moduleTask.Value;
                await module.DisposeAsync();
            }
        }
    }
}
```

In a Razor Class Library, static assets like JavaScript files are typically accessible under the `_content/[LibraryName]/` path. 


We can dynamically load Dynamsoft JavaScript barcode SDK in the `barcodeJsInterop.js` file as follows:

```javascript
export function init() {
    return new Promise((resolve, reject) => {
        let script = document.createElement('script');
        script.type = 'text/javascript';
        script.src = '_content/RazorBarcodeLibrary/dbr.js';
        script.onload = async () => {
            resolve();
        };
        script.onerror = () => {
            reject();
        };
        document.head.appendChild(script);
    });
}
```

The **Promise** allows the Blazor application to await the loading of the script, ensuring that any JavaScript functionality provided by the script is ready to use before the Blazor application attempts to interact with it.

Returning to the `BarcodeJsInterop.cs` file, we can add a `LoadJS()` method to load the JavaScript barcode SDK:

```csharp
public async Task LoadJS()
{
    var module = await moduleTask.Value;
    await module.InvokeAsync<object>("init");
}
```

So far, we have established the basic interop setup. We can now define additional JavaScript APIs in the `barcodeJsInterop.js` file and invoke them from C#.

## Activating Dynamsoft JavaScript Barcode SDK
Although the JavaScript file of the Dynamsoft barcode SDK has been loaded, the barcode reader has not yet been activated. Two more steps remain to be completed:

1. Set the license key. You can get a free trial license from [here](https://www.dynamsoft.com/customer/license/trialLicense?product=dbr&utm_source=github&utm_campaign=razor-barcode-library).

    **JavaScript**

    ```javascript
    export function setLicense(license) {
        if (!Dynamsoft) return;
        try {
            Dynamsoft.DBR.BarcodeScanner.license = license;
        }
        catch (ex) {
            console.error(ex);
        }
    }
    ```

    **C#**

    ```csharp
    public async Task SetLicense(string license)
    {
        var module = await moduleTask.Value;
        await module.InvokeVoidAsync("setLicense", license);
    }
    ```

2. Load the wasm file, which contains the core barcode detection algorithms. It's important to note that once the wasm file is loaded, the license key cannot be changed.

    **JavaScript**

    ```javascript
    export async function loadWasm() {
        if (!Dynamsoft) return;
        try {
            await Dynamsoft.DBR.BarcodeReader.loadWasm();
        }
        catch (ex) {
            console.error(ex);
        }
    }
    ```

    **C#**

    ```csharp
    public async Task LoadWasm()
    {
        var module = await moduleTask.Value;
        await module.InvokeVoidAsync("loadWasm");
    }
    ```

## Handling a Barcode Reader Instance in **C#**
A Barcode Reader instance can be instantiated in JavaScript as follows:

```javascript
export async function createBarcodeReader() {
    if (!Dynamsoft) return;

    try {
        let reader = await Dynamsoft.DBR.BarcodeReader.createInstance();
        reader.ifSaveOriginalImageInACanvas = true;
        return reader;
    }
    catch (ex) {
        console.error(ex);
    }
    return null;
}
```

The `ifSaveOriginalImageInACanvas` property saves the original image in a canvas, which is necessary for retrieving the image's width and height. These dimensions are required to render the barcode results as an overlay on the image.

```javascript
export function getSourceWidth(reader) {
    let canvas = reader.getOriginalImageInACanvas();
    return canvas.width;
}

export function getSourceHeight(reader) {
    let canvas = reader.getOriginalImageInACanvas();
    return canvas.height;
}
```

To implement the corresponding C# methods, we need to define a `BarcodeReader` class containing an `IJSObjectReference` object that references the JavaScript object.

```csharp
public async Task<BarcodeReader> CreateBarcodeReader()
{
    var module = await moduleTask.Value;
    IJSObjectReference jsObjectReference = await module.InvokeAsync<IJSObjectReference>("createBarcodeReader");
    BarcodeReader reader = new BarcodeReader(module, jsObjectReference);
    return reader;
}

using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection.PortableExecutable;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;

namespace RazorBarcodeLibrary
{
    public class BarcodeReader
    {
        private IJSObjectReference _module;
        private IJSObjectReference _jsObjectReference;

        public int SourceWidth, SourceHeight;

        public BarcodeReader(IJSObjectReference module, IJSObjectReference reader)
        {
            _module = module;
            _jsObjectReference = reader;
        }
    }
}
```

In a web application, any loaded image can be converted to a base64 string. Therefore, we create a `DecodeBase64()` method in the `BarcodeReader` class:

```csharp
public async Task<List<BarcodeResult>> DecodeBase64(string base64)
{
    JsonElement? result = await _jsObjectReference.InvokeAsync<JsonElement>("decode", base64);
    SourceWidth = await _module.InvokeAsync<int>("getSourceWidth", _jsObjectReference);
    SourceHeight = await _module.InvokeAsync<int>("getSourceHeight", _jsObjectReference);
    return BarcodeResult.WrapResult(result);
}
```

In this case, we don't need to create a corresponding JavaScript method. Instead, we can use the `InvokeAsync()` method to call the `decode()` method already defined in the JavaScript instance of the barcode reader.

## Converting Barcode Results from JavaScript JSON Object to C# Object
The barcode reading results are returned as a JavaScript JSON array, which is automatically converted into a C# `JsonElement` object. The code below demonstrates how to deserialize this `JsonElement` object into a `List<BarcodeResult>` object.

```csharp
using System.Text.Json;

namespace RazorBarcodeLibrary
{
    public class BarcodeResult
    {
        public string Text { get; set; } = string.Empty;
        public string Format { get; set; } = string.Empty;

        public string FullInfo { get; set; } = string.Empty;

        public int X1 { get; set; }
        public int Y1 { get; set; }
        public int X2 { get; set; }
        public int Y2 { get; set; }
        public int X3 { get; set; }
        public int Y3 { get; set; }
        public int X4 { get; set; }
        public int Y4 { get; set; }

        public static List<BarcodeResult> WrapResult(JsonElement? result)
        {
            List<BarcodeResult> results = new List<BarcodeResult>();
            if (result != null)
            {
                JsonElement element = result.Value;

                if (element.ValueKind == JsonValueKind.Array)
                {
                    foreach (JsonElement item in element.EnumerateArray())
                    {
                        BarcodeResult barcodeResult = new BarcodeResult();
                        barcodeResult.FullInfo = item.ToString();
                        if (item.TryGetProperty("barcodeFormatString", out JsonElement formatValue))
                        {
                            string? value = formatValue.GetString();
                            if (value != null)
                            {
                                barcodeResult.Format = value;
                            }
                        }

                        if (item.TryGetProperty("barcodeText", out JsonElement textValue))
                        {
                            string? value = textValue.GetString();
                            if (value != null)
                            {
                                barcodeResult.Text = value;
                            }

                        }

                        if (item.TryGetProperty("localizationResult", out JsonElement localizationResult))
                        {
                            if (localizationResult.TryGetProperty("x1", out JsonElement x1Value))
                            {
                                int intValue = x1Value.GetInt32();
                                barcodeResult.X1 = intValue;
                            }

                            if (localizationResult.TryGetProperty("y1", out JsonElement y1Value))
                            {
                                int intValue = y1Value.GetInt32();
                                barcodeResult.Y1 = intValue;
                            }

                            if (localizationResult.TryGetProperty("x2", out JsonElement x2Value))
                            {
                                int intValue = x2Value.GetInt32();
                                barcodeResult.X2 = intValue;
                            }

                            if (localizationResult.TryGetProperty("y2", out JsonElement y2Value))
                            {
                                int intValue = y2Value.GetInt32();
                                barcodeResult.Y2 = intValue;
                            }

                            if (localizationResult.TryGetProperty("x3", out JsonElement x3Value))
                            {
                                int intValue = x3Value.GetInt32();
                                barcodeResult.X3 = intValue;
                            }

                            if (localizationResult.TryGetProperty("y3", out JsonElement y3Value))
                            {
                                int intValue = y3Value.GetInt32();
                                barcodeResult.Y3 = intValue;
                            }

                            if (localizationResult.TryGetProperty("x4", out JsonElement x4Value))
                            {
                                int intValue = x4Value.GetInt32();
                                barcodeResult.X4 = intValue;
                            }

                            if (localizationResult.TryGetProperty("y4", out JsonElement y4Value))
                            {
                                int intValue = y4Value.GetInt32();
                                barcodeResult.Y4 = intValue;
                            }

                            Console.WriteLine(barcodeResult.ToString());

                        }

                        results.Add(barcodeResult);
                    }
                }
            }
            return results;
        }
    }
}
```

## Building the Razor Class Library into a NuGet Package
Open the `RazorBarcodeLibrary.csproj` file and add the following code:

```xml
<Project Sdk="Microsoft.NET.Sdk.Razor">

  <PropertyGroup>
    ...
    <PackageReadmeFile>README.md</PackageReadmeFile>
    <GeneratePackageOnBuild>true</GeneratePackageOnBuild>
  </PropertyGroup>

  ...

  <ItemGroup>
    <None Include="../README.md" Pack="true" PackagePath="" />
  </ItemGroup>

</Project>
```

Based on the project structure, the `README.md` file is located in the parent folder of the library project. Additionally, the `GeneratePackageOnBuild` property is utilized to automatically generate a NuGet package during the project build process using the `dotnet build` command.

```bash
dotnet build --configuration Release
```

## Creating a Blazor Barcode Reader App with the Razor Class Library
1. Create a new Blazor WebAssembly project in Visual Studio.
2. Install the NuGet package of the Razor class library.
    
    ```bash
    dotnet add package RazorBarcodeLibrary 
    ```
3. Add a reference to the library in the `_Imports.razor` file.

    ```csharp
    @using RazorBarcodeLibrary
    ```
4. Add the following code to the `Pages/Index.razor` file:
    
    ```csharp
    @page "/"
    @inject IJSRuntime JSRuntime
    @using System.Text.Json

    <PageTitle>Index</PageTitle>

    <h1>Razor Barcode Library</h1>
    <label>License key: </label>
    <input type="text" placeholder="@licenseKey" @bind="licenseKey">
    <button @onclick="Activate">Activate SDK</button>

    <div>
        <InputFile OnChange="LoadImage" />
        @if (imageSrc != null)
        {
            <div id="imageview">
                <img id="image" src="@imageSrc" />
            </div>
        }
    </div>

    <div>
        <button @onclick="Decode">Read Barcode</button>
        <p>@result</p>
    </div>

    @code {
        BarcodeReader? reader;
        BarcodeJsInterop? barcodeJsInterop;
        private MarkupString result;
        private string? imageSrc;
        private string licenseKey = "LICENSE-KEY";

        private async Task LoadImage(InputFileChangeEventArgs e)
        {
            result = new MarkupString("");
            if (barcodeJsInterop != null) await barcodeJsInterop.ClearCanvas("overlay");
            var imageFiles = e.GetMultipleFiles();
            var format = "image/png"; 

            if (imageFiles.Count > 0)
            {
                var file = imageFiles.First();
                var maxAllowedSize = 20 * 1024 * 1024;
                var buffer = new byte[file.Size];
                await file.OpenReadStream(maxAllowedSize).ReadAsync(buffer);

                imageSrc = $"data:{format};base64,{Convert.ToBase64String(buffer)}";
            }
        }

        protected override async Task OnInitializedAsync()
        {
            barcodeJsInterop = new BarcodeJsInterop(JSRuntime);
            await barcodeJsInterop.LoadJS();
        }

        public async Task Decode()
        {
            if (barcodeJsInterop == null || imageSrc == null || reader == null) return;
            try
            {
                List<BarcodeResult> results = await reader.DecodeBase64(imageSrc);
                if (results.Count > 0)
                {
                    string text = "";
                    foreach (BarcodeResult result in results)
                    {
                        text += "format: " + result.Format + ", ";
                        text += "text: " + result.Text + "<br>";
                    }
                    result = new MarkupString(text);

                    await barcodeJsInterop.DrawCanvas("overlay", reader.SourceWidth, reader.SourceHeight, results);
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
        }

        public async Task Activate()
        {
            if (barcodeJsInterop == null) return;
            await barcodeJsInterop.SetLicense(licenseKey);
            await barcodeJsInterop.LoadWasm();
            reader = await barcodeJsInterop.CreateBarcodeReader();
        }
    }
    ```

5. Run the application.
    
    ![Blazor WebAssembly barcode reader](https://user-images.githubusercontent.com/2202306/282373638-b2cf96db-8466-4c3a-a802-4fd39cec42cd.png)

## Deploying the Project to GitHub Pages
1. Enable `Read and write permissions` in `Settings > Actions > General > Workflow permissions`.
2. To build the example project, create a new workflow file, set the `working-directory` to `./example`, and change the base-tag in the `index.html` file from `/` to the repository name. This ensures the correct loading of JavaScript files.
    
    ```yml
    name: blazorwasm

    on:
      push:
        branches: [ main ]
      pull_request:
        branches: [ main ]
  
    jobs:
      build:
        runs-on: ubuntu-latest
  
        steps:
          - uses: actions/checkout@v3
          
          - name: Setup .NET Core SDK
            uses: actions/setup-dotnet@v2
            with:
              dotnet-version: '7.0.x'
              include-prerelease: true
            
          - name: Publish .NET Core Project
            run: dotnet publish example.csproj -c Release -o release --nologo 
            working-directory: ./example
            
          - name: Change base-tag in index.html from / to Razor-Barcode-Library
            run: sed -i 's/<base href="\/" \/>/<base href="\/Razor-Barcode-Library\/" \/>/g' release/wwwroot/index.html
            working-directory: ./example
            
          - name: copy index.html to 404.html
            run: cp release/wwwroot/index.html release/wwwroot/404.html
            working-directory: ./example
            
          - name: Add .nojekyll file
            run: touch release/wwwroot/.nojekyll
            working-directory: ./example
          
          - name: Commit wwwroot to GitHub Pages
            uses: JamesIves/github-pages-deploy-action@3.7.1
            with:
              GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
              BRANCH: gh-pages
              FOLDER: example/release/wwwroot
    ```

## Online Barcode Reader Demo
[https://yushulx.me/Razor-Barcode-Library/](https://yushulx.me/Razor-Barcode-Library/)


## Source Code
[https://github.com/yushulx/Razor-Barcode-Library](https://github.com/yushulx/Razor-Barcode-Library)