How to Build a .NET MAUI VIN Barcode Scanner for Android and iOS
A Vehicle Identification Number (VIN) scanner application is a useful tool for car owners and car manufacturers, enabling quick retrieval of a vehicle’s unique identifier. In this tutorial, we’ll walk through creating a cross-platform .NET MAUI VIN barcode scanner using Dynamsoft Capture Vision, covering setup, UI implementation, and VIN recognition logic for Android and iOS.
What you’ll build: A cross-platform .NET MAUI VIN barcode scanner app for Android and iOS that uses the device camera to detect VIN barcodes in real time and display fully parsed vehicle data including WMI, VDS, model year, and check digit.
Key Takeaways
- This tutorial demonstrates how to build a .NET MAUI VIN barcode scanner that reads VIN barcodes from a live camera feed on both Android and iOS.
- The
Dynamsoft.VIN.MauiNuGet package provides a pre-trained VIN recognition model that decodes and validates all VIN fields (WMI, VDS, check digit, model year, plant code, serial number). - The scan region overlay (
DMRect) constrains recognition to a narrow horizontal band, reducing false positives and improving accuracy in real-world environments. - The same C# codebase compiles to both Android and iOS targets without platform-specific branching for the core scanning logic.
Common Developer Questions
- How do I build a VIN barcode scanner app for Android and iOS with .NET MAUI?
- How do I restrict barcode scanning to a specific region using Dynamsoft CameraEnhancer in MAUI?
- How do I parse and validate VIN fields like WMI, model year, and check digit in C#?
Demo: VIN Scanner App for Android
Prerequisites
Before starting, ensure you have these tools and resources:
- Visual Studio 2022 or Visual Studio Code (with C# support)
- .NET SDK
-
MAUI Workloads configured via CLI:
dotnet workload install maui - A free trial license key for Dynamsoft Capture Vision.
Step 1: Set Up the .NET MAUI Project
1.1 Create a New MAUI Project
Generate a cross-platform MAUI project using the command line:
dotnet new maui -n VINScanner
1.2 Add Platform-Specific Dependencies
Open the project file (VINScanner.csproj) and include these NuGet packages with Android/iOS-only conditions:
<PackageReference
Include="Dynamsoft.CaptureVisionBundle.Maui"
Version="2.6.1001"
Condition="'$(TargetFramework)' == 'net9.0-android' OR '$(TargetFramework)' == 'net9.0-ios'" />
<PackageReference
Include="Dynamsoft.VIN.Maui"
Version="3.4.201"
Condition="'$(TargetFramework)' == 'net9.0-android' OR '$(TargetFramework)' == 'net9.0-ios'" />
- Dynamsoft.CaptureVisionBundle.Maui: Core image processing and camera handling.
- Dynamsoft.VIN.Maui: Pre-trained VIN recognition model for accurate character extraction.
1.3 Configure Target Frameworks
To avoid Windows/macOS compatibility issues, restrict the project to mobile targets in VINScanner.csproj:
<TargetFrameworks>net9.0-android;net9.0-ios;</TargetFrameworks>
1.4 Register Camera View Handler
In MauiProgram.cs, register the CameraView handler for cross-platform camera integration:
using Microsoft.Extensions.Logging;
using Dynamsoft.CameraEnhancer.Maui;
using Dynamsoft.CameraEnhancer.Maui.Handlers;
namespace VINScanner;
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
})
.ConfigureMauiHandlers(handlers =>
{
handlers.AddHandler(typeof(CameraView), typeof(CameraViewHandler));
});
#if DEBUG
builder.Logging.AddDebug();
#endif
return builder.Build();
}
}
Step 2: Build the Three-Page App Architecture
The app features three core pages:
MainPage.xaml: Entry point with a scan initiation button.CameraPage.xaml: Live camera feed with a VIN scanning region overlay.ResultPage.xaml: Displays parsed VIN details in a structured format.
2.1 Implement the Main Page (Entry Point)
UI Layout (MainPage.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="VINScanner.MainPage">
<StackLayout VerticalOptions="Center"
HorizontalOptions="Center">
<Button
x:Name="CameraBtn"
Text="Start Scanning"
SemanticProperties.Hint="Open Camera"
Clicked="OnCameraClicked"
HorizontalOptions="CenterAndExpand"
VerticalOptions="CenterAndExpand"/>
<Label
x:Name="errorMessage"
TextColor="Red"
VerticalOptions="Center"
HorizontalOptions="Center"
FontSize="Medium"
Margin="0,20,0,0"/>
</StackLayout>
</ContentPage>
License Initialization (MainPage.xaml.cs)
using Dynamsoft.License.Maui;
namespace VINScanner;
public partial class MainPage : ContentPage, ILicenseVerificationListener
{
public MainPage()
{
InitializeComponent();
LicenseManager.InitLicense("LICENSE-KEY", this);
}
private async void OnCameraClicked(object sender, EventArgs e)
{
await Navigation.PushAsync(new CameraPage());
}
public void OnLicenseVerified(bool isSuccess, string message)
{
if (!isSuccess)
{
MainThread.BeginInvokeOnMainThread(() =>
{
errorMessage.Text = "License initialization failed: " + message;
});
}
}
}
2.2 Build the Camera Page (Live Scanning)
Camera Feed & Capture Button (CameraPage.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"
xmlns:controls="clr-namespace:Dynamsoft.CameraEnhancer.Maui;assembly=Dynamsoft.CameraEnhancer.Maui"
x:Class="VINScanner.CameraPage"
Title="VINScanner">
<AbsoluteLayout>
<controls:CameraView x:Name="camera"
AbsoluteLayout.LayoutBounds="0,0,1,1"
AbsoluteLayout.LayoutFlags="All"/>
<Button Text="Capture"
AbsoluteLayout.LayoutBounds="0.5, 0.8, 0.8, 0.1"
AbsoluteLayout.LayoutFlags="All"
HorizontalOptions="Center"
VerticalOptions="End"
Clicked="OnCaptureClicked"/>
</AbsoluteLayout>
</ContentPage>
Scanning Logic & Result Handling (CameraPage.xaml.cs)
using Dynamsoft.Core.Maui;
using Dynamsoft.CaptureVisionRouter.Maui;
using Dynamsoft.CameraEnhancer.Maui;
using Dynamsoft.CodeParser.Maui;
namespace VINScanner;
public partial class CameraPage : ContentPage, ICapturedResultReceiver, ICompletionListener
{
public CameraEnhancer enhancer = new CameraEnhancer();
CaptureVisionRouter router = new CaptureVisionRouter();
bool isCaptured = false;
public CameraPage()
{
InitializeComponent();
router.SetInput(enhancer);
router.AddResultReceiver(this);
}
protected override void OnHandlerChanged()
{
base.OnHandlerChanged();
if (this.Handler != null)
{
enhancer.SetCameraView(camera);
var region = new DMRect(0.1f, 0.4f, 0.9f, 0.6f, true);
enhancer.SetScanRegion(region);
}
}
protected override async void OnAppearing()
{
isCaptured = false;
base.OnAppearing();
await Permissions.RequestAsync<Permissions.Camera>();
enhancer.Open();
router.StartCapturing("ReadVIN", this);
}
protected override void OnDisappearing()
{
base.OnDisappearing();
enhancer.Close();
router.StopCapturing();
}
public void OnParsedResultsReceived(ParsedResult result)
{
if (result?.Items?.Count > 0)
{
ParsedResultItem parsedResultItem = result.Items[0];
if (result.Items.Count > 1)
{
foreach (var item in result.Items)
{
if (item.TaskName == "parse-vin-barcode")
{
parsedResultItem = item;
break;
}
}
}
var dictionary = ConvertToVINDictionary(parsedResultItem);
if (dictionary != null && isCaptured)
{
router.StopCapturing();
enhancer.ClearBuffer();
MainThread.BeginInvokeOnMainThread(async () =>
{
await Navigation.PushAsync(new ResultPage(dictionary));
});
}
}
}
private void OnCaptureClicked(object sender, EventArgs e)
{
isCaptured = true;
}
public Dictionary<string, string>? ConvertToVINDictionary(ParsedResultItem item)
{
if (item.ParsedFields.TryGetValue("vinString", out ParsedField? value) && value != null)
{
Dictionary<string, string> dic = [];
string[] infoLists = ["vinString", "WMI", "region", "VDS", "checkDigit", "modelYear", "plantCode", "serialNumber"];
foreach (var info in infoLists)
{
if (item.ParsedFields.TryGetValue(info, out ParsedField? field) && field != null)
{
if (item.ParsedFields[info].ValidationStatus == EnumValidationStatus.VS_FAILED)
{
return null;
}
else
{
dic.Add(CapitalizeFirstLetter(info), item.ParsedFields[info].Value);
}
}
}
return dic;
}
else
{
return null;
}
}
public static string CapitalizeFirstLetter(string input)
{
if (string.IsNullOrWhiteSpace(input))
return input;
return char.ToUpper(input[0]) + input.Substring(1);
}
public void OnFailure(int errorCode, string errorMessage)
{
MainThread.BeginInvokeOnMainThread(() =>
{
DisplayAlert("Error", errorMessage, "OK");
});
}
}

2.3 Display VIN Results with Structured Data
Data Presentation UI (ResultPage.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="VINScanner.ResultPage"
Title="VIN Result">
<ContentPage.Content>
<CollectionView ItemsSource="{Binding TableItems}">
<CollectionView.ItemTemplate>
<DataTemplate>
<VerticalStackLayout Padding="10">
<Label Text="{Binding Key}"
FontAttributes="Bold"
TextColor="Black"
FontSize="16"/>
<BoxView HeightRequest="1"
BackgroundColor="LightGray"
Margin="0,5"/>
<Label Text="{Binding Value}"
FontAttributes="None"
TextColor="Gray"
FontSize="14"/>
<BoxView HeightRequest="1"
BackgroundColor="LightGray"
Margin="0,5"/>
</VerticalStackLayout>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</ContentPage.Content>
</ContentPage>
Data Binding Logic (ResultPage.xaml.cs)
using System.Collections.ObjectModel;
namespace VINScanner;
public partial class ResultPage : ContentPage
{
public ObservableCollection<TableItem> TableItems { get; set; }
public ResultPage(Dictionary<String, String> dictionary)
{
InitializeComponent();
TableItems = [];
foreach (var item in dictionary)
{
TableItems.Add(new TableItem { Key = item.Key, Value = item.Value });
}
BindingContext = this;
}
}
public class TableItem
{
public string Key { get; set; }
public string Value { get; set; }
}

Common Issues & Edge Cases
- VIN barcode not detected: VIN barcodes on vehicle dashboards and windshields are typically Code 39 or Code 128. If scanning fails, confirm the
DMRectscan region fully covers the barcode and ensure adequate ambient lighting. Scanning through tinted glass significantly raises the failure rate. VS_FAILEDvalidation status returned:ConvertToVINDictionaryreturnsnullwhen any VIN field fails validation, preventing display of corrupted data. This is expected for physically damaged barcodes or partial reads caused by motion blur or glare — prompt the user to retake the capture.- Camera permission denied on Android: The app requests camera permission in
OnAppearing. If the user denies it, Android will not re-prompt automatically after the first denial — guide the user to enable the permission manually via the device’s application settings.
Source Code
https://github.com/yushulx/maui-barcode-mrz-document-scanner/tree/main/examples/VINScanner