Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/CodexaCP/DG_APK/llms.txt

Use this file to discover all available pages before exploring further.

Dragon Guard Handheld WMS is built around a strict MVVM pattern wired together by .NET MAUI’s built-in dependency injection. This page documents the file layout, how the app starts up, how services are registered, the two HTTP client pipelines, and the rules governing navigation and base URL resolution. Read this before touching app startup, DI wiring, or any HTTP handler.
Do not use Shell navigation. AppShell.xaml is excluded from compilation in Handheld.csproj — it crashes on rugged Android firmware during fragment startup. Navigation is handled exclusively through NavigationService, which wraps a NavigationPage root set in App.xaml.cs.

File layout

Handheld/
├── Constants/          API base-URL constants (ApiConstants.cs)
├── Components/         Reusable XAML controls — Bar, Card
├── Helpers/            StatusPalette: badge colors and short status labels
├── Models/             DTOs — plain POCOs with System.Text.Json attributes only
├── Services/           All API and infrastructure services
│   └── Scanning/       IScanCaptureService + adapters (RFID, keyboard wedge)
├── ViewModels/         MVVM ViewModels — inherit BaseViewModel
├── Views/              ContentPage XAML + code-behind pairs
├── Platforms/
│   └── Android/        MainActivity, VendorRfidScanAdapter, network_security_config
├── Resources/          Styles, Colors, Fonts, AppIcon, Splash, Languages
└── Docs/               Architecture and module decisions (source of truth)
FolderResponsibility
Constants/Compile-time API base URLs and other app-wide literals
Components/Reusable XAML widgets shared across multiple pages
Helpers/StatusPalette — maps status codes to short labels (Rel/Recv/Ship/Part/Done) and badge colors
Models/Plain POCO DTOs; no business logic, JSON attributes only
Services/All I/O: API calls, session state, navigation, device identity, scanning abstraction
ViewModels/Page-level state and commands; load data on demand, keep only in-memory collections
Views/ContentPage XAML + code-behind; minimal logic, delegates to VM
Platforms/Android/MainActivity, rugged scanner SDK adapter, Android network security config
Resources/Styles, colour palette, fonts, localisation resources
Docs/Authoritative architecture and module decision records

Startup flow

1

App.xaml.cs creates the NavigationPage root

