How to Develop a Cross-Platform Barcode Reader Application by Hybridizing .NET MAUI and Blazor
.NET Multi-platform App UI (MAUI) is a cross-platform UI framework for building native and modern applications in C#. It allows developers to create a single codebase for multiple platforms. Blazor is a web UI framework for building interactive client-side web applications with .NET. It allows developers to write C# code that runs in the browser through the use of WebAssembly. When used together, .NET MAUI and Blazor provide a powerful combination for building cross-platform applications that can run on multiple platforms, including desktop, web, and mobile. In this article, we will demonstrate how to create a Blazor Hybrid app with Dynamsoft Barcode SDK. The app will be able to scan linear and two-dimensional barcodes on Windows, macOS, iOS, and Android.
Prerequisites
Getting Started with Blazor WebAssembly
Since Blazor UI components can be shared between Blazor WebAssembly and .NET MAUI Blazor projects, we will start by creating a Blazor WebAssembly project. To do this, open Visual Studio 2022 and create a new Blazor WebAssembly App project.
To save time on writing the code for web barcode reader and scanner, we will utilize the repository https://github.com/yushulx/javascript-barcode-qr-code-scanner. This repository features examples that have been built using Dynamsoft JavaScript Barcode SDK.
The steps to integrate the JavaScript Barcode SDK into the Blazor WebAssembly project are as follows:
- Create two Razor components in the Pages folder: Reader.razor and Scanner.razor.
- Copy the HTML5 UI code from the examples to the Razor components.
- Reader.razor: Load an image file via the InputFile component and display the image in the img element. The canvas element is used to draw the barcode location and the barcode text. The p element is used to display the barcode text.
@page "/barcodereader"
@inject IJSRuntime JSRuntime
<InputFile OnChange="LoadImage" />
<p class="p-result">@result</p>
<div id="imageview">
<img id="image" />
<canvas id="overlay"></canvas>
</div>
@code {
String result = "";
private DotNetObjectReference<Reader> objRef;
private async Task LoadImage(InputFileChangeEventArgs e)
{
result = "";
var imageFile = e.File;
var jsImageStream = imageFile.OpenReadStream(1024 * 1024 * 20);
var dotnetImageStream = new DotNetStreamReference(jsImageStream);
await JSRuntime.InvokeAsync<byte[]>("jsFunctions.setImageUsingStreaming", objRef, "overlay",
"image", dotnetImageStream);
}
protected override void OnInitialized()
{
objRef = DotNetObjectReference.Create(this);
}
[JSInvokable]
public void ReturnBarcodeResultsAsync(String text)
{
result = text;
StateHasChanged();
}
public void Dispose()
{
objRef?.Dispose();
}
}
- Scanner.razor: The select element is used to select the video source. The div element is used to display the video stream. The canvas element is used to draw the barcode location and the barcode text.
@page "/barcodescanner"
@inject IJSRuntime JSRuntime
<div class="select">
<label for="videoSource">Video source: </label>
<select id="videoSource"></select>
</div>
<div id="videoview">
<div class="dce-video-container" id="videoContainer"></div>
<canvas id="overlay"></canvas>
</div>
@code {
String result = "";
private DotNetObjectReference<Scanner> objRef;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
objRef = DotNetObjectReference.Create(this);
await JSRuntime.InvokeAsync<Boolean>("jsFunctions.initScanner", objRef, "videoContainer", "videoSource", "overlay");
}
}
[JSInvokable]
public void ReturnBarcodeResultsAsync(String text)
{
result = text;
StateHasChanged();
}
public void Dispose()
{
objRef?.Dispose();
}
}
3. Copy the JavaScript code from the examples to the wwwroot/jsInterop.js file.
window.jsFunctions = {
setImageUsingStreaming: async function setImageUsingStreaming(dotnetRef, overlayId, imageId, imageStream) {
const arrayBuffer = await imageStream.arrayBuffer();
const blob = new Blob([arrayBuffer]);
const url = URL.createObjectURL(blob);
document.getElementById(imageId).src = url;
document.getElementById(imageId).style.display = 'block';
initOverlay(document.getElementById(overlayId));
if (reader) {
reader.maxCvsSideLength = 9999
decodeImage(dotnetRef, url, blob);
}
},
initSDK: async function () {
if (reader != null) {
return true;
}
let result = true;
try {
reader = await Dynamsoft.DBR.BarcodeReader.createInstance();
await reader.updateRuntimeSettings("balance");
} catch (e) {
console.log(e);
result = false;
}
return result;
},
initScanner: async function(dotnetRef, videoId, selectId, overlayId) {
let canvas = document.getElementById(overlayId);
initOverlay(canvas);
videoSelect = document.getElementById(selectId);
videoSelect.onchange = openCamera;
dotnetHelper = dotnetRef;
try {
scanner = await Dynamsoft.DBR.BarcodeScanner.createInstance();
await scanner.setUIElement(document.getElementById(videoId));
await scanner.updateRuntimeSettings("speed");
let cameras = await scanner.getAllCameras();
listCameras(cameras);
await openCamera();
scanner.onFrameRead = results => {
showResults(results);
};
scanner.onUnduplicatedRead = (txt, result) => { };
scanner.onPlayed = function () {
updateResolution();
}
await scanner.show();
} catch (e) {
console.log(e);
result = false;
}
return true;
},
};
These JavaScript functions can be called from the Razor components. The dotnetRef
parameter is used to call .NET methods in the Razor component.
4. In the index.html
file, add the following code to load the Dynamsoft JavaScript Barcode SDK and the jsInterop.js file.
<script src="https://cdn.jsdelivr.net/npm/dynamsoft-javascript-barcode@9.6.11/dist/dbr.js"></script>
<script src="jsInterop.js"></script>
5. Afterwards, you can run the Blazor Web Barcode Reader application.
To deploy the project to GitHub Pages, you can use the following workflow file:
name: blazorwasm
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup .NET Core SDK
uses: actions/setup-dotnet@v2
with:
dotnet-version: '6.0.x'
include-prerelease: true
- name: Publish .NET Core Project
run: dotnet publish BlazorBarcodeSample.csproj -c Release -o release --nologo
- name: Change base-tag in index.html from / to blazor-barcode-qrcode-reader-scanner
run: sed -i 's/<base href="\/" \/>/<base href="\/blazor-barcode-qrcode-reader-scanner\/" \/>/g' release/wwwroot/index.html
- name: copy index.html to 404.html
run: cp release/wwwroot/index.html release/wwwroot/404.html
- name: Add .nojekyll file
run: touch release/wwwroot/.nojekyll
- name: Commit wwwroot to GitHub Pages
uses: JamesIves/github-pages-deploy-action@3.7.1
with:
GITHUB_TOKEN: $
BRANCH: gh-pages
FOLDER: release/wwwroot
Please modify BlazorBarcodeSample.csproj
and blazor-barcode-qrcode-reader-scanner
according to your project and repository names.
Migrating Blazor WebAssembly to .NET MAUI Blazor
To create a new .NET MAUI Blazor project, follow these steps:
- Compare the project structure of .NET MAUI Blazor with that of Blazor WebAssembly to understand the similarities.
- Copy the
wwwroot
andPages
folders from the Blazor WebAssembly project to the new .NET MAUI Blazor project to get it up and running quickly.
It’s important to note that unlike web apps, .NET MAUI Blazor apps are native apps that are sandboxed and require user permission to access the camera. Therefore, you must add the following C# code to the Scanner.razor
file to request permission to access the camera.
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
var status = await Permissions.CheckStatusAsync<Permissions.Camera>();
if (status == PermissionStatus.Granted)
{
isGranted = true;
}
else
{
status = await Permissions.RequestAsync<Permissions.Camera>();
if (status == PermissionStatus.Granted)
{
isGranted = true;
}
}
if (isGranted)
{
StateHasChanged();
objRef = DotNetObjectReference.Create(this);
await JSRuntime.InvokeAsync<Boolean>("jsFunctions.initScanner", objRef, "videoContainer", "videoSource", "overlay");
}
}
}
The next step is to address certain platform-specific considerations. As we work with Windows, Android, iOS, and macOS, it’s important to note that each may exhibit distinct behaviors.
Request Camera Permissions in .NET MAUI Blazor
Windows
No additional work is required.
Android
- Create a custom
WebChromeClient
class in thePlatforms/Android/MyWebChromeClient.cs
file:
using Android.Content;
using Android.Webkit;
namespace BarcodeScanner.Platforms.Android
{
public class MyWebChromeClient : WebChromeClient
{
private MainActivity _activity;
public MyWebChromeClient(Context context)
{
_activity = context as MainActivity;
}
public override void OnPermissionRequest(PermissionRequest request)
{
try
{
request.Grant(request.GetResources());
base.OnPermissionRequest(request);
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
}
public override bool OnShowFileChooser(global::Android.Webkit.WebView webView, IValueCallback filePathCallback, FileChooserParams fileChooserParams)
{
base.OnShowFileChooser(webView, filePathCallback, fileChooserParams);
return _activity.ChooseFile(filePathCallback, fileChooserParams.CreateIntent(), fileChooserParams.Title);
}
}
}
You must override the OnPermissionRequest
and OnShowFileChooser
methods. The OnPermissionRequest
method is used to grant the camera access permission. The OnShowFileChooser
method is used to start an activity to select a file.
2. In the MainActivity.cs
file, add the following code to receive the returned image file and trigger the callback method:
public class MainActivity : MauiAppCompatActivity
{
private IValueCallback _filePathCallback;
private int _requestCode = 100;
protected override void OnActivityResult(int requestCode, Result resultCode, Intent data)
{
if (_requestCode == requestCode)
{
if (_filePathCallback == null)
return;
Java.Lang.Object result = FileChooserParams.ParseResult((int)resultCode, data);
_filePathCallback.OnReceiveValue(result);
}
}
public bool ChooseFile(IValueCallback filePathCallback, Intent intent, string title)
{
_filePathCallback = filePathCallback;
StartActivityForResult(Intent.CreateChooser(intent, title), _requestCode);
return true;
}
}
3. Create a MauiBlazorWebViewHandler.cs
file to set the custom web view:
namespace BarcodeScanner.Platforms.Android
{
public class MauiBlazorWebViewHandler : BlazorWebViewHandler
{
protected override global::Android.Webkit.WebView CreatePlatformView()
{
var view = base.CreatePlatformView();
view.SetWebChromeClient(new MyWebChromeClient(this.Context));
return view;
}
}
}
4. Register the MauiBlazorWebViewHandler
in the MauiProgram.cs
file:
using Microsoft.AspNetCore.Components.WebView.Maui;
#if ANDROID
using BarcodeScanner.Platforms.Android;
#endif
namespace BarcodeScanner;
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
}).ConfigureMauiHandlers(handlers =>
{
#if ANDROID
handlers.AddHandler<BlazorWebView, MauiBlazorWebViewHandler>();
#endif
});
builder.Services.AddMauiBlazorWebView();
#if DEBUG
builder.Services.AddBlazorWebViewDeveloperTools();
#endif
return builder.Build();
}
}
iOS
- Name the
BlazorWebView
towebView:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:BarcodeScanner"
x:Class="BarcodeScanner.WebContentPage"
Title="WebContentPage"
BackgroundColor="{DynamicResource PageBackgroundColor}">
<BlazorWebView x:Name="webView" HostPage="wwwroot/index.html">
<BlazorWebView.RootComponents>
<RootComponent Selector="#app" ComponentType="{x:Type local:WebContent}" />
</BlazorWebView.RootComponents>
</BlazorWebView>
</ContentPage>
2. Configure WKWebView
properties in the corresponding C# file:
public partial class WebContentPage : ContentPage
{
public WebContentPage()
{
InitializeComponent();
webView.BlazorWebViewInitializing += WebView_BlazorWebViewInitializing;
}
private void WebView_BlazorWebViewInitializing(object sender, BlazorWebViewInitializingEventArgs e)
{
#if IOS || MACCATALYST
e.Configuration.AllowsInlineMediaPlayback = true;
e.Configuration.MediaTypesRequiringUserActionForPlayback = WebKit.WKAudiovisualMediaTypes.None;
#endif
}
}
macOS
As of now, accessing the camera is not possible in the .NET MAUI Blazor app on macOS due to the lack of support for getUserMedia()
in WKWebView.
Creating a Hybrid Barcode Scanner App with .NET and Web Barcode SDK
We have successfully developed a cross-platform barcode scanner app using .NET MAUI Blazor. However, the barcode scanning logic is implemented in JavaScript which may have an impact on performance. In order to optimize performance, it is recommended to use a .NET native barcode SDK, unless there are specific features that are not supported by the SDK, such as camera stream APIs for image-processing scenarios.
For Windows .NET MAUI apps, decoding barcodes from image files can be done using a MAUI content page, while decoding barcodes from camera streams can be achieved using a Blazor webview.
Here are the steps to create a hybrid barcode scanner app:
- Get the existing .NET MAUI example project from https://github.com/yushulx/dotnet-barcode-qr-code-sdk/tree/main/example/maui. The project supports decoding barcodes from image files and camera streams using BarcodeQRCodeSDK, which is a .NET native barcode SDK. The project cannot scan barcodes from camera streams on Windows due to the lack of .NET MAUI camera APIs.
2. Modify the *.csproj
file by comparing .NET MAUI Blazor project:
- <Project Sdk="Microsoft.NET.Sdk">
+ <Project Sdk="Microsoft.NET.Sdk.Razor">
+ <EnableDefaultCssItems>false</EnableDefaultCssItems>
3. Change the MauiProgram.cs
file to add BlazorWebView
support:
using Microsoft.Maui.Controls.Compatibility.Hosting;
using SkiaSharp.Views.Maui.Controls.Hosting;
using Microsoft.AspNetCore.Components.WebView.Maui;
namespace BarcodeQrScanner;
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder.UseSkiaSharp()
.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
}).UseMauiCompatibility()
.ConfigureMauiHandlers((handlers) => {
#if ANDROID
handlers.AddCompatibilityRenderer(typeof(CameraPreview), typeof(BarcodeQrScanner.Platforms.Android.CameraPreviewRenderer));
#endif
#if IOS
handlers.AddHandler(typeof(CameraPreview), typeof(BarcodeQrScanner.Platforms.iOS.CameraPreviewRenderer));
#endif
});
builder.Services.AddMauiBlazorWebView();
#if DEBUG
builder.Services.AddBlazorWebViewDeveloperTools();
#endif
return builder.Build();
}
}
4. Copy wwwroot
, Pages
, Shared
folders from the .NET MAUI Blazor project to the .NET MAUI project.
5. In your .NET MAUI Blazor project, rename Main.razor
to WebContent.razor
and update the code:
<Router AppAssembly="@typeof(WebContent).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
</Found>
<NotFound>
<LayoutView Layout="@typeof(MainLayout)">
<p role="alert">Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
Rename MainPage.xaml
to WebContentPage.xaml
and update the code:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:BarcodeQrScanner"
x:Class="BarcodeQrScanner.WebContentPage"
Title="WebContentPage"
BackgroundColor="{DynamicResource PageBackgroundColor}">
<BlazorWebView x:Name="webView" HostPage="wwwroot/index.html">
<BlazorWebView.RootComponents>
<RootComponent Selector="#app" ComponentType="{x:Type local:WebContent}" />
</BlazorWebView.RootComponents>
</BlazorWebView>
</ContentPage>
6. Copy WebContent.razor
, WebContentPage.xaml
, and WebContentPage.xaml.cs
to the .NET MAUI project.
7. In the MainPage.xaml.cs
file, add the following code to navigate to the WebContentPage:
async void OnTakeVideoButtonClicked(object sender, EventArgs e)
{
if (DeviceInfo.Current.Platform == DevicePlatform.WinUI || DeviceInfo.Current.Platform == DevicePlatform.MacCatalyst)
{
await Navigation.PushAsync(new WebContentPage());
return;
}
var status = await Permissions.CheckStatusAsync<Permissions.Camera>();
if (status == PermissionStatus.Granted)
{
await Navigation.PushAsync(new CameraPage());
}
else
{
status = await Permissions.RequestAsync<Permissions.Camera>();
if (status == PermissionStatus.Granted)
{
await Navigation.PushAsync(new CameraPage());
}
else
{
await DisplayAlert("Permission needed", "I will need Camera permission for this action", "Ok");
}
}
}
8. Now the Windows .NET MAUI app can decode barcodes from image files and camera streams using .NET and web APIs respectively.
Source Code
- Blazor WebAssembly: https://github.com/yushulx/blazor-barcode-qrcode-reader-scanner
- .NET MAUI Blazor: https://github.com/yushulx/DotNet-MAUI-Blazor-Barcode-Scanner
- Hybrid App: https://github.com/yushulx/dotnet-barcode-qr-code-sdk/tree/main/example/maui
Originally published at https://www.dynamsoft.com on April 12, 2023.