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 Install the .NET 9.0 SDK. Obtain a trial license key for Dynamsoft Barcode Reader. 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: net9.0-android;net9.0-ios $(TargetFrameworks);net9.0-windows10.0.19041.0 Exe BarcodeQrScanner true true enable enable BarcodeQrScanner com.companyname.barcodeqrscanner 1.0 1 None 15.0 21.0 10.0.19041.0 10.0.19041.0 6.5 Next, conditionally include the mobile-specific NuGet package Dynamsoft.CaptureVisionBundle.Maui: The desktop package Dynamsoft.DotNet.BarcodeReader.Bundle can be added globally without issues: 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 } } 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)); } } 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 Reuse the PicturePage and CameraPage code from the

.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
- Install the .NET 9.0 SDK.
- Obtain a trial license key for Dynamsoft Barcode Reader.
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:
net9.0-android;net9.0-ios
Condition="$([MSBuild]::IsOSPlatform('windows'))">$(TargetFrameworks);net9.0-windows10.0.19041.0
Exe
BarcodeQrScanner
true
true
enable
enable
BarcodeQrScanner
com.companyname.barcodeqrscanner
1.0
1
None
Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">15.0
Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">21.0
Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.19041.0
Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.19041.0
Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'tizen'">6.5
Next, conditionally include the mobile-specific NuGet package Dynamsoft.CaptureVisionBundle.Maui
:
Condition="'$(TargetFramework)' == 'net9.0-android' Or '$(TargetFramework)' == 'net9.0-ios'">
Include="Dynamsoft.CaptureVisionBundle.Maui" Version="2.6.1000" />
The desktop package Dynamsoft.DotNet.BarcodeReader.Bundle
can be added globally without issues:
Include="Dynamsoft.DotNet.BarcodeReader.Bundle" Version="10.4.2000" />
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
}
}
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 bySKCanvasView
). -
iOS: Uses
SKCanvasView
(resolves text-rendering issues inGraphicsView
).
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));
}
}
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
- Reuse the
PicturePage
andCameraPage
code from thehttps://github.com/yushulx/maui-barcode-mrz-document-scanner/tree/main/examples/BarcodeQrScanner
asAndroidPicturePage
andAndroidCameraPage
. -
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}"); } } ... }
-
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
-
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>
-
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
-
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.
-
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 theSKCanvasView
. UseOnCanvasViewPaintSurface
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
-
In Visual Studio Code, click the curly brackets icon at the bottom.
-
Select the target device.
-
Press
F5
to run the application.
Source Code
https://github.com/yushulx/maui-barcode-mrz-document-scanner/tree/main/examples/WindowsDesktop