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:
<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>
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>
The desktop package Dynamsoft.DotNet.BarcodeReader.Bundle
can be added globally without issues:
<PackageReference 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}");
}
}
...
}
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
- 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.
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 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.

2. Select the target device.

3. Press F5
to run the application.

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