Reputation: 3581
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
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