How to Scan Barcodes in .NET MAUI’s HybridWebView

Starting from .NET 9, there is a new HybridWebView control in MAUI which makes it easy to host arbitrary HTML/JS/CSS content in a web view, and enables communication between the code in the web view (JavaScript) and the code that hosts the web view (C#/.NET).

In this article, we are going to talk about how to use the JavaScript version of Dynamsoft Barcode Reader to scan barcodes and QR codes in MAUI’s HybridWebView. The app can run on Windows, Android and iOS.

Demo video:

New Project

Open Visual Studio to create a .NET MAUI app and select .NET 9 as the runtime.

In MainPage.xaml, add two buttons to control the scanner and a HybridWebView to host the scanner.

<?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"
             x:Class="WebBarcodeScannerMAUI.MainPage">
    <Grid RowDefinitions="Auto,*"
          ColumnDefinitions="*">
        <StackLayout Orientation="Horizontal" HorizontalOptions="Center">
            <Button Text="Start Scanning" 
                VerticalOptions="Center"
                HorizontalOptions="Center"
                />
            <Button Text="Stop Scanning"
                VerticalOptions="Center"
                HorizontalOptions="Center"
                />
        </StackLayout>
        
        <ScrollView Grid.Row="1">
            <HybridWebView x:Name="hybridWebView" />
        </ScrollView>
    </Grid>
</ContentPage>

Setup for Camera Access

Declare Camera Permission

For Android, add the following to AndroidManifest.xml:

<uses-permission android:name="android.permission.CAMERA" />

For iOS, add the following to Info.plist:

<key>NSCameraUsageDescription</key>
<string>For barcode scanning</string>

Request Camera Permission

Request camera permission in MainPage.xaml.cs. This is needed for Android.

PermissionStatus status = await Permissions.RequestAsync<Permissions.Camera>();

Add Handlers to Configure the WebView

Android

  1. Create a Platforms/Android/MyWebChromeClient.cs file to override the WebChromeClient class to allow camera permission.

    namespace WebBarcodeScannerMAUI.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);
                }
            }
    
        }
    }
    
  2. Create a handler under Platforms/Android/MyHybridWebViewHandler.cs. It uses the Chrome client we just defined and set the MediaPlaybackRequiresUserGesture setting of the web view to true so that we can directly open the camera when the web page is loaded.

    namespace WebBarcodeScannerMAUI.Platforms.Android
    {
        public class MyHybridWebViewHandler : HybridWebViewHandler
        {
    
            protected override global::Android.Webkit.WebView CreatePlatformView()
            {
                var view = base.CreatePlatformView();
                view.Settings.MediaPlaybackRequiresUserGesture = false;
                var client = new MyWebChromeClient(this.Context);
                view.SetWebChromeClient(client);
                return view;
            }
        }
    }
    
  3. Use the handler in MauiProgram.cs.

    var builder = MauiApp.CreateBuilder();
    builder
        .UseMauiApp<App>()
        .ConfigureMauiHandlers(handlers =>
        {
    #if ANDROID
            handlers.AddHandler<HybridWebView, WebBarcodeScannerMAUI.Platforms.Android.MyHybridWebViewHandler>();
    #endif
        })
    

iOS

For iOS, we only need to define the handler under Platforms/iOS/MyHybridWebViewHandler.cs to update the settings of the web view.

namespace WebBarcodeScannerMAUI.Platforms.iOS
{
    public class MyHybridWebViewHandler : HybridWebViewHandler
    {
        protected override WebKit.WKWebView CreatePlatformView()
        {
            var view = base.CreatePlatformView();
            view.Configuration.AllowsInlineMediaPlayback = true;
            view.Configuration.MediaTypesRequiringUserActionForPlayback = WebKit.WKAudiovisualMediaTypes.None;
            return view;
        }
    }
}

