Search code examples
blazorblazor-webassembly

Blazor - How to launch task on UI Dispatcher thread from a non-UI component?


I am using Xamarin Mobile Blazor Bindings to develop an Android/iOS location tracker app. I don't think the fact that I am using Xamarin Mobile Blazor Bindings specifically is relevant, but I mention it for completeness.

The app polls the mobile device's current GPS position every 15 seconds, and makes a call to a custom event whenever a new position is received, notifying any Blazor UI Components that have registered for the event.

All the GPS logic and event handling is done within a simple (non-UI) singleton class, as follows:

public class MyLocationManager
{
    public event EventHandler<LocationUpdatedEventArgs> OnLocationUpdateReceived;

    private Timer _timer;

    public void StartLocationTracking()
    {
        if (DeviceInfo.Platform == DevicePlatform.Android)
        {
            // On Android, Location can be polled using a timer
            _timer = new Timer(
                async stateInfo =>
                {
                    var newLocationCoordinates = await _addCurrentLocationPoint();  

                    var eventArgs = new LocationUpdatedEventArgs
                    {
                       Latitude = newLocationCoordinates.Latitude,
                       Longitude = newLocationCoordinates.Longitude
                    };

                    // **** The following line generates an Exception ****
                    OnLocationUpdateReceived?.Invoke(this, eventArgs)
                },
                new AutoResetEvent(false),
                15000 /* Wait 15 seconds before initial call */,
                15000 /* Then repeat every 15 seconds */
            );
        }
    }
}

Unfortunately, when the timer triggers, the following code generates an exception:

OnLocationUpdateReceived?.Invoke(this, eventArgs)

The Exception is:

System.InvalidOperationException: 'The current thread is not associated with the Dispatcher. Use InvokeAsync() to switch execution to the Dispatcher when triggering rendering or component state.'

Now, I understand what the Exception is saying, that the non-UI thread that is currently running can't be used to invoke the event, so I need to somehow use the Dispatcher thread instead.

However, the "InvokeAsync()" method that is mentioned only seems to exist within the base class for UI Components, which "MyLocationManager" class isn't.

Can anyone offer any advice on how I can achieve the same from a non-UI class such as this one?

Any advice gratefully received.


Solution

  • Have you tried to use InvokeAsync via a state handler on the component? Instead of using it on the manager, you can use this approach: How to fix 'The current thread is not associated with the renderer's synchronization context'?

    In practice you register a change handler event and execute the InvokeAsync on component side:

        private async void OnMyChangeHandler(object sender, EventArgs e)
        {
            // InvokeAsync is inherited, it syncs the call back to the render thread
            await InvokeAsync(() => {
                DoStuff();
                StateHasChanged());
            }
        }
    }