Geolocation II: Position Updates

In the previous post, Geolocation in Blazor, I started wrapping the browser’s Geolocation API in a Blazor component. That first article showed how to get the device’s current position, including how to get prototype-based JavaScript objects back across the C#/JS interop boundary.

In this post, I’ll look at using a JavaScript callback to raise a C# event when the device moves.

GitHub: https://github.com/darnton/BlazorDeviceInterop
NuGet: https://www.nuget.org/packages/Darnton.Blazor.DeviceInterop

Watch Events

If getting one position is good, getting lots is better. That’s what the watchPosition function is for. In the JavaScript version, the first argument is a callback that’s invoked every time the device’s position changes. I’ll C#-ify this in the GeolocationService and raise an event.

Blazor JavaScript interop is a two-way street. Raising the event in C# means we have to make a call from JavaScript back to C#. The sequence goes like this:

In the GeolocationService I have a WatchPosition method. This creates a .NET object reference to the GeolocationService instance. Then it invokes my JavaScript warpper function, giving it the service instance and the method name to call back to when the device’s position changes.

    public async Task<long?> WatchPosition(PositionOptions options = null)
    {
        var module = await _jsBinder.GetModule();
        var callbackObj = DotNetObjectReference.Create(this);
        return await module.InvokeAsync<int>("Geolocation.watchPosition",
            callbackObj, nameof(SetWatchPosition), options);
    }

I’ve wrapped the Geolocation API’s watchPosition function in my own watchPosition function, which is exported as part of the Geolocation module. When I call the wrapper function, I pass in the C# object reference and method name. The .NET callback reference is used to construct the JavaScript callback functions that the real watchPosition function invokes.

    watchPosition: async function (dotNetCallbackRef, callbackMethod, options) {
        if (!navigator.geolocation) return null;

        return navigator.geolocation.watchPosition(
            (position) => {
                let result = { position: null, error: null };
                this.mapPositionToResult(position, result);
                dotNetCallbackRef.invokeMethodAsync(callbackMethod, result);
            },
            (error) => {
                let result = { position: null, error: null };
                this.mapErrorToResult(error, result);
                dotNetCallbackRef.invokeMethodAsync(callbackMethod, result);
            },
            options);
    },

The watchPosition function returns a watchId that we’ll use later to clear the watch.

When the device’s position changes, the callback is invoked. The JavaScript callback invokes the .NET callback with the new GeolocationResult. The GeolocationService’s SetWatchPosition method is decorated with the JSInvokable attribute to let Blazor know this is allowed. It invokes an event handler.

    [JSInvokable]
    public void SetWatchPosition(GeolocationResult watchResult)
    {
        WatchPositionReceived?.Invoke(this, new GeolocationEventArgs
        {
            GeolocationResult = watchResult
        });
    }

The Test Rig

The test rig for watching the position is almost the same as the one for getting the current position. The biggest difference is that the button is a toggle that turns watching on and off.

When the button is clicked for the first time an event handler is added and the WatchPosition method is called.

    public async void TogglePositionWatch()
    {
        if (isWatching)
        {
            await StopWatching();
            ...
        }
        else
        {
            GeolocationService.WatchPositionReceived += HandleWatchPositionReceived;
            WatchHandlerId = await GeolocationService.WatchPosition();
        }
        StateHasChanged();
    }

When the WatchPositionReceived event is raised, the event handler checks for a successful result and adds a marker to the map.

        private async void HandleWatchPositionReceived(object sender, GeolocationEventArgs e)
        {
            LastWatchPositionResult = e.GeolocationResult;
            if (LastWatchPositionResult.IsSuccess)
            {
                var latlng = LastWatchPositionResult.Position.ToLeafletLatLng();
                var marker = new Marker(latlng, null);
                if (WatchPath is null)
                {
                    WatchMarkers = new List<Marker> { marker };
                    WatchPath = new Polyline(WatchMarkers.Select(m => m.LatLng), new PolylineOptions());
                    await WatchPath.AddTo(WatchMap);
                }
                else
                {
                    WatchMarkers.Add(marker);
                    await WatchPath.AddLatLng(latlng);
                }
                await marker.AddTo(WatchMap);
            }
            StateHasChanged();
        }

Stop Watching

When you want to stop watching the device’s position, pass the watchId to the clearWatch function. From a blazor component, this can be done explicitly, like I do with the ‘Stop Watching’ button, or automatically when the component is disposed.

        private async Task StopWatching()
        {
            GeolocationService.WatchPositionReceived -= HandleWatchPositionReceived;
            await GeolocationService.ClearWatch(WatchHandlerId.Value);
        }

        public async void Dispose()
        {
            if (isWatching)
            {
                await StopWatching();
            }
        }

The JS layer for this is very simple.

    clearWatch: function (id) {
        if (navigator.geolocation) {
            navigator.geolocation.clearWatch(id);
        }
    }

And, with that, all the functions in the Geolocation API are covered.

Get the Code

GitHub: https://github.com/darnton/BlazorDeviceInterop
NuGet: https://www.nuget.org/packages/Darnton.Blazor.DeviceInterop

One thought on “Geolocation II: Position Updates

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s