App.CreateWindow() resolves SafeBootPage from DI and wraps it in a NavigationPage with Dragon Guard navy (#15325B) chrome. The window is returned immediately so Android draws the screen while startup continues asynchronously.
App.xaml.cs
protected override Window CreateWindow(IActivationState? activationState)
{
    _mainWindow = new Window(CreateNavigationRoot(_serviceProvider.GetRequiredService<Views.SafeBootPage>()));
    _ = InitializeStartupAsync();
    return _mainWindow;
}

private static NavigationPage CreateNavigationRoot(Page page)
{
    return new NavigationPage(page)
    {
        BarBackgroundColor = Color.FromArgb("#15325B"),
        BarTextColor = Colors.White
    };
}
2

SafeBootPage displays the splash

SafeBootPage is a pure code-behind ContentPage (no XAML) that renders a full-screen blue gradient and an activity spinner with the text “Preparando entorno operativo”. It has no ViewModel and takes no user input — it exists solely to give the user visual feedback while the session initialises.
3

InitializeStartupAsync() restores session state

Running on a background thread, InitializeStartupAsync() calls SessionService.InitializeAsync(), which reads the persisted JWT token and company context from SecureStorage.
App.xaml.cs
private async Task InitializeStartupAsync()
{
    if (_startupCompleted)
        return;

    try
    {
        await _sessionService.InitializeAsync();

        if (_sessionService.HasValidOperationalContext)
            await ShowHomeAsync();
        else
            await ShowLoginAsync();
    }
    catch
    {
        await _sessionService.SignOutAsync();
        await ShowLoginAsync();
    }
    finally
    {
        _startupCompleted = true;
    }
}
4

Route to MainPage or LoginPage

SessionService.HasValidOperationalContext returns true when a non-expired token and exactly one active company are present in storage. If it returns false — missing token, no valid company — the app navigates directly to LoginPage. Only an unhandled exception during initialisation triggers SignOutAsync() before redirecting to LoginPage. The NavigationPage root is replaced on the main thread via SetRootPage().

Dependency injection registration

All services and pages are registered in MauiProgram.CreateMauiApp(). The separation between singletons and transients is deliberate:
Singletons hold cross-cutting state that must survive for the lifetime of the app.
MauiProgram.cs
builder.Services.AddSingleton<ApiSettingsService>();
builder.Services.AddSingleton<AppExperienceService>();
builder.Services.AddSingleton<SessionService>();
builder.Services.AddSingleton<DeviceIdentityService>();
builder.Services.AddSingleton<NavigationService>();
builder.Services.AddSingleton<IScanCaptureService, ScanCaptureService>();
SingletonResponsibility
SessionServiceJWT token, user identity, active company, company access list
ApiSettingsServiceCurrent base URL; DEBUG override allowed
AppExperienceServiceBuild-mode flags (AllowConnectionOverride, ShowConnectionSettings)
NavigationServiceThread-safe push/pop; blocks protected navigation without a token
DeviceIdentityServiceWiFi MAC → ANDROID_ID → GUID fallback chain
IScanCaptureServiceBarcode/RFID abstraction; raises TagObserved for EPC reads

Special case: RfidHandheldContextService

RfidHandheldContextService must be a singleton because it caches per-module RFID context across navigations. However, AddHttpClient<T> always registers T as a transient. To work around this, a named client is registered instead and the singleton is constructed manually using a factory:
MauiProgram.cs
builder.Services.AddHttpClient("rfid-handheld-context", ConfigureHttpClient)
#if ANDROID
    .ConfigurePrimaryHttpMessageHandler(CreateAndroidHandler)
#endif
    .AddHttpMessageHandler<DynamicBaseUrlMessageHandler>()
    .AddHttpMessageHandler<AuthenticatedHttpMessageHandler>();

builder.Services.AddSingleton<RfidHandheldContextService>(sp =>
{
    var http = sp.GetRequiredService<IHttpClientFactory>().CreateClient("rfid-handheld-context");
    return new RfidHandheldContextService(http);
});

Two HTTP client pipelines

MauiProgram.cs defines two private helpers that configure the handler chain for every typed service client:
private static void ConfigurePublicClient<TClient>(IServiceCollection services)
    where TClient : class
{
    services.AddHttpClient<TClient>(ConfigureHttpClient)
#if ANDROID
        .ConfigurePrimaryHttpMessageHandler(CreateAndroidHandler)
#endif
        .AddHttpMessageHandler<DynamicBaseUrlMessageHandler>();
}
PipelineHandler chainServices
PublicDynamicBaseUrlMessageHandlerAuthService
ProtectedDynamicBaseUrlMessageHandlerAuthenticatedHttpMessageHandlerItemService, ReceivingService, ShipmentService, ShipmentLineService, ReceivingLineService, InventoryMovementService, PickService, CompanyService, BinService, TransferService
Every HttpClient starts with PlaceholderBaseUrl as its BaseAddress:
MauiProgram.cs
private static void ConfigureHttpClient(HttpClient client)
{
    client.BaseAddress = new Uri(ApiConstants.PlaceholderBaseUrl);
}
DynamicBaseUrlMessageHandler replaces this placeholder with ApiSettingsService.CurrentBaseUrl at request time, so the backend host can change (on DEBUG builds) without rebuilding the APK. On Android, SocketsHttpHandler replaces the default handler to avoid known rugged-firmware compatibility issues with HttpClientHandler.

AuthenticatedHttpMessageHandler

Every request routed through the protected pipeline is decorated with two headers by AuthenticatedHttpMessageHandler:
  • Authorization: Bearer <token> — the Dragon Guard JWT from SessionService
  • X-Company-Id: <guid> — the active company’s identifier from SessionService
If the token is absent when a request fires, the handler redirects to LoginPage rather than sending an unauthenticated request. NavigationService is the only safe abstraction for pushing and popping pages. It uses a SemaphoreSlim(1, 1) to prevent concurrent navigation calls from multiple async paths:
Services/NavigationService.cs
public async Task PushAsync(Page page)
{
    if (!_sessionService.IsAuthenticated)
    {
        if (Application.Current is App app)
            await app.ShowLoginAsync();

        return;
    }

    if (!await _navigationLock.WaitAsync(0))
        return;

    try
    {
        await MainThread.InvokeOnMainThreadAsync(async () =>
        {
            if (Application.Current?.Windows.FirstOrDefault()?.Page is NavigationPage navigationPage)
                await navigationPage.PushAsync(page, animated: false);
        });
    }
    finally
    {
        _navigationLock.Release();
    }
}
Key behaviours:
  • PushAsync — protected; redirects to LoginPage if SessionService.IsAuthenticated is false.
  • PushUnprotectedAsync — skips the authentication check; used for pages reachable before login (e.g. WelcomeSetupPage).
  • PopAsync — pops only if the navigation stack has more than one entry; no-ops silently otherwise.
  • Non-blocking semaphore (WaitAsync(0)) — a concurrent push/pop is dropped rather than queued, preventing navigation races.

Base URL strategy

API base URLs are defined as compile-time constants:
Constants/ApiConstants.cs
namespace Handheld.Constants;

public static class ApiConstants
{
    public const string DefaultAndroidBaseUrl = "http://192.168.100.4:5262/";
    public const string DefaultDesktopBaseUrl = "http://localhost:5262/";
    public const string PlaceholderBaseUrl = "http://placeholder.invalid/";
}
ApiSettingsService reads and exposes the current base URL at runtime. Its behaviour differs between build configurations:
Services/AppExperienceService.cs
public sealed class AppExperienceService
{
#if DEBUG
    public bool ShowConnectionSettings => true;
    public bool AllowConnectionOverride => true;
    public string ExperienceMode => "TEST";
#else
    public bool ShowConnectionSettings => false;
    public bool AllowConnectionOverride => false;
    public string ExperienceMode => "OFFICIAL";
#endif
}
BuildAllowConnectionOverrideBehaviour
DEBUGtrueLoads saved URL from Preferences; operator can change it in Settings
ReleasefalseDeletes any saved URL key; always uses DefaultAndroidBaseUrl
This ensures that official APK builds always connect to the production backend regardless of what was previously saved on the device, while test builds remain flexible for on-site configuration.
Never add a Settings screen URL override to a Release build. ApiSettingsService.Save() returns an error message (“La configuracion de conexion solo esta disponible en builds de prueba.”) and refuses to persist the value when AllowConnectionOverride is false.

Build docs developers (and LLMs) love