DEV Community

Xiao Ling
Xiao Ling

Posted on • Originally published at dynamsoft.com

Cross-Platform Barcode Scanner with .NET MAUI: Merging Mobile and Desktop Projects

.NET MAUI is designed for cross-platform development, but achieving seamless compatibility across all platforms isn't always straightforward. While developers might assume cross-platform apps are easy to build with .NET MAUI, many existing libraries were initially tailored for Xamarin and remain limited to Android and iOS. Creating a unified .NET MAUI project for desktop and mobile requires addressing platform-specific challenges. For instance, Dynamsoft's barcode SDKs are split into two NuGet packages: Dynamsoft.DotNet.BarcodeReader.Bundle (for Windows desktop ) and Dynamsoft.CaptureVisionBundle.Maui (for mobile ), which do not provide unified APIs. This article explains how to merge MAUI desktop barcode scanner and MAUI mobile barcode scanner into a single project supporting Windows, Android, and iOS.

iOS Barcode Scanner in .NET MAUI

Prerequisites

Configuring the *.csproj File for Windows, Android and iOS Build

First, remove macOS from the target frameworks to avoid build conflicts when compiling for iOS on macOS:

<PropertyGroup>
        <TargetFrameworks>net9.0-android;net9.0-ios</TargetFrameworks>
        <TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows'))">$(TargetFrameworks);net9.0-windows10.0.19041.0</TargetFrameworks>

        <OutputType>Exe</OutputType>
        <RootNamespace>BarcodeQrScanner</RootNamespace>
        <UseMaui>true</UseMaui>
        <SingleProject>true</SingleProject>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>

        <ApplicationTitle>BarcodeQrScanner</ApplicationTitle>

        <ApplicationId>com.companyname.barcodeqrscanner</ApplicationId>

        <ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
        <ApplicationVersion>1</ApplicationVersion>

        <WindowsPackageType>None</WindowsPackageType>

        <SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">15.0</SupportedOSPlatformVersion>
        <!-- <SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'maccatalyst'">15.0</SupportedOSPlatformVersion> -->
        <SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">21.0</SupportedOSPlatformVersion>
        <SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.19041.0</SupportedOSPlatformVersion>
        <TargetPlatformMinVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.19041.0</TargetPlatformMinVersion>
        <SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'tizen'">6.5</SupportedOSPlatformVersion>
    </PropertyGroup>
Enter fullscreen mode Exit fullscreen mode

Next, conditionally include the mobile-specific NuGet package Dynamsoft.CaptureVisionBundle.Maui:

<ItemGroup Condition="'$(TargetFramework)' == 'net9.0-android' Or '$(TargetFramework)' == 'net9.0-ios'">
    <PackageReference Include="Dynamsoft.CaptureVisionBundle.Maui" Version="2.6.1000" />
</ItemGroup>
Enter fullscreen mode Exit fullscreen mode

The desktop package Dynamsoft.DotNet.BarcodeReader.Bundle can be added globally without issues:

<PackageReference Include="Dynamsoft.DotNet.BarcodeReader.Bundle" Version="10.4.2000" />
Enter fullscreen mode Exit fullscreen mode

Platform-Specific Code with Preprocessor Directives

Use #if directives to isolate code for Android/iOS and Windows:

#if ANDROID || IOS
using Dynamsoft.License.Maui;
#endif


public partial class MainPage : ContentPage
{
#if ANDROID || IOS
    class LicenseVerificationListener : ILicenseVerificationListener
    {
        public void OnLicenseVerified(bool isSuccess, string message)
        {
            if (!isSuccess)
            {
                Debug.WriteLine(message);
            }
        }
    }
#endif

    public MainPage()
    {
        InitializeComponent();

#if ANDROID || IOS
        LicenseManager.InitLicense("LICENSE-KEY", new LicenseVerificationListener());
#endif
    }
}
Enter fullscreen mode Exit fullscreen mode

Handling Platform-Specific UI Components

To manage barcode scanning from files or cameras, we create four pages due to rendering differences:

  • AndroidPicturePage.xaml / iOSPicturePage.xaml: Handle image-based barcode detection.
  • AndroidCameraPage.xaml / iOSCameraPage.xaml: Enable real-time camera scanning.