Then use the handler in MauiProgram.cs.

   var builder = MauiApp.CreateBuilder();
   builder
       .UseMauiApp<App>()
       .ConfigureMauiHandlers(handlers =>
       {
   #if ANDROID
           handlers.AddHandler<HybridWebView, WebBarcodeScannerMAUI.Platforms.Android.MyHybridWebViewHandler>();
   #endif
   #if IOS
           handlers.AddHandler<HybridWebView, WebBarcodeScannerMAUI.Platforms.iOS.MyHybridWebViewHandler>();
   #endif
       })

Communication between the Two Sides

Let’s learn about how to communication between the host app and the web view.

Send Messages from C# to JavaScript

C#:

hybridWebView.SendRawMessage("stop");

JS:

window.addEventListener(
  "HybridWebViewMessageReceived",
  function (e) {
    console.log("Raw message: " + e.detail.message);
  }
);

Send Messages from JavaScript to C#

JS:

window.HybridWebView.SendRawMessage("message");

C#:

private void OnHybridWebViewRawMessageReceived(object sender, HybridWebViewRawMessageReceivedEventArgs e)
{
    MainThread.BeginInvokeOnMainThread(async () =>
    {
        await DisplayAlert("Message Received", e.Message, "OK");
    });
}

Write a Web Barcode Scanner

