Pieterjan
Pieterjan

Reputation: 3581

.NET Maui - Update UI from bluetooth events

I'm trying to rewrite my old Xamarin android bluetooth app to .NET Maui. However, I seem to be struggling to update the Blazor UI when I receive bluetooth events (like DiscoveryStarted, DeviceFound, DiscoveryFinished). While the data in the collection changes, or the state of a bool within the component changes, the UI doesn't update.

Even when I put my code inside a InvokeAsync call, the view doesn't update. I explicitly have to call StateHasChanged everywhere something in the background is changed.

What's the best way to deal with this? I prefer not to have to write all these InvokeAsync and StateHasChanged calls.

Here's the code of my Razor page

@inject IBluetoothService bluetoothService;

<span class="input-group">
    @{
        if (isDiscovering)
        {
            <button class="btn btn-secondary" @onclick="StopDiscovery">Stop Discovery</button>
        }
        else
        {
            <button class="btn btn-secondary" @onclick="StartDiscovery">Start Discovery</button>
        }
    }
</span>
<ul class="list-group">
    @foreach (var device in devices)
    {
        <li class="list-group-item">@device</li>
    }
</ul>

@code
{
    private bool isDiscovering = false;
    private ObservableCollection<string> devices = new();

    private async Task StartDiscovery()
    {
        isDiscovering = true;
        await bluetoothService.StartDiscovery(OnDeviceDiscovered, OnDiscoveryFinished);
    }

    private async Task OnDeviceDiscovered(string deviceName)
    {
        await InvokeAsync(() =>
        {
            devices.Add(deviceName);
            StateHasChanged(); // Have to do this before the UI updates
        });
    }

    private async Task OnDiscoveryFinished()
    {
        await InvokeAsync(() =>
        {
            isDiscovering = false;
            StateHasChanged(); // Have to do this before the UI updates
        });
    }

    private void StopDiscovery()
    {
        isDiscovering = false;
    }
}

The BluetoothService which is injected here


internal class BluetoothService : IBluetoothService
{
    private readonly BluetoothAdapter? bluetoothAdapter = BluetoothAdapter.DefaultAdapter;
    private readonly global::Android.Content.Context context;
    CustomCode.BluetoothReceiver? bluetoothReceiver = new();

    public bool IsDiscovering { get; private set; }

    public BluetoothService()
    {
        context = global::Microsoft.Maui.ApplicationModel.Platform.CurrentActivity
            ?? global::Microsoft.Maui.MauiApplication.Context;

        if (bluetoothAdapter == null || !bluetoothAdapter.IsEnabled)
            throw new Exception("Bluetooth not available/enabled");

        bluetoothReceiver = new BluetoothReceiver();

        bluetoothReceiver.DiscoveryStarted += BluetoothReceiver_DiscoveryStarted;
        bluetoothReceiver.DiscoveryFinished += BluetoothReceiver_DiscoveryFinished;
        bluetoothReceiver.DeviceFound += BluetoothReceiver_DeviceFound;

        foreach (var action in new[] { BluetoothDevice.ActionFound, BluetoothAdapter.ActionDiscoveryStarted, BluetoothAdapter.ActionDiscoveryFinished, BluetoothDevice.ActionBondStateChanged })
            context.RegisterReceiver(bluetoothReceiver, new global::Android.Content.IntentFilter(action));
    }

    private Func<string, Task> deviceFound;
    private Func<Task> discoveryFinished;
    public Task StartDiscovery(Func<string, Task> deviceFound, Func<Task> discoveryFinished)
    {
        if (IsDiscovering) throw new InvalidOperationException();

        IsDiscovering = true;
        this.deviceFound = deviceFound;
        this.discoveryFinished = discoveryFinished;

        //BluetoothDevices.Clear();
        ActivityCompat.RequestPermissions(global::Microsoft.Maui.ApplicationModel.Platform.CurrentActivity!, [
            global::Android.Manifest.Permission.Bluetooth,
            global::Android.Manifest.Permission.BluetoothAdmin,
            global::Android.Manifest.Permission.BluetoothAdvertise,
            global::Android.Manifest.Permission.BluetoothConnect,
            global::Android.Manifest.Permission.BluetoothPrivileged,
            global::Android.Manifest.Permission.BluetoothScan,
            global::Android.Manifest.Permission.AccessCoarseLocation,
            global::Android.Manifest.Permission.AccessFineLocation,
            //"android.hardware.sensor.accelerometer"
        ], 1);
        return Task.CompletedTask;
    }

