DEV Community

Xiao Ling
Xiao Ling

Posted on • Originally published at dynamsoft.com

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:

  1. Create two Razor components in the Pages folder: Reader.razor and Scanner.razor.

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

    blazor webassembly barcode reader

    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: ${{ secrets.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:

  1. Compare the project structure of .NET MAUI Blazor with that of Blazor WebAssembly to understand the similarities.
  2. Copy the wwwroot and Pages 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");
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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

  1. Create a custom WebChromeClient class in the Platforms/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

  1. Name the BlazorWebView to webView:

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

.NET MAUI blazor macos camera error

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:

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

    .NET MAUI barcode QR code scanner

Source Code

Top comments (0)