Next, let’s write the web barcode scanner for the web view to use.

  1. Download the zip of Dynamsoft Barcode Reader. Unzip the distributable files under Resources/raw.

    maui resources

  2. Create a new index.html file with the following content. It opens the barcode scanner which takes the entire part of the page. If it scans a barcode, send it to the C# side.

    <!DOCTYPE html>
    <html lang="en">
    
    <head>
      <meta charset="utf-8" />
      <meta name="viewport" content="width=device-width,initial-scale=1.0" />
      <meta name="description" content="Quickly read barcodes with Dynamsoft Barcode Reader from a live camera stream." />
      <meta name="keywords" content="barcode, camera" />
      <title>Dynamsoft Barcode Reader Sample</title>
      <style>
        #camera-view-container {
          position: absolute;
          left: 0;
          top: 0;
          width: 100%;
          height: 100%;
        }
    
        body {
          margin: 0;
          padding: 0;
          overflow: hidden;
        }
      </style>
    </head>
    <body>
      <div id="camera-view-container"></div>
      <script src="./HybridWebView.js"></script>
      <script src="./distributables/dbr.bundle.js"></script>
      <script>
        window.addEventListener(
          "HybridWebViewMessageReceived",
          function (e) {
            console.log("Raw message: " + e.detail.message);
            if (e.detail.message === "stop") {
              stopScanning();
            }else if (e.detail.message === "start") {
              startScanning();
            }
          }
        );
        /** LICENSE ALERT - README
         * To use the library, you need to first specify a license key using the API "initLicense()" as shown below.
         */
        Dynamsoft.Core.CoreModule.engineResourcePaths.rootDirectory = "distributables";
        Dynamsoft.License.LicenseManager.initLicense("LICENSE KEY");
    
        /**
         * You can visit https://www.dynamsoft.com/customer/license/trialLicense?utm_source=samples&product=dbr&package=js to get your own trial license good for 30 days.
         * Note that if you downloaded this sample from Dynamsoft while logged in, the above license key may already be your own 30-day trial license.
         * For more information, see https://www.dynamsoft.com/barcode-reader/docs/web/programming/javascript/user-guide/index.html?ver=10.5.3000&cVer=true#specify-the-license&utm_source=samples or contact support@dynamsoft.com.
         * LICENSE ALERT - THE END
         */
    
        // Optional. Used to load wasm resources in advance, reducing latency between video playing and barcode decoding.
        Dynamsoft.Core.CoreModule.loadWasm(["DBR"]);
        // Defined globally for easy debugging.
        let cameraEnhancer, cvRouter, cameraView;
    
        (async () => {
          try {
            // Create a `CameraEnhancer` instance for camera control and a `CameraView` instance for UI control.
            cameraView = await Dynamsoft.DCE.CameraView.createInstance();
            cameraEnhancer = await Dynamsoft.DCE.CameraEnhancer.createInstance(cameraView);
            // Get default UI and append it to DOM.
            document.querySelector("#camera-view-container").append(cameraView.getUIElement());
    
            // Create a `CaptureVisionRouter` instance and set `CameraEnhancer` instance as its image source.
            cvRouter = await Dynamsoft.CVR.CaptureVisionRouter.createInstance();
            cvRouter.setInput(cameraEnhancer);
    
            // Define a callback for results.
            cvRouter.addResultReceiver({
              onDecodedBarcodesReceived: (result) => {
                console.log(result);
                let barcodeResults = [];
                if (result.barcodeResultItems) {
                  for (let i = 0; i < result.barcodeResultItems.length; i++) {
                    let barcodeResult = result.barcodeResultItems[i];
                    let simpleResult = {
                      barcodeText: barcodeResult.text,
                      barcodeFormat: barcodeResult.formatString,
                    };
                    barcodeResults.push(simpleResult);
                  }
                };
                console.log(barcodeResults);
                if (barcodeResults.length > 0) {
                  // Send the results to the MAUI app.
                  window.HybridWebView.SendRawMessage(JSON.stringify(barcodeResults));
                }
              },
            });
    
            // Filter out unchecked and duplicate results.
            const filter = new Dynamsoft.Utility.MultiFrameResultCrossFilter();
            // Filter out unchecked barcodes.
            filter.enableResultCrossVerification("barcode", true);
            // Filter out duplicate barcodes within 3 seconds.
            filter.enableResultDeduplication("barcode", true);
            await cvRouter.addResultFilter(filter);
    
            // Open camera and start scanning barcode.
            await cameraEnhancer.open();
            cameraView.setScanLaserVisible(true);
            await cvRouter.startCapturing("ReadBarcodes_SpeedFirst");
          } catch (ex) {
            let errMsg = ex.message || ex;
            console.error(errMsg);
            alert(errMsg);
          }
        })();
    
        function stopScanning(){
          cameraView.setScanLaserVisible(false);
          cvRouter.stopCapturing();
          cameraEnhancer.close();
        }
    
        async function startScanning(){
          cameraView.setScanLaserVisible(true);
          await cameraEnhancer.open();
          await cvRouter.startCapturing("ReadBarcodes_SpeedFirst");
        }
      </script>
    </body>
    
    </html>
    
  3. Add a HybridWebView.js file which is provided by Microsoft.

    window.HybridWebView = {
        "Init": function Init() {
            function DispatchHybridWebViewMessage(message) {
                const event = new CustomEvent("HybridWebViewMessageReceived", { detail: { message: message } });
                window.dispatchEvent(event);
            }
    
            if (window.chrome && window.chrome.webview) {
                // Windows WebView2
                window.chrome.webview.addEventListener('message', arg => {
                    DispatchHybridWebViewMessage(arg.data);
                });
            }
            else if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.webwindowinterop) {
                // iOS and MacCatalyst WKWebView
                window.external = {
                    "receiveMessage": message => {
                        DispatchHybridWebViewMessage(message);
                    }
                };
            }
            else {
                // Android WebView
                window.addEventListener('message', arg => {
                    DispatchHybridWebViewMessage(arg.data);
                });
            }
        },
    
        "SendRawMessage": function SendRawMessage(message) {
            window.HybridWebView.__SendMessageInternal('__RawMessage', message);
        },
    
        "InvokeDotNet": async function InvokeDotNetAsync(methodName, paramValues) {
            const body = {
                MethodName: methodName
            };
    
            if (typeof paramValues !== 'undefined') {
                if (!Array.isArray(paramValues)) {
                    paramValues = [paramValues];
                }
    
                for (var i = 0; i < paramValues.length; i++) {
                    paramValues[i] = JSON.stringify(paramValues[i]);
                }
    
                if (paramValues.length > 0) {
                    body.ParamValues = paramValues;
                }
            }
    
            const message = JSON.stringify(body);
    
            var requestUrl = `${window.location.origin}/__hwvInvokeDotNet?data=${encodeURIComponent(message)}`;
    
            const rawResponse = await fetch(requestUrl, {
                method: 'GET',
                headers: {
                    'Accept': 'application/json'
                }
            });
            const response = await rawResponse.json();
    
            if (response) {
                if (response.IsJson) {
                    return JSON.parse(response.Result);
                }
    
                return response.Result;
            }
    
            return null;
        },
    
        "__SendMessageInternal": function __SendMessageInternal(type, message) {
    
            const messageToSend = type + '|' + message;
    
            if (window.chrome && window.chrome.webview) {
                // Windows WebView2
                window.chrome.webview.postMessage(messageToSend);
            }
            else if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.webwindowinterop) {
                // iOS and MacCatalyst WKWebView
                window.webkit.messageHandlers.webwindowinterop.postMessage(messageToSend);
            }
            else {
                // Android WebView
                hybridWebViewHost.sendMessage(messageToSend);
            }
        },
    
        "__InvokeJavaScript": async function __InvokeJavaScript(taskId, methodName, args) {
            try {
                var result = null;
                if (methodName[Symbol.toStringTag] === 'AsyncFunction') {
                    result = await methodName(...args);
                } else {
                    result = methodName(...args);
                }
                window.HybridWebView.__TriggerAsyncCallback(taskId, result);
            } catch (ex) {
                console.error(ex);
                window.HybridWebView.__TriggerAsyncFailedCallback(taskId, ex);
            }
        },
    
        "__TriggerAsyncCallback": function __TriggerAsyncCallback(taskId, result) {
            const json = JSON.stringify(result);
            window.HybridWebView.__SendMessageInternal('__InvokeJavaScriptCompleted', taskId + '|' + json);
        },
    
        "__TriggerAsyncFailedCallback": function __TriggerAsyncCallback(taskId, error) {
    
            if (!error) {
                json = {
                    Message: "Unknown error",
                    StackTrace: Error().stack
                };
            } else if (error instanceof Error) {
                json = {
                    Name: error.name,
                    Message: error.message,
                    StackTrace: error.stack
                };
            } else if (typeof (error) === 'string') {
                json = {
                    Message: error,
                    StackTrace: Error().stack
                };
            } else {
                json = {
                    Message: JSON.stringify(error),
                    StackTrace: Error().stack
                };
            }
    
            json = JSON.stringify(json);
            window.HybridWebView.__SendMessageInternal('__InvokeJavaScriptFailed', taskId + '|' + json);
        }
    }
    
    window.HybridWebView.Init();
    