This separation is necessary because:

  • Android: Uses GraphicsView (avoids crashes caused by SKCanvasView).
  • iOS: Uses SKCanvasView (resolves text-rendering issues in GraphicsView).
private async void OnFileButtonClicked(object sender, EventArgs e)
{
    try
    {

        FileResult? photo = null;
        if (DeviceInfo.Current.Platform == DevicePlatform.WinUI || DeviceInfo.Current.Platform == DevicePlatform.MacCatalyst)
        {
            photo = await FilePicker.PickAsync();
        }
        else if (DeviceInfo.Current.Platform == DevicePlatform.Android || DeviceInfo.Current.Platform == DevicePlatform.iOS)
        {
            photo = await MediaPicker.CapturePhotoAsync();
        }
        await LoadPhotoAsync(photo);
    }
    catch (Exception ex)
    {
        Debug.WriteLine($"CapturePhotoAsync THREW: {ex.Message}");
    }
}

private async void OnCameraButtonClicked(object sender, EventArgs e)
{
    if (DeviceInfo.Current.Platform == DevicePlatform.Android)
    {
        await Navigation.PushAsync(new AndroidCameraPage());
    }
    else if (DeviceInfo.Current.Platform == DevicePlatform.iOS)
    {
        await Navigation.PushAsync(new iOSCameraPage());
    }
    else
    {
        await Navigation.PushAsync(new CameraPage());
    }

}

async Task LoadPhotoAsync(FileResult? photo)
{
    if (photo == null)
    {
        return;
    }

    if (DeviceInfo.Current.Platform == DevicePlatform.Android)
    {
        await Navigation.PushAsync(new AndroidPicturePage(photo));
    }
    else if (DeviceInfo.Current.Platform == DevicePlatform.iOS)
    {
        await Navigation.PushAsync(new iOSPicturePage(photo));
    }
    else
    {
        await Navigation.PushAsync(new PicturePage(photo.FullPath));
    }
}
Enter fullscreen mode Exit fullscreen mode

Why Not Use a Single Page for All Platforms?

While SKCanvasView and GraphicsView are cross-platform in theory, they exhibit critical bugs:

  • Android: SKCanvasView causes app crashes and black screens.
  • iOS: GraphicsView fails to render text overlays. Using platform-specific pages ensures stability and performance.

Implementing Picture and Camera Pages for Android

  1. Reuse the PicturePage and CameraPage code from the https://github.com/yushulx/maui-barcode-mrz-document-scanner/tree/main/examples/BarcodeQrScanner as AndroidPicturePage and AndroidCameraPage.
  2. Add platform directives to AndroidPicturePage.xaml.cs.

    #if ANDROID || IOS
    using Dynamsoft.CaptureVisionRouter.Maui;
    using Dynamsoft.BarcodeReader.Maui;
    #endif
    using SkiaSharp;
    using System.Diagnostics;
    using Microsoft.Maui.Graphics.Platform;
    
    namespace BarcodeQrScanner;
    
    public partial class AndroidPicturePage : ContentPage
    {
    #if ANDROID || IOS
        private CaptureVisionRouter router = new CaptureVisionRouter();
    #endif
    
        ...
    
        async private void LoadImageWithOverlay(FileResult result)
        {
            var filePath = result.FullPath;
            var stream = await result.OpenReadAsync();
    
            float originalWidth = 0;
            float originalHeight = 0;
    
            try
            {
                ...
    
    #if ANDROID || IOS
                var streamcopy = await result.OpenReadAsync();
                byte[] filestream = new byte[streamcopy.Length];
                int offset = 0;
                while (offset < filestream.Length)
                {
                    int bytesRead = streamcopy.Read(filestream, offset, filestream.Length - offset);
                    if (bytesRead == 0)
                        break;
                    offset += bytesRead;
                }
    
                streamcopy.Close();
                if (offset != filestream.Length)
                {
                    throw new IOException("Could not read the entire stream.");
                }
                CapturedResult capturedResult = router.Capture(filestream, EnumPresetTemplate.PT_READ_BARCODES);
                DecodedBarcodesResult? barcodeResults = null;
    
                if (capturedResult != null)
                {
                    barcodeResults = capturedResult.DecodedBarcodesResult;
                }
    
                var drawable = new ImageWithOverlayDrawable(barcodeResults, originalWidth, originalHeight, true);
    
                OverlayGraphicsView.Drawable = drawable;
                OverlayGraphicsView.Invalidate(); 
    #endif
            }
            catch (Exception ex)
            {
                Console.WriteLine($"An error occurred: {ex.Message}");
            }
        }
    
        ...
    }
    
  3. Add directives to AndroidCameraPage.xaml.cs.

    
    namespace BarcodeQrScanner;
    
    #if ANDROID || IOS
    using Dynamsoft.Core.Maui;
    using Dynamsoft.CaptureVisionRouter.Maui;
    using Dynamsoft.BarcodeReader.Maui;
    using Dynamsoft.CameraEnhancer.Maui;
    using System.Diagnostics;
    using CommunityToolkit.Mvvm.Messaging;
    
    public partial class AndroidCameraPage : ContentPage, ICapturedResultReceiver, ICompletionListener
    {
        ...
    }
    
    #endif
    

