1

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

asked Feb 2, 2025 at 13:26

1 Answer 1

0

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();
 };
}
Anuj Karki
6318 silver badges31 bronze badges
answered Feb 3, 2025 at 3:30
Sign up to request clarification or add additional context in comments.

2 Comments

Thanks for the response, but this doesn't solve the problem. I committed the changes here and the result is the same. The breakpoint inside BluetoothReceiver_DiscoveryFinished is hit, but the UI doesn't update. Maui seems to expect that statechanges have happened when the handler method is completed, however with bluetooth we can't do that. The code must go through android's OnRequestPermissionsResult method first, while the method that triggered ActivityCompat.RequestPermissions was called from the button click
I cloned you project and reproduced the problem. The CollectionChanged event can work but you pass the OnDeviceDiscovered and OnDiscoveryFinished method as function and can change them to void return type. So you have to call StateHasChanged(). And the StopDiscovery() can update UI correctly.

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.