How to Build a Razor Class Library for JavaScript Barcode Reader

Previously, we published a GitHub repository named 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

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.

     - 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 via the npm command:

     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

  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.


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:

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:

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.

    JavaScript

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

    C#

     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

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

    C#

     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:

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.

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.

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:

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.

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:

<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.

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.

     dotnet add package RazorBarcodeLibrary 
    
  3. Add a reference to the library in the _Imports.razor file.

     @using RazorBarcodeLibrary
    
  4. Add the following code to the Pages/Index.razor file:

     @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

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.

     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: $
               BRANCH: gh-pages
               FOLDER: example/release/wwwroot
    

Online Barcode Reader Demo

https://yushulx.me/Razor-Barcode-Library/

Source Code

https://github.com/yushulx/Razor-Barcode-Library