From f4e87bb9df232432c4bfc49bd4685635b835a21f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85dne=20Sigurd=20Leirdal?= Date: Mon, 22 Dec 2025 13:40:30 +0100 Subject: [PATCH 1/3] Added marker event handlers --- .../MapLibre.razor.cs | 83 +++++++++++++++++-- .../MapLibre.razor.js | 58 ++++++++++++- .../Models/Marker/MarkerEvent.cs | 21 +++++ .../Models/Marker/MarkerOptions.cs | 54 ++++++++++++ 4 files changed, 209 insertions(+), 7 deletions(-) create mode 100644 src/Community.Blazor.MapLibre/Models/Marker/MarkerEvent.cs diff --git a/src/Community.Blazor.MapLibre/MapLibre.razor.cs b/src/Community.Blazor.MapLibre/MapLibre.razor.cs index 01b337b..d135d17 100644 --- a/src/Community.Blazor.MapLibre/MapLibre.razor.cs +++ b/src/Community.Blazor.MapLibre/MapLibre.razor.cs @@ -146,7 +146,8 @@ await JsRuntime.InvokeAsync("import", Options.Container = MapId; // Initialize the MapLibre map - _mapObject = await _jsModule.InvokeAsync("initializeMap", Options, _dotNetObjectReference); + _mapObject = + await _jsModule.InvokeAsync("initializeMap", Options, _dotNetObjectReference); // Load the plugins after the map has been initialized foreach (var plugin in _plugins) @@ -239,7 +240,7 @@ public Task OnClick(string? layerId, Action handler) => /// The asynchronous callback. /// A task of type . public Task OnClick(string? layerId, Func handler) => - AddAsyncListener("click", handler, layerId); + AddAsyncListener("click", handler, layerId); #endregion @@ -276,6 +277,7 @@ public async ValueTask AddImage(string id, string url, StyleImageMetadata? optio _bulkTransaction.Add("addImage", id, url, options); return; } + await _jsModule.InvokeVoidAsync("addImage", MapId, id, url, options); } @@ -292,6 +294,7 @@ public async ValueTask AddLayer(Layer layer, string? beforeId = null) _bulkTransaction.Add("addLayer", layer, beforeId); return; } + await _jsModule.InvokeVoidAsync("addLayer", MapId, layer, beforeId); } @@ -308,6 +311,7 @@ public async ValueTask AddSource(string id, ISource source) _bulkTransaction.Add("addSource", id, source); return; } + await _jsModule.InvokeVoidAsync("addSource", MapId, id, source); } @@ -324,6 +328,7 @@ public async ValueTask SetSourceData(string id, GeoJsonSource source) _bulkTransaction.Add("setSourceData", id, dataNode); return; } + await _jsModule.InvokeVoidAsync("setSourceData", MapId, id, dataNode); } @@ -341,6 +346,7 @@ public async ValueTask AddSprite(string id, string url, StyleSetterOptions? opti _bulkTransaction.Add("addSprite", id, url, options); return; } + await _jsModule.InvokeVoidAsync("addSprite", MapId, id, url, options); } @@ -390,7 +396,8 @@ await _jsModule.InvokeAsync("calculateCameraOptionsFromTo", MapId /// The geographical bounding box to be fitted. /// Optional parameters to customize the calculation. /// A task that represents the asynchronous operation, containing the resulting center, zoom, and bearing. - public async ValueTask CameraForBounds(LngLatBounds bounds, CameraForBoundsOptions? options = null) => + public async ValueTask CameraForBounds(LngLatBounds bounds, + CameraForBoundsOptions? options = null) => await _jsModule.InvokeAsync("cameraForBounds", MapId, bounds, options); /// @@ -939,7 +946,8 @@ public async ValueTask QueryRenderedFeatures(object query, object? opt /// }); /// /// - public async ValueTask QuerySourceFeatures(string sourceId, QuerySourceFeatureOptions parameters) => + public async ValueTask + QuerySourceFeatures(string sourceId, QuerySourceFeatureOptions parameters) => await _jsModule.InvokeAsync("querySourceFeatures", MapId, sourceId, parameters); /// @@ -983,6 +991,7 @@ public async ValueTask RemoveControl(object control) _bulkTransaction.Add("removeControl", control); return; } + await _jsModule.InvokeVoidAsync("removeControl", MapId, control); } @@ -1036,6 +1045,7 @@ public async ValueTask RemoveFeatureState(FeatureIdentifier target, string? key _bulkTransaction.Add("removeFeatureState", target, key); return; } + await _jsModule.InvokeVoidAsync("removeFeatureState", MapId, target, key); } @@ -1050,6 +1060,7 @@ public async ValueTask RemoveImage(string id) _bulkTransaction.Add("removeImage", id); return; } + await _jsModule.InvokeVoidAsync("removeImage", MapId, id); } @@ -1064,6 +1075,7 @@ public async ValueTask RemoveLayer(string id) _bulkTransaction.Add("removeLayer", id); return; } + await _jsModule.InvokeVoidAsync("removeLayer", MapId, id); } @@ -1078,6 +1090,7 @@ public async ValueTask RemoveSource(string id) _bulkTransaction.Add("removeSource", id); return; } + await _jsModule.InvokeVoidAsync("removeSource", MapId, id); } @@ -1092,6 +1105,7 @@ public async ValueTask RemoveSprite(string id) _bulkTransaction.Add("removeSprite", id); return; } + await _jsModule.InvokeVoidAsync("removeSprite", MapId, id); } @@ -1316,6 +1330,31 @@ public async Task AddMarker(MarkerOptions options, LngLat position, Guid? { var id = markerId ?? Guid.NewGuid(); await _jsModule.InvokeVoidAsync("createMarker", MapId, id, options, position); + // Register event handlers if provided + if (options.OnClick != null) + await AddMarkerListener(id, "click", options.OnClick); + + if (options.OnClickAsync != null) + await AddAsyncMarkerListener(id, "click", options.OnClickAsync); + + if (options.OnDragStart != null) + await AddMarkerListener(id, "dragstart", options.OnDragStart); + + if (options.OnDragStartAsync != null) + await AddAsyncMarkerListener(id, "dragstart", options.OnDragStartAsync); + + if (options.OnDrag != null) + await AddMarkerListener(id, "drag", options.OnDrag); + + if (options.OnDragAsync != null) + await AddAsyncMarkerListener(id, "drag", options.OnDragAsync); + + if (options.OnDragEnd != null) + await AddMarkerListener(id, "dragend", options.OnDragEnd); + + if (options.OnDragEndAsync != null) + await AddAsyncMarkerListener(id, "dragend", options.OnDragEndAsync); + return id; } @@ -1335,6 +1374,39 @@ public async Task RemoveMarker(Guid markerId) public async Task MoveMarker(Guid markerId, LngLat position) => await _jsModule.InvokeVoidAsync("moveMarker", markerId, position); + /// + /// Registers a synchronous event listener for a marker. + /// + /// The type of the event payload (typically ). + /// The id of the marker. + /// The name of the event to listen for (e.g., "click", "dragstart", "drag", "dragend"). + /// The synchronous callback action to execute when the event occurs. + /// A instance that allows removal of the registered listener. + public Task AddMarkerListener(Guid markerId, string eventName, Action handler) => + AddMarkerListenerInternal(markerId, eventName, handler); + + /// + /// Registers an asynchronous event listener for a marker. + /// + /// The type of the event payload (typically ). + /// The id of the marker. + /// The name of the event to listen for (e.g., "click", "dragstart", "drag", "dragend"). + /// The asynchronous callback action to execute when the event occurs. + /// A instance that allows removal of the registered listener. + public Task AddAsyncMarkerListener(Guid markerId, string eventName, Func handler) => + AddMarkerListenerInternal(markerId, eventName, handler); + + private async Task AddMarkerListenerInternal(Guid markerId, string eventName, Delegate handler) + { + var callback = new CallbackHandler(_jsModule, eventName, handler, typeof(T)); + var reference = DotNetObjectReference.Create(callback); + _references.TryAdd(Guid.NewGuid(), reference); + + await _jsModule.InvokeVoidAsync("onMarker", markerId, eventName, reference); + + return new Listener(callback); + } + #endregion #region Bulk Transaction @@ -1370,5 +1442,4 @@ public async ValueTask Commit() } #endregion - -} +} \ No newline at end of file diff --git a/src/Community.Blazor.MapLibre/MapLibre.razor.js b/src/Community.Blazor.MapLibre/MapLibre.razor.js index 8e94f38..fb15f99 100644 --- a/src/Community.Blazor.MapLibre/MapLibre.razor.js +++ b/src/Community.Blazor.MapLibre/MapLibre.razor.js @@ -38,7 +38,7 @@ export function initializeMap(options, dotnetReference) { map.on('load', function () { dotnetReference.invokeMethodAsync("OnLoadCallback") }); - + return map; } @@ -65,6 +65,7 @@ export function on(container, eventType, dotnetReference, layerIds) { }) } } + /** * Adds a specified control to the given map container. * @@ -1231,6 +1232,10 @@ export function createMarker(container, markerId, options, position) { markerInstances[markerId] = new maplibregl.Marker(options) .setLngLat([position.lng, position.lat]) .addTo(mapInstances[container]); + + if (!markerInstances[markerId]._clickListeners) { + markerInstances[markerId]._clickListeners = []; + } if (options.extensions) { const extensions = options.extensions; @@ -1269,6 +1274,57 @@ export function moveMarker(markerId, position) { marker.setLngLat([position.lng, position.lat]); } +/** + * Attaches an event listener to a specified marker instance. + * + * @param {string} markerId - The marker ID. + * @param {string} eventType - The type of event to listen for (e.g., "click", "dragstart", "drag", "dragend"). + * @param {object} dotnetReference - A reference to a .NET object used for invoking asynchronous methods. + */ +export function onMarker(markerId, eventType, dotnetReference) { + const marker = markerInstances[markerId]; + + if (!marker) { + console.warn(`Marker with id ${markerId} not found`); + return; + } + const createEventData = () => { + const lngLat = marker.getLngLat(); + return { + type: eventType, + lngLat: { + lng: lngLat.lng, + lat: lngLat.lat + } + }; + }; + + if (eventType === 'click') { + const clickHandler = (e) => { + e.stopPropagation(); // Prevent event from bubbling to map + const eventData = createEventData(); + const result = JSON.stringify(eventData); + dotnetReference.invokeMethodAsync('Invoke', result); + }; + + marker.getElement().addEventListener('click', clickHandler); + + // Store listener for cleanup + if (!marker._clickListeners) { + marker._clickListeners = []; + } + marker._clickListeners.push({handler: clickHandler, dotnetRef: dotnetReference}); + } else if (['dragstart', 'drag', 'dragend'].includes(eventType)) { + marker.on(eventType, function (e) { + const eventData = createEventData(); + const result = JSON.stringify(eventData); + dotnetReference.invokeMethodAsync('Invoke', result); + }); + } else { + console.warn(`Event type '${eventType}' is not supported for markers`); + } +} + /** * Perform all applied bulk transactions. * The only purpose of bulk transaction send multiple transactions in one message, reducing the roundtrip time. diff --git a/src/Community.Blazor.MapLibre/Models/Marker/MarkerEvent.cs b/src/Community.Blazor.MapLibre/Models/Marker/MarkerEvent.cs new file mode 100644 index 0000000..600d5ba --- /dev/null +++ b/src/Community.Blazor.MapLibre/Models/Marker/MarkerEvent.cs @@ -0,0 +1,21 @@ +namespace Community.Blazor.MapLibre.Models.Marker; + +using System.Text.Json.Serialization; + +/// +/// Represents an event that occurred on a marker. +/// +public class MarkerEvent +{ + /// + /// The type of event (e.g., "click", "dragstart", "drag", "dragend"). + /// + [JsonPropertyName("type")] + public string Type { get; set; } = string.Empty; + + /// + /// The geographical position of the marker when the event occurred. + /// + [JsonPropertyName("lngLat")] + public LngLat LngLat { get; set; } = new(); +} \ No newline at end of file diff --git a/src/Community.Blazor.MapLibre/Models/Marker/MarkerOptions.cs b/src/Community.Blazor.MapLibre/Models/Marker/MarkerOptions.cs index fda662e..6a4ad38 100644 --- a/src/Community.Blazor.MapLibre/Models/Marker/MarkerOptions.cs +++ b/src/Community.Blazor.MapLibre/Models/Marker/MarkerOptions.cs @@ -115,6 +115,60 @@ public class MarkerOptions [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public bool? SubpixelPositioning { get; set; } + #region Event Handlers + + /// + /// Event handler called when the marker is clicked. + /// + /// Important: This will overwrite any other click-related listeners for the marker, such as Popup. + [JsonIgnore] + public Action? OnClick { get; set; } + + /// + /// Async event handler called when the marker is clicked. + /// + /// Important: This will overwrite any other click-related listeners for the marker, such as Popup. + [JsonIgnore] + public Func? OnClickAsync { get; set; } + + /// + /// Event handler called when dragging starts. + /// + [JsonIgnore] + public Action? OnDragStart { get; set; } + + /// + /// Async event handler called when dragging starts. + /// + [JsonIgnore] + public Func? OnDragStartAsync { get; set; } + + /// + /// Event handler called while dragging. + /// + [JsonIgnore] + public Action? OnDrag { get; set; } + + /// + /// Async event handler called while dragging. + /// + [JsonIgnore] + public Func? OnDragAsync { get; set; } + + /// + /// Event handler called when dragging ends. + /// + [JsonIgnore] + public Action? OnDragEnd { get; set; } + + /// + /// Async event handler called when dragging ends. + /// + [JsonIgnore] + public Func? OnDragEndAsync { get; set; } + + #endregion + /// /// An object representing additional, non-standard extensions for marker configuration. /// Extensions can include properties or functionality not directly supported by the default MarkerOptions, allowing for enhanced customization. From c7993e024c812f9e450100f94c2a28e8a4aec792 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85dne=20Sigurd=20Leirdal?= Date: Mon, 22 Dec 2025 13:47:33 +0100 Subject: [PATCH 2/3] Added remarks for dragging events. --- .../Models/Marker/MarkerOptions.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Community.Blazor.MapLibre/Models/Marker/MarkerOptions.cs b/src/Community.Blazor.MapLibre/Models/Marker/MarkerOptions.cs index 6a4ad38..ee79c80 100644 --- a/src/Community.Blazor.MapLibre/Models/Marker/MarkerOptions.cs +++ b/src/Community.Blazor.MapLibre/Models/Marker/MarkerOptions.cs @@ -134,36 +134,42 @@ public class MarkerOptions /// /// Event handler called when dragging starts. /// + /// Important: Draggable MUST be true for the event handler to be registered. [JsonIgnore] public Action? OnDragStart { get; set; } /// /// Async event handler called when dragging starts. /// + /// Important: Draggable MUST be true for the event handler to be registered. [JsonIgnore] public Func? OnDragStartAsync { get; set; } /// /// Event handler called while dragging. /// + /// Important: Draggable MUST be true for the event handler to be registered. [JsonIgnore] public Action? OnDrag { get; set; } /// /// Async event handler called while dragging. /// + /// Important: Draggable MUST be true for the event handler to be registered. [JsonIgnore] public Func? OnDragAsync { get; set; } /// /// Event handler called when dragging ends. /// + /// Important: Draggable MUST be true for the event handler to be registered. [JsonIgnore] public Action? OnDragEnd { get; set; } /// /// Async event handler called when dragging ends. /// + /// Important: Draggable MUST be true for the event handler to be registered. [JsonIgnore] public Func? OnDragEndAsync { get; set; } From ec344b4fca24a1aefc8b8b9dc99c4742ecd5ade1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85dne=20Sigurd=20Leirdal?= Date: Mon, 22 Dec 2025 13:47:50 +0100 Subject: [PATCH 3/3] Updated example to show latitude, longitude when dragging a marker. --- .../Examples/AddMarker.razor | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/examples/Community.Blazor.MapLibre.Examples/Examples/AddMarker.razor b/examples/Community.Blazor.MapLibre.Examples/Examples/AddMarker.razor index eced5d2..cd12269 100644 --- a/examples/Community.Blazor.MapLibre.Examples/Examples/AddMarker.razor +++ b/examples/Community.Blazor.MapLibre.Examples/Examples/AddMarker.razor @@ -10,11 +10,17 @@ Height="600px"> - +
+
+ +

Try drag a marker!

+
+

@DragInfo

+
@code { private MapLibre _mapListener { get; set; } = new MapLibre(); - + public string DragInfo { get; set; } = ""; private readonly MapOptions _mapOptions = new() { Style = "https://demotiles.maplibre.org/style.json" @@ -24,6 +30,8 @@ { var options = new MarkerOptions { + Draggable = true, + OnDragEndAsync = OnMarkerDragEnd, Extensions = new MarkerOptionsExtensions { HtmlContent = "
", @@ -33,5 +41,14 @@ await _mapListener.AddMarker(options, new LngLat(Random.Shared.NextDouble() * 180 - 90, Random.Shared.Next(-90, 90))); } + + private Task OnMarkerDragEnd(MarkerEvent arg) + { + DragInfo = $"Lat: {arg.LngLat.Latitude}, Lng: {arg.LngLat.Longitude}"; + Console.WriteLine($"Marker dragged to: Lat={arg.LngLat.Latitude}, Lng={arg.LngLat.Longitude}"); + StateHasChanged(); + + return Task.CompletedTask; + } }