Building .NET MAUI Barcode Scanner with Visual Studio Code on macOS
Tags: dotnet, maui, csharp, android
Published: 2024-07-03 08:48:23

Recently, Dynamsoft released a new **.NET MAUI Barcode SDK** for building barcode scanning applications on **Android** and **iOS**. In this tutorial, we will use **Visual Studio Code** to create a .NET MAUI Barcode Scanner from scratch. Our application will decode barcodes and QR codes from both image files and camera video stream.

<video src="https://github.com/yushulx/maui-barcode-qrcode-scanner/assets/2202306/b76aae4d-cc59-4370-a2ba-df8d46532713" controls="controls" muted="muted" style="max-height:640px;max-width:100%;"></video>


## Prerequisites
To get started, you'll need to install the following tools:

- [.NET 8.0 SDK](https://dotnet.microsoft.com/download/dotnet/8.0)
- [Visual Studio Code](https://code.visualstudio.com/)
- [.NET MAUI extension for Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.dotnet-maui)
- [C# Kit](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csharp)
- [Android SDK](https://developer.android.com/studio)
- [iOS SDK](https://developer.apple.com/xcode/)

For detailed installation instructions, refer to the [Microsoft tutorial](https://learn.microsoft.com/en-us/dotnet/maui/get-started/installation?view=net-maui-8.0&tabs=visual-studio-code).

### Why Not Visual Studio for Mac?
Microsoft has announced the retirement of Visual Studio for Mac, with support ending on **August 31, 2024**. The new .NET MAUI extension for Visual Studio Code offers a superior development experience for cross-platform applications.

## Step 1: Scaffold a .NET MAUI Project
Create a new .NET MAUI project in Visual Studio Code:
1. Open the command palette by pressing `Cmd + Shift + P` or `F1`.
2. Type `> .NET: New Project` and press `Enter`.
3. Select `.NET MAUI App` and press `Enter`.
4. Enter the project name and choose the location to save the project.
     

To run the project on an Android device or iOS device:
1. Open the command palette and type `> .NET MAUI: Pick Android Device` or `> .NET MAUI: Pick iOS Device` to select a device.
    
    ![Pick Android Device](https://www.dynamsoft.com/codepool/img/2024/07/visual-studio-code-dotnet-maui-development.png)

2. Press `F5` to build and run the project.

## Step 2: Install Dependencies for Barcode Detection and Android Lifecycle Notifications
To enable barcode detection and handle Android lifecycle notifications, install the following NuGet packages:
- [Dynamsoft.BarcodeReaderBundle.Maui](https://www.nuget.org/packages/Dynamsoft.BarcodeReaderBundle.Maui): A .NET MAUI barcode SDK.
- [CommunityToolkit.Mvvm](https://www.nuget.org/packages/CommunityToolkit.Mvvm): A messaging library for Android lifecycle notifications.

Run the following commands to add these packages to your project:

```bash
dotnet add package Dynamsoft.BarcodeReaderBundle.Maui
dotnet add package CommunityToolkit.Mvvm
```

Next, configure the dependencies in the `MauiProgram.cs` file:

```csharp
using Microsoft.Extensions.Logging;
using Dynamsoft.CameraEnhancer.Maui;
using Dynamsoft.CameraEnhancer.Maui.Handlers;
using CommunityToolkit.Maui;
using Microsoft.Maui.LifecycleEvents;
using CommunityToolkit.Mvvm.Messaging;

namespace BarcodeQrScanner;

public static class MauiProgram
{
	public static MauiApp CreateMauiApp()
	{
		var builder = MauiApp.CreateBuilder();
		builder
			.UseMauiApp<App>().UseMauiCommunityToolkit()
			.ConfigureFonts(fonts =>
			{
				fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
				fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
			})
			.ConfigureLifecycleEvents(events =>
                            {
#if ANDROID
                                events.AddAndroid(android => android
                                    .OnResume((activity) =>
                                    {
										NotifyPage("Resume");
									})
                                    .OnStop((activity) =>
                                    {
										NotifyPage("Stop");
                                    }));
#endif
                            })
							.ConfigureMauiHandlers(handlers =>
            {
                handlers.AddHandler(typeof(CameraView), typeof(CameraViewHandler));
            });

#if DEBUG
		builder.Logging.AddDebug();
#endif

		return builder.Build();
	}

	private static void NotifyPage(string eventName)
	{
		WeakReferenceMessenger.Default.Send(new LifecycleEventMessage(eventName));
	}
}

```

## Step 3: Add Permission Descriptions for Android and iOS
To enable the application to pick images from the gallery and access the camera, you need to add permission descriptions in the `AndroidManifest.xml` and `Info.plist` files.

**Android**

Add the following permissions to your `AndroidManifest.xml` file:

```xml
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.CAMERA" />
```

**iOS**

Add the following keys to your Info.plist file to describe why your app needs access to the photo library, camera, and microphone:

```xml
<key>NSPhotoLibraryUsageDescription</key>
<string>App needs access to the photo library to pick images.</string>
<key>NSCameraUsageDescription</key>
<string>This app is using the camera</string>
<key>NSMicrophoneUsageDescription</key>
<string>This app needs access to microphone for taking videos.</string>
```

By adding these permission descriptions, you ensure that your application has the necessary access to the device's camera and photo library, complying with Android and iOS security requirements.

## Step 4: Activate Dynamsoft Barcode Reader SDK
To use the Dynamsoft Barcode Reader SDK, you need to activate it with a valid license key in the `MainPage.xaml.cs` file. You can obtain a [30-day free trial license](https://www.dynamsoft.com/customer/license/trialLicense/?product=dbr) from Dynamsoft.

```csharp
public partial class MainPage : ContentPage, ILicenseVerificationListener
{
    public MainPage()
    {
        InitializeComponent();
        LicenseManager.InitLicense("LICENSE-KEY", this);
    }

    public void OnLicenseVerified(bool isSuccess, string message)
    {
        if (!isSuccess)
        {
            Debug.WriteLine(message);
        }
    }
}

```

## Step 5: Add Two Buttons to the Main Page
First, create a `PicturePage` for decoding barcodes from image files and a `CameraPage` for scanning barcodes from the camera video stream. Then, add two buttons to the main page: one for picking an image from the gallery and navigating to `PicturePage`, and another for requesting camera permissions and navigating to `CameraPage`.

**MainPage.xaml**

```xml
<?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.MainPage"
             >

    <ScrollView>
        <StackLayout>
            <Button x:Name="takePhotoButton" Text="Image File" HorizontalOptions="Center" VerticalOptions="CenterAndExpand" Clicked="OnTakePhotoButtonClicked" />
            <Button x:Name="takeVideoButton" Text="Video Stream" HorizontalOptions="Center" VerticalOptions="CenterAndExpand" Clicked="OnTakeVideoButtonClicked" />
        </StackLayout>
    </ScrollView>

</ContentPage>
```

**MainPage.xaml.cs**

```csharp
async void OnTakePhotoButtonClicked(object sender, EventArgs e)
{
    try
    {
        var result = await FilePicker.Default.PickAsync(new PickOptions
        {
            FileTypes = FilePickerFileType.Images,
            PickerTitle = "Please select an image"
        });

        if (result != null)
        {
            await Navigation.PushAsync(new PicturePage(result));
        }
    }
    catch (Exception ex)
    {
        // Handle exceptions if any
        Console.WriteLine($"An error occurred: {ex.Message}");
    }
}

async void OnTakeVideoButtonClicked(object sender, EventArgs e)
{

    var status = await Permissions.CheckStatusAsync<Permissions.Camera>();
    if (status == PermissionStatus.Granted)
    {
        await Navigation.PushAsync(new CameraPage());
    }
    else
    {
        status = await Permissions.RequestAsync<Permissions.Camera>();
        if (status == PermissionStatus.Granted)
        {
            await Navigation.PushAsync(new CameraPage());
        }
        else
        {
            await DisplayAlert("Permission needed", "I will need Camera permission for this action", "Ok");
        }
    }
}
```

## Step 6: Read Barcodes from Image Files
1. Add an `Image` control to display the selected image and a `GraphicsView` control to overlay the barcode results. 

    ```xml
    <ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
                 xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
                 x:Class="BarcodeQrScanner.PicturePage"
                 Title="Barcode Reader">
    
        <Grid>
            <Image x:Name="PickedImage" Aspect="AspectFit" VerticalOptions="CenterAndExpand" HorizontalOptions="CenterAndExpand" SizeChanged="OnImageSizeChanged"/>
            <GraphicsView x:Name="OverlayGraphicsView" />
        </Grid>
    </ContentPage>
    ```

    Ensure the size of the `GraphicsView` matches the size of the Image control, and update the `GraphicsView` size when the `Image` control size changes.
    
    ```csharp
    private void OnImageSizeChanged(object sender, EventArgs e)
    {
        // Adjust the GraphicsView size to match the Image size
        OverlayGraphicsView.WidthRequest = PickedImage.Width;
        OverlayGraphicsView.HeightRequest = PickedImage.Height;
    }
    ```

2. Get the image width and height for calculating the overlay position:

    ```csharp
    public PicturePage(FileResult result)
    {
        InitializeComponent();
        LoadImageWithOverlay(result);
    }

    async private void LoadImageWithOverlay(FileResult result)
    {
        // Get the file path
        var filePath = result.FullPath;
        var stream = await result.OpenReadAsync();

        float originalWidth = 0;
        float originalHeight = 0;

        try
        {
            var image = PlatformImage.FromStream(stream);
            originalWidth = image.Width;
            originalHeight = image.Height;

            ...
        }
        catch (Exception ex)
        {
            Console.WriteLine($"An error occurred: {ex.Message}");
        }
    }
    ```

3. Reset the file stream position for displaying the image:

    ```csharp
    stream.Position = 0;
    ImageSource imageSource = ImageSource.FromStream(() => stream);
    PickedImage.Source = imageSource;
    ```

4. Decode barcodes from the image file:

    ```csharp
    private CaptureVisionRouter router = new CaptureVisionRouter();
    CapturedResult capturedResult = router.Capture(filePath, EnumPresetTemplate.PT_READ_BARCODES);
    DecodedBarcodesResult? barcodeResults = null;

    if (capturedResult != null) {
        // Get the barcode results
        barcodeResults = capturedResult.DecodedBarcodesResult;
    }
    ```

5. Draw the barcode results over the image:

    ```csharp
    public class ImageWithOverlayDrawable : IDrawable
    {
        private readonly DecodedBarcodesResult? _barcodeResults;
        private readonly float _originalWidth;
        private readonly float _originalHeight;
    
        private bool _isFile;
    
        public ImageWithOverlayDrawable(DecodedBarcodesResult? barcodeResults, float originalWidth, float originalHeight, bool isFile = false)
        {
            _barcodeResults = barcodeResults;
            _originalWidth = originalWidth;
            _originalHeight = originalHeight;
            _isFile = isFile;
        }
    
        public void Draw(ICanvas canvas, RectF dirtyRect)
        {
            // Calculate scaling factors
            float scaleX = (int)dirtyRect.Width / _originalWidth;
            float scaleY = (int)dirtyRect.Height / _originalHeight;
    
            // Set scaling to maintain aspect ratio
            float scale = Math.Min(scaleX, scaleY);
    
            canvas.StrokeColor = Colors.Red;
            canvas.StrokeSize = 2;
            canvas.FontColor = Colors.Red;
    
            if (_barcodeResults != null) {
                var items = _barcodeResults.Items;
                foreach (var item in items) {
                    Microsoft.Maui.Graphics.Point[] points = item.Location.Points;

                    if (_isFile){
                        canvas.DrawLine((float)points[0].X * scale, (float)points[0].Y * scale, (float)points[1].X * scale, (float)points[1].Y * scale);
                        canvas.DrawLine((float)points[1].X * scale, (float)points[1].Y * scale, (float)points[2].X * scale, (float)points[2].Y * scale);
                        canvas.DrawLine((float)points[2].X * scale, (float)points[2].Y * scale, (float)points[3].X * scale, (float)points[3].Y * scale);
                        canvas.DrawLine((float)points[3].X * scale, (float)points[3].Y * scale, (float)points[0].X * scale, (float)points[0].Y * scale);
                    }
    
                    canvas.DrawString(item.Text, (float)points[0].X * scale, (float)points[0].Y * scale - 10, HorizontalAlignment.Left);
                }
            }
        }
    }

    var drawable = new ImageWithOverlayDrawable(barcodeResults, originalWidth, originalHeight, true);

    // Set drawable to GraphicsView
    OverlayGraphicsView.Drawable = drawable;
    OverlayGraphicsView.Invalidate();
    ```

    ![Barcode Reader](https://www.dynamsoft.com/codepool/img/2024/07/dotnet-maui-barcode-reader.jpg)


## Step 7: Scan Barcodes from Camera Video Stream 
1. In the `CameraPage` layout, add a `CameraView` control to display the camera video stream and a `GraphicsView` control to overlay the barcode results on the video stream.
   
    **CameraPage.xaml**
    ```xml
    <?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.CaptureVisionRouter.Maui"
                 x:Class="BarcodeQrScanner.CameraPage"
                 Title="Barcode Scanner">
        <Grid Margin="0">
            <controls:CameraView x:Name="CameraPreview"
                                  SizeChanged="OnImageSizeChanged"/>
            <GraphicsView x:Name="OverlayGraphicsView"/>
        </Grid>
    </ContentPage>
    ```
    **CameraPage.xaml.cs**
    ```csharp
    private void OnImageSizeChanged(object sender, EventArgs e)
    {
        // Adjust the GraphicsView size to match the Image size
        OverlayGraphicsView.WidthRequest = PickedImage.Width;
        OverlayGraphicsView.HeightRequest = PickedImage.Height;
    }
    ```
    

2. In the `CameraPage.xaml.cs` file, instantiate `CameraEnhancer` and start the camera preview. Use `WeakReferenceMessenger` to handle Android lifecycle events.

    ```csharp
    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 CameraPage : ContentPage, ICapturedResultReceiver, ICompletionListener
    {
        private CameraEnhancer? enhancer = null;
        private CaptureVisionRouter router;
        private float previewWidth = 0;
        private float previewHeight = 0;
    
        public CameraPage()
        {
            InitializeComponent();
            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();
                }
            });
        }
    
        protected override void OnHandlerChanged()
        {
            base.OnHandlerChanged();
            if (this.Handler != null && enhancer != null)
            {
                enhancer.SetCameraView(CameraPreview);
                enhancer.Open();
            }
        }
    
        protected override async void OnAppearing()
        {
            base.OnAppearing();
            await Permissions.RequestAsync<Permissions.Camera>();
            router?.StartCapturing(EnumPresetTemplate.PT_READ_BARCODES, this);
        }
    
        protected override void OnDisappearing()
        {
            base.OnDisappearing();
            enhancer?.Close();
            router?.StopCapturing();
        }
    }
    ```

3. Receive the barcode results via callback functions and draw them on the video stream using the `GraphicsView`.

    ```csharp
    public void OnCapturedResultReceived(CapturedResult result)
    {
        MainThread.BeginInvokeOnMainThread(() =>
        {
            var drawable = new ImageWithOverlayDrawable(null, previewWidth, previewHeight, false);

            // Set drawable to GraphicsView
            OverlayGraphicsView.Drawable = drawable;
            OverlayGraphicsView.Invalidate();
        });
    }

    public void OnDecodedBarcodesReceived(DecodedBarcodesResult result)
    {
        if (previewWidth == 0 && previewHeight == 0)
        {
            IntermediateResultManager manager = router.GetIntermediateResultManager();
            ImageData data = manager.GetOriginalImage(result.OriginalImageHashId);
            
            // Create a drawable with the barcode results
            previewWidth = (float)data.Width;
            previewHeight = (float)data.Height;
        }

        MainThread.BeginInvokeOnMainThread(() =>
        {
            var drawable = new ImageWithOverlayDrawable(result, previewWidth, previewHeight, false);

            // Set drawable to GraphicsView
            OverlayGraphicsView.Drawable = drawable;
            OverlayGraphicsView.Invalidate();
        });
    }
    ```
    
    ![Barcode Scanner](https://www.dynamsoft.com/codepool/img/2024/07/dotnet-maui-qrcode-scanner.jpg)
    
## Known Issues 

**iOS Text Rendering Issue**
The `canvas.DrawString` method does not work properly on **iOS**, resulting in no text being rendered on the `GraphicsView`.

## Source Code
[https://github.com/yushulx/maui-barcode-qrcode-scanner](https://github.com/yushulx/maui-barcode-qrcode-scanner)