Embedding a Leaflet map on a Blazor SPA

Note: The sample code below was created and tested in a WebAssembly Blazor App stand alone…

An object reference is required for the nonstatic field, method, or property ‘member’

Let’s create the object class, whose object reference will be passed to your JavaScript’s object when initialized. When the user clicks on a location on the map, a click event is triggered for the JS map object, from which the C# object’s JSInvokable method is called and passed the latitude and longitude…

GCSService.cs

public class GCSService
{
    public GCSService() {}
    public event Func<Task> Notify;
    public string LatLng { get; set; }

    [JSInvokableAttribute("GetLatLng")]
    public async Task GetLatLng(string latLng)
    {
       LatLng = latLng;

       if (Notify != null)
       {
           await Notify?.Invoke();
       }
    }
}

Note the definition of an event delegate named Notify. This event is triggered
whenever the value of LatLng property is being changed from JavaScript. This allows you to subscribe to the event, and call the StateHasChanged method in order to refresh the UI.

Index.razor

@page "/"
@implements IDisposable
@inject IJSRuntime JSRuntime

@if (GCS != null)
{
    <div>Latitude and Longitude: @GCS.LatLng</div>
}
<div id="mapid"></div>

@code{
    private DotNetObjectReference<GCSService> objRef;
    private GCSService GCS;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            await JSRuntime.InvokeAsync<object>(
            "leafletJsFunctions.initialize", objRef);
        }
        base.OnAfterRender(firstRender);
    }

    protected override void OnInitialized()
    {
        GCS = new GCSService();

        objRef = DotNetObjectReference.Create(GCS);

        GCS.Notify += OnNotify;
    }

    public void Dispose()
    {
        GCS.Notify -= OnNotify;

        objRef?.Dispose();
    }

    public async Task OnNotify()
    {
        await InvokeAsync(() =>
        {
            StateHasChanged();
        });
   }
 }

Add this CSS rule to the app.css:

#mapid {
    height: 400px;
}

Note that your JavaScript object is initialized only once and from the OnAfterRenderAsync lifecycle method…

Here is the relevant JavaScript code that should be placed at the bottom of the index.html file, below the script element for blazor.webassembly.js

<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css"
          integrity="sha512-xodZBNTC5n17Xt2atTPuE1HxjVMSvLVW9ocqUKLsCC5CXdbqCmblAshOMAS6/keqq/sMZMZ19scR4PsZChSR7A=="
          crossorigin="" />

<!-- Make sure you put this AFTER Leaflet's CSS -->
<script src="https://unpkg.com/[email protected]/dist/leaflet.js"
        integrity="sha512-XQoYMqMTK8LvdxXYG3nZ448hOEQiglfqkJs1NOQV44cWnUrBc8PkAOcXy20w0vlaXaVUearIOBhiXZ5V3ynxwA=="
        crossorigin="">

</script>

<script type="text/javascript">
    window.leafletJsFunctions = {
        initialize: function (dotnetHelper) {
            var mymap = L.map('mapid').setView([51.505, -0.09], 13);
            L.tileLayer('https://api.mapbox.com/styles/v1/{id}/tiles/{z}/{x}/{y}?access_token=pk.eyJ1IjoibWFwYm94IiwiYSI6ImNpejY4NXVycTA2emYycXBndHRqcmZ3N3gifQ.rJcFIG214AriISLbB6B5aw', {
                maxZoom: 18,
                attribution: 'Map data &copy; <a href="https://www.openstreetmap.org/">OpenStreetMap</a> contributors, ' +
                    '<a href="https://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>, ' +
                    'Imagery © <a href="https://www.mapbox.com/">Mapbox</a>',
                id: 'mapbox/streets-v11',
                tileSize: 512,
                zoomOffset: -1
            }).addTo(mymap);

            L.marker([51.5, -0.09]).addTo(mymap)
                .bindPopup("<b>Hello world!</b><br />I am a popup.").openPopup();

            L.circle([51.508, -0.11], 500, {
                color: 'red',
                fillColor: '#f03',
                fillOpacity: 0.5
            }).addTo(mymap).bindPopup("I am a circle.");

            L.polygon([
                [51.509, -0.08],
                [51.503, -0.06],
                [51.51, -0.047]
            ]).addTo(mymap).bindPopup("I am a polygon.");

            var popup = L.popup();

            function onMapClick(e) {
                // Invoke the instance method GetLatLng, passing it the
                // Latitude and Logitude value
                return dotnetHelper.invokeMethodAsync('GetLatLng',
                                            e.latlng.toString());
             }

            mymap.on('click', onMapClick);
       }
     };
</script>

Leave a Comment