DEV Community

Xiao Ling
Xiao Ling

Posted on • Originally published at dynamsoft.com

How to Build a VIN Scanner App for Android and iOS with .NET MAUI

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 VIN scanner using .NET MAUI and Dynamsoft Capture Vision, covering setup, UI implementation, and VIN recognition logic for Android and iOS.

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
Enter fullscreen mode Exit fullscreen mode

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'" />
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Design the 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>
Enter fullscreen mode Exit fullscreen mode

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;
            });
        }

    }
}
Enter fullscreen mode Exit fullscreen mode

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>

Enter fullscreen mode Exit fullscreen mode

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");
        });
    }
}

Enter fullscreen mode Exit fullscreen mode

Scan VIN in a .NET MAUI app

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>

Enter fullscreen mode Exit fullscreen mode

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; }
}

Enter fullscreen mode Exit fullscreen mode

Parse the VIN result

Source Code

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

Top comments (0)