Implementing Picture and Camera Pages for iOS

As mentioned earlier, the GraphicsView has some UI rendering issues. To resolve this issue, we use SKCanvasView instead.

Picture Page for iOS

  1. Add the following layout code to iOSPicturePage.xaml:

    <?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="BarcodeQrScanner.iOSPicturePage"
                 xmlns:skia="clr-namespace:SkiaSharp.Views.Maui.Controls;assembly=SkiaSharp.Views.Maui.Controls"
                 Title="iOSPicturePage">
    
        <ContentPage.Content>
            <Grid>
                <skia:SKCanvasView x:Name="canvasView"
                                   HorizontalOptions="Fill"
                                   VerticalOptions="Fill"
                                   PaintSurface="OnCanvasViewPaintSurface"/>
                <Label FontSize="18"
                       FontAttributes="Bold"
                       x:Name="ResultLabel"
                       Text=""
                       TextColor="Red"
                       HorizontalOptions="Center"
                       VerticalOptions="Center"/>
    
            </Grid>
        </ContentPage.Content>
    
    </ContentPage>
    
  2. In iOSPicturePage.xaml.cs, follow these steps:

    • Decode an image file to SKBitmap:

      var stream = await fileResult.OpenReadAsync();
      bitmap = SKBitmap.Decode(stream);
      
    • Read barcodes from the image stream:

      private CaptureVisionRouter router = new CaptureVisionRouter();
      stream = await fileResult.OpenReadAsync();
      byte[] filestream = new byte[stream.Length];
      int offset = 0;
      while (offset < filestream.Length)
      {
          int bytesRead = stream.Read(filestream, offset, filestream.Length - offset);
          if (bytesRead == 0)
              break;
          offset += bytesRead;
      }
      
      stream.Close();
      if (offset != filestream.Length)
      {
          throw new IOException("Could not read the entire stream.");
      }
      
      result = router.Capture(filestream, EnumPresetTemplate.PT_READ_BARCODES);
      
    • Render the bitmap and barcode results on SKCanvasView:

          void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
          {
              if (!isDataReady)
              {
                  return;
              }
              SKImageInfo info = args.Info;
              SKSurface surface = args.Surface;
              SKCanvas canvas = surface.Canvas;
              canvas.Clear();
      
              if (bitmap != null)
              {
                  var imageCanvas = new SKCanvas(bitmap);
      
                  float textSize = 28;
                  float StrokeWidth = 4;
      
                  if (DeviceInfo.Current.Platform == DevicePlatform.Android || DeviceInfo.Current.Platform == DevicePlatform.iOS)
                  {
                      textSize = (float)(18 * DeviceDisplay.MainDisplayInfo.Density);
                      StrokeWidth = 4;
                  }
      
                  SKPaint skPaint = new SKPaint
                  {
                      Style = SKPaintStyle.Stroke,
                      Color = SKColors.Blue,
                      StrokeWidth = StrokeWidth,
                  };
      
                  SKPaint textPaint = new SKPaint
                  {
                      Style = SKPaintStyle.Stroke,
                      Color = SKColors.Red,
                      StrokeWidth = StrokeWidth,
                  };
      
                  SKFont font = new SKFont() { Size = textSize };
      #if ANDROID || IOS
                  if (isDataReady)
                  {
                      if (result != null)
                      {
                          ResultLabel.Text = "";
                          DecodedBarcodesResult? barcodesResult = result.DecodedBarcodesResult;
                          if (barcodesResult != null)
                          {
                              var items = barcodesResult.Items;
                              foreach (var barcodeItem in items)
                              {
                                  Microsoft.Maui.Graphics.Point[] points = barcodeItem.Location.Points;
                                  imageCanvas.DrawText(barcodeItem.Text, (float)points[0].X, (float)points[0].Y, SKTextAlign.Left, font, textPaint);
                                  imageCanvas.DrawLine((float)points[0].X, (float)points[0].Y, (float)points[1].X, (float)points[1].Y, skPaint);
                                  imageCanvas.DrawLine((float)points[1].X, (float)points[1].Y, (float)points[2].X, (float)points[2].Y, skPaint);
                                  imageCanvas.DrawLine((float)points[2].X, (float)points[2].Y, (float)points[3].X, (float)points[3].Y, skPaint);
                                  imageCanvas.DrawLine((float)points[3].X, (float)points[3].Y, (float)points[0].X, (float)points[0].Y, skPaint);
                              }
                          }
                      }
                      else
                      {
                          ResultLabel.Text = "No 1D/2D barcode found";
                      }
                  }
      #endif
      
                  float scale = Math.Min((float)info.Width / bitmap.Width,
                                     (float)info.Height / bitmap.Height);
                  float x = (info.Width - scale * bitmap.Width) / 2;
                  float y = (info.Height - scale * bitmap.Height) / 2;
                  SKRect destRect = new SKRect(x, y, x + scale * bitmap.Width,
                                                     y + scale * bitmap.Height);
      
                  canvas.DrawBitmap(bitmap, destRect);
              }
      
          }
      

