How to Develop a Cross-Platform Barcode Reader Application by Hybridizing .NET MAUI and Blazor
Tags: dotnet, blazor, programming, csharp
Published: 2023-04-17 00:56:18

[.NET Multi-platform App UI](https://learn.microsoft.com/en-us/dotnet/maui/?view=net-maui-7.0) (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](https://learn.microsoft.com/en-us/aspnet/core/blazor/?view=aspnetcore-7.0) 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](https://www.dynamsoft.com/barcode-reader/overview/). The app will be able to scan linear and two-dimensional barcodes on **Windows**, **macOS**, **iOS**, and **Android**.

<video src="https://user-images.githubusercontent.com/2202306/231950419-f42c6bb5-c51a-4f5f-9443-372940e4f6da.mp4" controls="controls" muted="muted" style="max-height:640px; min-height: 200px; max-width: 100%;"></video>

## Prerequisites
- [.NET 6 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/6.0)
- [Visual Studio 2022](https://visualstudio.microsoft.com/downloads/)
- [Trial License of Dynamsoft Barcode Reader SDK](https://www.dynamsoft.com/customer/license/trialLicense?product=dbr&package=blazor)

## 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](https://github.com/yushulx/javascript-barcode-qr-code-scanner). This repository features examples that have been built using [Dynamsoft JavaScript Barcode SDK](https://www.dynamsoft.com/barcode-reader/sdk-javascript/).

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.
      ```cs
      @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.
      ```cs
      @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. 
    ```javascript
    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.
    ```html
    <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](https://www.dynamsoft.com/codepool/img/2023/04/blazor-webassembly-barcode-reader.png)

    To deploy the project to GitHub Pages, you can use the following workflow file:

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

```csharp
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**

1. Create a custom `WebChromeClient` class in the `Platforms/Android/MyWebChromeClient.cs` file:
    ```cs
    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:
    ```cs
    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:

    ```cs
    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:
    ```cs
    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
    <?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:
    ```cs
      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](https://developer.apple.com/documentation/webkit/wkwebview).

![.NET MAUI blazor macos camera error](https://www.dynamsoft.com/codepool/img/2023/04/maui-macos-webview-camera-error.png)

## 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](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](https://www.nuget.org/packages/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:

    ```diff
    - <Project Sdk="Microsoft.NET.Sdk">
    + <Project Sdk="Microsoft.NET.Sdk.Razor">

    + <EnableDefaultCssItems>false</EnableDefaultCssItems>
    ```

3. Change the `MauiProgram.cs` file to add `BlazorWebView` support:
    ```csharp
    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:
    ```razor
    <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:
    ```razor
    <?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`:
    ```csharp
    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](https://www.dynamsoft.com/codepool/img/2023/04/dotnet-maui-blazor-barcode-scanner.png)

## Source Code
- Blazor WebAssembly: [https://github.com/yushulx/blazor-barcode-qrcode-reader-scanner](https://github.com/yushulx/blazor-barcode-qrcode-reader-scanner)
- .NET MAUI Blazor: [https://github.com/yushulx/DotNet-MAUI-Blazor-Barcode-Scanner](https://github.com/yushulx/DotNet-MAUI-Blazor-Barcode-Scanner)
- Hybrid App: [https://github.com/yushulx/dotnet-barcode-qr-code-sdk/tree/main/example/maui](https://github.com/yushulx/dotnet-barcode-qr-code-sdk/tree/main/example/maui)