Geolocation in Blazor

One of the joys of Blazor is never having to use JavaScript again. With C# running in WebAssembly, events like button clicks can be handled by handlers written in C# and old dogs don’t have to learn new tricks.

There are some browser capabilities that aren’t exposed and these need JavaScript interop. The first of the browser Web APIs that I wanted to use in a Blazor app was the Geolocation API. This gives us the position of the device, among other things.

The goal of this project is to create a component that encapsulates the JavaScript interop malarkey. I want to create a Geolocation service that a host application can call to get its position using only C#.

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

getCurrentPosition returns either a GeolocationPosition or a GeolocationPositionError. I reproduced both of these interfaces in C#. In JavaScript the getCurrentPosition() function takes a success callback and an error callback. I wanted to return a single object, so I created a combined GeolocationResult class that had Position and Error properties.

GeolocationResult has a GeolocationPosition, which has GeolocationCoordinates, etc.

These are simple data transfer objects (DTOs) with properties that match those in the Geolocation API spec. Some of the properties are nullable because the device may not be able to provide them.

    public class GeolocationCoordinates
    {
        public double Latitude { get; set; }
        public double Longitude { get; set; }
        public double? Altitude { get; set; }
        public double Accuracy { get; set; }
        public double? AltitudeAccuracy { get; set; }
        public double? Heading { get; set; }
        public double? Speed { get; set; }
    }

I wanted to protect my client app from the horrors of JavaScript, so all of my JavaScript interop is wrapped in a GeolocationService and the consuming code need never know what’s going on. All it needs to do is call the service’s GetCurrentPosition method with or without an options argument and await the return of the result.

The consuming code is simple

    CurrentPositionResult = await GeolocationService.GetCurrentPosition();

The service method invokes a function from the component’s JavaScript module to get the device position.

        public async Task<GeolocationResult> GetCurrentPosition(PositionOptions options = null)
        {
            var module = await _jsBinder.GetModule();
            return await module.InvokeAsync<GeolocationResult>("Geolocation.getCurrentPosition", options);
        }

I’m using the same JavaScript isolation technique that I used in the Leaflet Maps project to hide the JavaScript from the host application.

The JavaScript creates a result object and wraps the call to the real getCurrentPosition function in a Promise. The promise either resolves and sets the position on the result or fails and sets the error.

“return result;” Is Harder than It Looks

The problem is that the position and error properties are never returned to the C# side of the app. What should happen is that the object that’s returned from the JavaScript function is serialised to JSON and sent back to the C# caller. The C# app will deserialise it back into a C# object.

What actually happens is that the position and error always come back as empty objects. Their serialisation is always “{ }”. This was baffling and I did a lot of console-dot-writing to work out what was going on. My latitude, longitude, and altitude were a mystery, but my aptitude was looking low.

My goal was to avoid doing any JavaScript and here I am reading the ECMAScript 2016 Language Specification, in particular section 24.3.2.3, “Runtime Semantics: SerializeJSONObject“. The short version of the story is that JSON serialisation doesn’t serialise properties that are inherited from a prototype. On GeolocationPosition, that’s all of them. The upshot is that a GeolocationPosition can’t cross the Blazor interop boundary, which is not very useful.

As a JavaScript amateur, I gave myself a head injury working this out. To get the information back, we need to create an object that has all the properties we want as its own properties, not inherited properties. There are two ways to do this – the easy way and proper way.

The proper way would be a general function that can take any object with inherited properties, iterate over them and make a deep copy. I did it the easy way, which is a quick-and-dirty function that just maps exactly what I want. If I find myself doing this more than a couple of times, I’ll do it properly.

export let Geolocation = {

    getCurrentPosition: async function (options) {
        var result = { position: null, error: null };
        var getCurrentPositionPromise = new Promise((resolve, reject) => {
            if (!navigator.geolocation) {
                reject({ code: 0, message: 'This device does not support geolocation.' });
            } else {
                navigator.geolocation.getCurrentPosition(resolve, reject, options);
            }
        });
        await getCurrentPositionPromise.then(
            (position) => { this.mapPositionToResult(position, result) }
        ).catch(
            (error) => { this.mapErrorToResult(error, result) }
        );
        return result;
    },

    mapPositionToResult: function (position, result) {
        result.position = {
            coords: {
                latitude: position.coords.latitude,
                longitude: position.coords.longitude,
                altitude: position.coords.altitude,
                accuracy: position.coords.accuracy,
                altitudeAccuracy: position.coords.altitudeAccuracy,
                heading: position.coords.heading,
                speed: position.coords.speed
            },
            timestamp: position.timestamp
        }
    },

    mapErrorToResult: function (error, result) {
        result.error = {
            code: error.code,
            message: error.message
        }
    }

}

