Xiao Ling
Posted on November 14, 2023
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:
Create a New Project: Start by creating a new project in Visual Studio and select the Razor Class Library option.
Add a Blazor WebAssembly Project: To facilitate debugging, add a new Blazor WebAssembly project to your solution.
Reference the Library: In your Blazor WebAssembly project, add a reference to the Razor Class Library project.
-
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
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.
-
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. Copy the resource files to the
wwwroot
folder of the Razor Class Library project.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:
-
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); }
-
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
- Create a new Blazor WebAssembly project in Visual Studio.
-
Install the NuGet package of the Razor class library.
dotnet add package RazorBarcodeLibrary
-
Add a reference to the library in the
_Imports.razor
file.
@using RazorBarcodeLibrary
-
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(); } }
-
Run the application.
Deploying the Project to GitHub Pages
- Enable
Read and write permissions
inSettings > Actions > General > Workflow permissions
. -
To build the example project, create a new workflow file, set the
working-directory
to./example
, and change the base-tag in theindex.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: ${{ secrets.GITHUB_TOKEN }} BRANCH: gh-pages FOLDER: example/release/wwwroot
Online Barcode Reader Demo
https://yushulx.me/Razor-Barcode-Library/
Source Code
Posted on November 14, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.