Use the Barcode Scanner in HybridWebView

In MainPage.xaml, define events for the buttons and the web view and set the default file for the web view to use the web page we wrote.

<StackLayout Orientation="Horizontal" HorizontalOptions="Center">
    <Button Text="Start Scanning" 
        Clicked="OnStartScanButtonClicked"
        VerticalOptions="Center"
        HorizontalOptions="Center"
        />
    <Button Text="Stop Scanning"
        Clicked="OnStopScanButtonClicked"
        VerticalOptions="Center"
        HorizontalOptions="Center"
        />
</StackLayout>

<ScrollView Grid.Row="1">
    <HybridWebView x:Name="hybridWebView" 
   RawMessageReceived="OnHybridWebViewRawMessageReceived"
   HybridRoot="" DefaultFile="index.html" />
</ScrollView>

In MainPage.xaml.cs, implement the events. When a barcode is scanned, stop the barcode scanner. The scanner can be started or stopped by the buttons.

private void OnHybridWebViewRawMessageReceived(object sender, HybridWebViewRawMessageReceivedEventArgs e)
{
    Debug.WriteLine(e.Message);
    MainThread.BeginInvokeOnMainThread(async () =>
    {
        hybridWebView.SendRawMessage("stop");
        await DisplayAlert("Barcode Results Received", e.Message, "OK");
    });
}

private void OnStartScanButtonClicked(object sender, EventArgs args)
{
    hybridWebView.SendRawMessage("start");
}

private void OnStopScanButtonClicked(object sender, EventArgs args)
{
    hybridWebView.SendRawMessage("stop");
}

Source Code

Get the source code of the demo to have a try:

https://github.com/tony-xlh/MAUI-HybridWebView-Camera-Barcode-Scanner