    private async void BluetoothReceiver_DeviceFound(object? sender, Platforms.Android.CustomCode.EventArgs.DeviceFoundEventArgs e)
    {
        if (e.Device?.Name is string name)
            await deviceFound(name);

        // Binding to this collection, and updating it is pointless
        //BluetoothDevices.Add(name);
    }

    private void BluetoothReceiver_DiscoveryFinished(object? sender, EventArgs e)
    {
        // Binding to this variable, and updating it is pointless
        IsDiscovering = false;
        discoveryFinished();
    }

    private void BluetoothReceiver_DiscoveryStarted(object? sender, EventArgs e) { }
}

And the BluetoothReceiver for android

internal class BluetoothReceiver : BroadcastReceiver
{
    public override void OnReceive(Context? context, Intent? intent)
    {
        switch (intent?.Action)
        {
            case BluetoothDevice.ActionFound:
                if (intent.GetParcelableExtra(BluetoothDevice.ExtraDevice) is BluetoothDevice device)
                    OnDeviceFound(new EventArgs.DeviceFoundEventArgs { Device = device });
                break;
            case BluetoothAdapter.ActionDiscoveryStarted:
                OnDiscoveryStarted(System.EventArgs.Empty);
                break;
            case BluetoothAdapter.ActionDiscoveryFinished:
                OnDiscoveryFinished(System.EventArgs.Empty);
                break;
            case BluetoothDevice.ActionBondStateChanged:
                if (intent.GetParcelableExtra(BluetoothDevice.ExtraDevice) is BluetoothDevice device2)
                {
                    var oldState = (Bond)(int)intent.GetParcelableExtra(BluetoothDevice.ExtraPreviousBondState);
                    var newState = (Bond)(int)intent.GetParcelableExtra(BluetoothDevice.ExtraBondState);
                    OnBondStateChanged(new EventArgs.BondStateChangedEventArgs { Device = device2, OldState = oldState, NewState = newState });
                }
                break;
            case BluetoothDevice.ActionUuid:
                if (intent.GetParcelableExtra(BluetoothDevice.ExtraUuid) is UUID uuid)
                    OnUUIDFetched(new EventArgs.UuidFetchedEventArgs { UUID = uuid });
                break;
        }
    }

    #region DeviceFound
    public event EventHandler<EventArgs.DeviceFoundEventArgs> DeviceFound;
    protected void OnDeviceFound(EventArgs.DeviceFoundEventArgs e)
    {
        if (DeviceFound != null)
            DeviceFound(this, e);
    }
    #endregion
    #region DiscoveryStarted
    public event EventHandler? DiscoveryStarted;
    protected void OnDiscoveryStarted(System.EventArgs e)
    {
        if (DiscoveryStarted != null)
            DiscoveryStarted(this, e);
    }
    #endregion
    #region DiscoveryFinished
    public event EventHandler? DiscoveryFinished;
    protected void OnDiscoveryFinished(System.EventArgs e)
    {
        if (DiscoveryFinished != null)
            DiscoveryFinished(this, e);
    }
    #endregion
    #region BondStateChanged
    public event EventHandler<EventArgs.BondStateChangedEventArgs>? BondStateChanged;
    protected void OnBondStateChanged(EventArgs.BondStateChangedEventArgs e)
    {
        if (BondStateChanged != null)
            BondStateChanged(this, e);
    }
    #endregion
    #region UuidFetched
    public event EventHandler<EventArgs.UuidFetchedEventArgs>? UuidFetched;
    protected void OnUUIDFetched(EventArgs.UuidFetchedEventArgs e)
    {
        if (UuidFetched != null)
            UuidFetched(this, e);
    }
    #endregion
}

EDIT:

I tried using

MainThread.BeginInvokeOnMainThread(() => BluetoothDevices.Add(name));

instead, but I got the same results

Upvotes: 1

Views: 93

Answers (1)

Liyun Zhang - MSFT
Liyun Zhang - MSFT

Reputation: 14509

The method is not async so you can just call it directly. Such as:

private void OnDiscoveryFinished()
{
    isDiscovering = false;
}

And for the ObservableCollection, you can use the CollectionChanged event:

protected override void OnInitialized()
{
    base.OnInitialized();
    devices.CollectionChanged += (s, e) =>
    {
        StateHasChanged();
    };
}

Upvotes: 0

Related Questions