Hiding One More Piece of JavaScript

There’s one more bit of JavaScript magic that needs to be hidden. The timestamp on the GeolocationPosition is a DOMTimestamp in milliseconds since the Unix epoch (i.e. 00:00:00 UTC on 1 January 1970). I’ve included a convenience property to turn this into a .NET DateTimeOffset.

    public class GeolocationPosition
    {
        public GeolocationCoordinates Coords { get; set; }
        public long Timestamp { get; set; }
        [JsonIgnore]
        public DateTimeOffset DateTimeOffset => DateTimeOffset.FromUnixTimeMilliseconds(Timestamp);
    }

Overnight Success

After a yak-shaving expedition into prototype-based programming and the weeds of the ECMAScript spec, getting our position is now very simple. In the GeolocationService:

        public async Task<GeolocationResult> GetCurrentPosition(PositionOptions options = null)
        {
            var module = await _jsBinder.GetModule();
            return await module.InvokeAsync<GeolocationResult>("Geolocation.getCurrentPosition", options);
        }

I’ve turned this into a NuGet package: Darnton.Blazor.DeviceInterop.

Pics or It Didn’t Happen

To prove the roaring success of the device interop layer, we need a demo.

My test rig project has a geolocation page. On that page I’ve put a Leaflet slippy map that uses an Open Street Maps tile layer. Now I can find my location and drop a marker onto the map.

Creating the Leaflet Blazor component was even more of a yak-shaving trip than what happened above. I’ve blogged that in another article: Slippy Maps in Blazor with Leaflet. The code is on GitHub if you’re interested, but other people have created more complete implementations. At the time of writing there are two versions of the Leaflet component on NuGet: the stable version for .NET Core 3.x and a prerelease version for .NET 5.

On the geolocation test page, there’s a button and a map. The map and it’s tile layer are set up when the page is created and then initialised by the Leaflet component once it’s rendered. See the link above for more about the mapping.

The test rig for the current position function looks like this:

<div class="my-3">
    <h3>Current Position</h3>
    <p class="my-0"><span class="font-weight-bold">Position </span> 
        @if(CurrentPositionResult?.Position is null)
        {
            <span>[unknown]</span>
        }
        else
        {
            <span>Lat: @CurrentLatitude °, Long: @CurrentLongitude °</span>
        }
    </p>
    <button class="btn btn-primary my-3" @onclick="ShowCurrentPosition">Current Location</button>
    @if (CurrentPositionResult?.Error != null)
    {
        <p class="bg-light text-danger">Error: @CurrentPositionResult.Error.Message</p>
    }
    <LeafletMap Map="PositionMap" TileLayer="PositionTileLayer" />
</div>

Clicking the button calls the Geolocation interop code and then drops a pin on the map.

        public async void ShowCurrentPosition()
        {
            if (CurrentPositionMarker != null)
            {
                await CurrentPositionMarker.Remove();
            }
            CurrentPositionResult = await GeolocationService.GetCurrentPosition();
            if (CurrentPositionResult.IsSuccess)
            {
                CurrentPositionMarker = new Marker(
                        CurrentPositionResult.Position.ToLeafletLatLng(), null
                    );
                await CurrentPositionMarker.AddTo(PositionMap);
            }
            StateHasChanged();
        }

This calls a scrap of code that maps between the Geolocation position and a LatLng that the map can use.

    public static class GeolocationPositionExtension
    {
        public static LatLng ToLeafletLatLng(this GeolocationPosition position)
        {
            var coords = position.Coords;
            return new LatLng(coords.Latitude, coords.Longitude);
        }
    }

Next Up: Callbacks When the Device Moves

If getting one position is good, getting lots is better. That’s what the watchPosition function is for. In the next post, I’ll look at using a JavaScript callback to raise a C# event when the device moves.

Get the Code

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

One thought on “Geolocation in Blazor

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