Camera Page for iOS

  1. Add the following layout code to iOSCameraPage.xaml:

    <?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="BarcodeQrScanner.iOSCameraPage"
                 xmlns:skia="clr-namespace:SkiaSharp.Views.Maui.Controls;assembly=SkiaSharp.Views.Maui.Controls"
                 Title="iOSCameraPage">
    
        <Grid x:Name="MainGrid"
              Margin="0">
            <skia:SKCanvasView x:Name="canvasView"
                               Margin="0"
                               HorizontalOptions="FillAndExpand"
                               VerticalOptions="FillAndExpand"/>
        </Grid>
    
    </ContentPage>
    

    Note: Do not place the camera preview control here. Instead, add it dynamically in the code-behind file to avoid build failures.

  2. In iOSCameraPage.xaml.cs, implement the camera preview and barcode scanning:

    • Initialize the camera preview and barcode scanner. Insert the camera preview control into MainGrid below the SKCanvasView. Use OnCanvasViewPaintSurface to render barcode results.

      public iOSCameraPage()
      {
          InitializeComponent();
      
          canvasView.PaintSurface += OnCanvasViewPaintSurface;
      
          if (DeviceInfo.Platform == DevicePlatform.Android ||
                  DeviceInfo.Platform == DevicePlatform.iOS)
          {
              CameraPreview = new Dynamsoft.CameraEnhancer.Maui.CameraView();
              MainGrid.Children.Insert(0, CameraPreview);
          }
      
          enhancer = new CameraEnhancer();
          router = new CaptureVisionRouter();
          router.SetInput(enhancer);
          router.AddResultReceiver(this);
      
          WeakReferenceMessenger.Default.Register<LifecycleEventMessage>(this, (r, message) =>
          {
              if (message.EventName == "Resume")
              {
                  if (this.Handler != null && enhancer != null)
                  {
                      enhancer.Open();
                  }
              }
              else if (message.EventName == "Stop")
              {
                  enhancer?.Close();
              }
          });
      }
      
    • Receive barcode results in a callback function and trigger SKCanvasView to render them.

          public void OnDecodedBarcodesReceived(DecodedBarcodesResult result)
          {
              if (imageWidth == 0 && imageHeight == 0)
              {
                  IntermediateResultManager manager = router.GetIntermediateResultManager();
                  ImageData data = manager.GetOriginalImage(result.OriginalImageHashId);
      
                  imageWidth = data.Width;
                  imageHeight = data.Height;
              }
      
              lock (_lockObject)
              {
                  _barcodeResult = result;
                  CameraPreview.GetDrawingLayer(EnumDrawingLayerId.DLI_DBR).Visible = false;
      
                  MainThread.BeginInvokeOnMainThread(() =>
                  {
                      canvasView.InvalidateSurface();
                  });
              }
      
          }
      
    • Render the result overlay in the OnCanvasViewPaintSurface event handler.

          void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
          {
              double width = canvasView.Width;
              double height = canvasView.Height;
      
              var mainDisplayInfo = DeviceDisplay.MainDisplayInfo;
              var orientation = mainDisplayInfo.Orientation;
              var rotation = mainDisplayInfo.Rotation;
              var density = mainDisplayInfo.Density;
      
              width *= density;
              height *= density;
      
              double scale, widthScale, heightScale, scaledWidth, scaledHeight;
              double previewWidth, previewHeight;
              if (orientation == DisplayOrientation.Portrait)
              {
                  previewWidth = imageWidth;
                  previewHeight = imageHeight;
              }
              else
              {
                  previewWidth = imageHeight;
                  previewHeight = imageWidth;
              }
      
              widthScale = previewWidth / width;
              heightScale = previewHeight / height;
              scale = widthScale < heightScale ? widthScale : heightScale;
              scaledWidth = previewWidth / scale;
              scaledHeight = previewHeight / scale;
      
              SKImageInfo info = args.Info;
              SKSurface surface = args.Surface;
              SKCanvas canvas = surface.Canvas;
      
              canvas.Clear();
      
              SKPaint skPaint = new SKPaint
              {
                  Style = SKPaintStyle.Stroke,
                  Color = SKColors.Blue,
                  StrokeWidth = 4,
              };
      
              SKPaint textPaint = new SKPaint
              {
                  Style = SKPaintStyle.Stroke,
                  Color = SKColors.Red,
                  StrokeWidth = 4,
              };
      
              float textSize = 18;
              SKFont font = new SKFont() { Size = textSize };
      
              lock (_lockObject)
              {
                  if (_barcodeResult != null)
                  {
                      DecodedBarcodesResult? barcodesResult = _barcodeResult;
                      if (barcodesResult != null)
                      {
                          var items = barcodesResult.Items;
                          if (items != null)
                          {
                              foreach (var barcodeItem in items)
                              {
                                  Microsoft.Maui.Graphics.Point[] points = barcodeItem.Location.Points;
      
                                  float x1 = (float)(points[0].X / scale);
                                  float y1 = (float)(points[0].Y / scale);
                                  float x2 = (float)(points[1].X / scale);
                                  float y2 = (float)(points[1].Y / scale);
                                  float x3 = (float)(points[2].X / scale);
                                  float y3 = (float)(points[2].Y / scale);
                                  float x4 = (float)(points[3].X / scale);
                                  float y4 = (float)(points[3].Y / scale);
      
                                  if (widthScale < heightScale)
                                  {
                                      y1 = (float)(y1 - (scaledHeight - height) / 2);
                                      y2 = (float)(y2 - (scaledHeight - height) / 2);
                                      y3 = (float)(y3 - (scaledHeight - height) / 2);
                                      y4 = (float)(y4 - (scaledHeight - height) / 2);
                                  }
                                  else
                                  {
                                      x1 = (float)(x1 - (scaledWidth - width) / 2);
                                      x2 = (float)(x2 - (scaledWidth - width) / 2);
                                      x3 = (float)(x3 - (scaledWidth - width) / 2);
                                      x4 = (float)(x4 - (scaledWidth - width) / 2);
                                  }
      
                                  canvas.DrawText(barcodeItem.Text, x1, y1 - 10, SKTextAlign.Left, font, textPaint);
                                  canvas.DrawLine(x1, y1, x2, y2, skPaint);
                                  canvas.DrawLine(x2, y2, x3, y3, skPaint);
                                  canvas.DrawLine(x3, y3, x4, y4, skPaint);
                                  canvas.DrawLine(x4, y4, x1, y1, skPaint);
                              }
                          }
      
                      }
                  }
              }
          }
      

Running the .NET MAUI Barcode Scanner on Windows, Android, and iOS

  1. In Visual Studio Code, click the curly brackets icon at the bottom.

    .NET MAUI Debug Target

  2. Select the target device.

    .NET MAUI Debug Target

  3. Press F5 to run the application.

    .NET MAUI iOS Barcode Scanner

Source Code

https://github.com/yushulx/maui-barcode-mrz-document-scanner/tree/main/examples/WindowsDesktop

Top comments (0)