From 8b403208b3430eedcabede25a64587c7e678ed31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roope=20Herpi=C3=B6?= Date: Mon, 27 Jan 2025 12:49:58 +0200 Subject: [PATCH 1/3] Add Drawing Manager component that can be used to add or remove shapes from drawing manager datasource --- .../Drawing/DrawingManager.cs | 151 ++++++++++++++++++ src/AzureMapsControl.Components/Map/Map.cs | 8 + .../typescript/drawing/drawing.ts | 11 ++ 3 files changed, 170 insertions(+) create mode 100644 src/AzureMapsControl.Components/Drawing/DrawingManager.cs diff --git a/src/AzureMapsControl.Components/Drawing/DrawingManager.cs b/src/AzureMapsControl.Components/Drawing/DrawingManager.cs new file mode 100644 index 0000000..1fe6879 --- /dev/null +++ b/src/AzureMapsControl.Components/Drawing/DrawingManager.cs @@ -0,0 +1,151 @@ +namespace AzureMapsControl.Components.Drawing +{ + using System.Collections.Generic; + using System.Linq; + using System.Threading.Tasks; + + using AzureMapsControl.Components.Atlas; + using AzureMapsControl.Components.Logger; + using AzureMapsControl.Components.Runtime; + + using Microsoft.Extensions.Logging; + + + /// + /// DrawingManager for the DrawingToolbar + /// + public sealed class DrawingManager + { + internal IMapJsRuntime JSRuntime { get; set; } + internal ILogger Logger { get; set; } + public bool Disposed { get; private set; } + + /// + /// List of shapes added to the data source + /// + private List _sourceShapes; + + /// + /// Add shapes to the drawing manager data source + /// + /// Shapes to add + /// + /// The control has not been added to the map + /// The control has already been disposed + public async ValueTask AddShapesAsync(IEnumerable shapes) + { + if (shapes == null || !shapes.Any()) + { + return; + } + + EnsureJsRuntimeExists(); + EnsureNotDisposed(); + + if (_sourceShapes == null) + { + _sourceShapes = new List(); + } + + var lineStrings = shapes.OfType>(); + if (lineStrings.Any()) + { + Logger?.LogAzureMapsControlDebug(AzureMapLogEvent.Source_AddAsync, $"{lineStrings.Count()} linestrings will be added"); + await JSRuntime.InvokeVoidAsync(Constants.JsConstants.Methods.Source.AddShapes.ToDrawingNamespace(), lineStrings); + } + + var multiLineStrings = shapes.OfType>(); + if (multiLineStrings.Any()) + { + Logger?.LogAzureMapsControlDebug(AzureMapLogEvent.Source_AddAsync, $"{multiLineStrings.Count()} multilinestrings will be added"); + await JSRuntime.InvokeVoidAsync(Constants.JsConstants.Methods.Source.AddShapes.ToDrawingNamespace(), multiLineStrings); + } + + var multiPoints = shapes.OfType>(); + if (multiPoints.Any()) + { + Logger?.LogAzureMapsControlDebug(AzureMapLogEvent.Source_AddAsync, $"{multiPoints.Count()} multipoints will be added"); + await JSRuntime.InvokeVoidAsync(Constants.JsConstants.Methods.Source.AddShapes.ToDrawingNamespace(), multiPoints); + } + + var multiPolygons = shapes.OfType>(); + if (multiPolygons.Any()) + { + Logger?.LogAzureMapsControlDebug(AzureMapLogEvent.Source_AddAsync, $"{multiPolygons.Count()} multipolygons will be added"); + await JSRuntime.InvokeVoidAsync(Constants.JsConstants.Methods.Source.AddShapes.ToDrawingNamespace(), multiPolygons); + } + + var points = shapes.OfType>(); + if (points.Any()) + { + Logger?.LogAzureMapsControlDebug(AzureMapLogEvent.Source_AddAsync, $"{points.Count()} points will be added"); + await JSRuntime.InvokeVoidAsync(Constants.JsConstants.Methods.Source.AddShapes.ToDrawingNamespace(), points); + } + + var polygons = shapes.OfType>(); + if (polygons.Any()) + { + Logger?.LogAzureMapsControlDebug(AzureMapLogEvent.Source_AddAsync, $"{polygons.Count()} polygons will be added"); + await JSRuntime.InvokeVoidAsync(Constants.JsConstants.Methods.Source.AddShapes.ToDrawingNamespace(), polygons); + } + + var routePoints = shapes.OfType>(); + if (routePoints.Any()) + { + Logger?.LogAzureMapsControlDebug(AzureMapLogEvent.Source_AddAsync, $"{routePoints.Count()} route points will be added"); + await JSRuntime.InvokeVoidAsync(Constants.JsConstants.Methods.Source.AddShapes.ToDrawingNamespace(), routePoints); + } + + _sourceShapes.AddRange(shapes); + } + + /// + /// Clear the drawing manager source + /// + /// + /// The control has not been added to the map + /// The control has already been disposed + public async ValueTask ClearAsync() + { + Logger?.LogAzureMapsControlInfo(AzureMapLogEvent.Source_ClearAsync, "Clearing drawing manager source"); + + EnsureJsRuntimeExists(); + EnsureNotDisposed(); + + _sourceShapes = null; + await JSRuntime.InvokeVoidAsync(Constants.JsConstants.Methods.Source.Clear.ToDrawingNamespace()); + } + + /// + /// Mark the control as disposed + /// + /// + /// The control has not been added to the map + /// The control has already been disposed + internal void Dispose() + { + Logger?.LogAzureMapsControlInfo(AzureMapLogEvent.Source_DisposeAsync, "DrawingManager - Dispose"); + + EnsureJsRuntimeExists(); + EnsureNotDisposed(); + + Disposed = true; + } + + private void EnsureJsRuntimeExists() + { + if (JSRuntime is null) + { + throw new Exceptions.ComponentNotAddedToMapException(); + } + } + + private void EnsureNotDisposed() + { + if (Disposed) + { + throw new Exceptions.ComponentDisposedException(); + } + } + } +} diff --git a/src/AzureMapsControl.Components/Map/Map.cs b/src/AzureMapsControl.Components/Map/Map.cs index 0bfee93..3b28206 100644 --- a/src/AzureMapsControl.Components/Map/Map.cs +++ b/src/AzureMapsControl.Components/Map/Map.cs @@ -67,6 +67,8 @@ public sealed class Map public DrawingToolbarOptions DrawingToolbarOptions { get; internal set; } + public DrawingManager DrawingManager { get; internal set; } + public IEnumerable Controls => _controls; public IEnumerable Layers => _layers; @@ -352,6 +354,10 @@ await _jsRuntime.InvokeVoidAsync(Constants.JsConstants.Methods.Drawing.AddDrawin Events = drawingToolbarOptions.Events?.EnabledEvents }, DotNetObjectReference.Create(_drawingToolbarEventInvokeHelper)); + DrawingManager = new DrawingManager() { + JSRuntime = _jsRuntime, + Logger = _logger + }; } } @@ -394,6 +400,8 @@ public async ValueTask RemoveDrawingToolbarAsync() { await _jsRuntime.InvokeVoidAsync(Constants.JsConstants.Methods.Drawing.RemoveDrawingToolbar.ToDrawingNamespace()); DrawingToolbarOptions = null; + DrawingManager?.Dispose(); + DrawingManager = null; } } diff --git a/src/AzureMapsControl.Components/typescript/drawing/drawing.ts b/src/AzureMapsControl.Components/typescript/drawing/drawing.ts index 4959cac..69120bf 100644 --- a/src/AzureMapsControl.Components/typescript/drawing/drawing.ts +++ b/src/AzureMapsControl.Components/typescript/drawing/drawing.ts @@ -3,6 +3,8 @@ import * as azmaps from 'azure-maps-control'; import { EventHelper } from '../events/event-helper'; import { Core } from '../core/core'; import { DrawingEventArgs } from './drawing-event-args'; +import { Shape } from '../geometries/geometry'; +import { GeometryBuilder } from '../geometries/geometry-builder'; export class Drawing { @@ -96,4 +98,13 @@ export class Drawing { }); } + public static addShapes(shapes: Shape[]): void { + const mapsShapes = shapes.map(shape => GeometryBuilder.buildShape(shape)); + this._drawingManager.getSource().add(mapsShapes); + } + + public static clear(): void { + this._drawingManager.getSource().clear(); + } + } \ No newline at end of file From 51cc2c40410e666f24fe5bb94b2ae0d1779b6d38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roope=20Herpi=C3=B6?= Date: Mon, 27 Jan 2025 15:39:02 +0200 Subject: [PATCH 2/3] Add sample page for adding and removing shapes. --- .../Components/Layout/NavMenu.razor | 3 + .../Drawing/DrawingManagerLoadData.razor | 103 ++++++++++++++++++ 2 files changed, 106 insertions(+) create mode 100644 samples/AzureMapsControl.Sample/Components/Pages/Drawing/DrawingManagerLoadData.razor diff --git a/samples/AzureMapsControl.Sample/Components/Layout/NavMenu.razor b/samples/AzureMapsControl.Sample/Components/Layout/NavMenu.razor index 4f33778..4a0e462 100644 --- a/samples/AzureMapsControl.Sample/Components/Layout/NavMenu.razor +++ b/samples/AzureMapsControl.Sample/Components/Layout/NavMenu.razor @@ -86,6 +86,9 @@
  • Toolbar update
  • +
  • + Load data to drawing manager +
  • Indoor diff --git a/samples/AzureMapsControl.Sample/Components/Pages/Drawing/DrawingManagerLoadData.razor b/samples/AzureMapsControl.Sample/Components/Pages/Drawing/DrawingManagerLoadData.razor new file mode 100644 index 0000000..6a88084 --- /dev/null +++ b/samples/AzureMapsControl.Sample/Components/Pages/Drawing/DrawingManagerLoadData.razor @@ -0,0 +1,103 @@ +@page "/Drawing/DrawingManagerLoadData" +@rendermode InteractiveServer + +@using AzureMapsControl.Components.Atlas +@using AzureMapsControl.Components.Drawing +@using AzureMapsControl.Components.Map + + + +
    + + +
    + + +@code { + private DrawingManager? _drawingManager; + private Position _center = new Position(-122.33, 47.6); + + public async Task MapReady(MapEventArgs eventArgs) + { + await eventArgs.Map.SetCameraOptionsAsync(options => + { + options.Zoom = 10; + options.Center = _center; + }); + await eventArgs.Map.AddDrawingToolbarAsync(new AzureMapsControl.Components.Drawing.DrawingToolbarOptions + { + Buttons = new[] + { + AzureMapsControl.Components.Drawing.DrawingButton.DrawCircle, + AzureMapsControl.Components.Drawing.DrawingButton.DrawLine, + AzureMapsControl.Components.Drawing.DrawingButton.EditGeometry + }, + Position = AzureMapsControl.Components.Controls.ControlPosition.TopRight, + Style = AzureMapsControl.Components.Drawing.DrawingToolbarStyle.Dark + }); + + var lineString = new AzureMapsControl.Components.Atlas.LineString(new[] + { + new AzureMapsControl.Components.Atlas.Position(-122.27577, 47.55938), + new AzureMapsControl.Components.Atlas.Position(-122.29705, 47.60662), + new AzureMapsControl.Components.Atlas.Position(-122.22358, 47.6367) + }); + var shape = new AzureMapsControl.Components.Atlas.Shape(lineString); + _drawingManager = eventArgs.Map.DrawingManager; + await _drawingManager.AddShapesAsync(new[] { shape }); + } + + private async Task AddRandomShape() + { + if (_drawingManager == null) return; + + var random = new Random(); + var shapeType = random.Next(3); + Shape shape; + var numberOfPoints = random.Next(3, 5); + var center = new Position(_center.Longitude + (random.NextDouble()-0.5) * 0.6, _center.Latitude + (random.NextDouble()-0.5) * 0.4); + + switch (shapeType) + { + case 0: // Circle + var radius = random.NextDouble() * 2000; + shape = new Shape(new Point(center), new Dictionary + { + { "subType", "Circle" }, + { "radius", radius } + }); + break; + case 1: // Polygon + var polygonPositions = new List(); + for (var i = 0; i < numberOfPoints; i++) + { + polygonPositions.Add(new Position(center.Longitude + (random.NextDouble()-0.5) * 0.1, center.Latitude + (random.NextDouble()-0.5) * 0.1)); + } + polygonPositions.Add(polygonPositions[0]); + shape = new Shape(new Polygon(new[] { polygonPositions })); + break; + case 2: // Polyline + var polylinePositions = new List(); + for (var i = 0; i < numberOfPoints; i++) + { + polylinePositions.Add(new Position(center.Longitude + (random.NextDouble()-0.5) * 0.1, center.Latitude + (random.NextDouble()-0.5) * 0.1)); + } + shape = new Shape(new LineString(polylinePositions)); + break; + default: + return; + } + + await _drawingManager.AddShapesAsync(new[] { shape }); + } + + private async Task ClearShapes() + { + if (_drawingManager != null) + { + await _drawingManager.ClearAsync(); + } + } +} \ No newline at end of file From c6b6b3518f646da5d10949fbc554331b0e69a46c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roope=20Herpi=C3=B6?= Date: Mon, 13 Oct 2025 16:55:41 +0300 Subject: [PATCH 3/3] Add tests for DrawingManager. --- .../Drawing/DrawingManager.cs | 313 ++++++++++++++++++ 1 file changed, 313 insertions(+) create mode 100644 tests/AzureMapsControl.Components.Tests/Drawing/DrawingManager.cs diff --git a/tests/AzureMapsControl.Components.Tests/Drawing/DrawingManager.cs b/tests/AzureMapsControl.Components.Tests/Drawing/DrawingManager.cs new file mode 100644 index 0000000..f6eab30 --- /dev/null +++ b/tests/AzureMapsControl.Components.Tests/Drawing/DrawingManager.cs @@ -0,0 +1,313 @@ +namespace AzureMapsControl.Components.Tests.Drawing +{ + using System.Collections.Generic; + using System.Linq; + using System.Reflection; + using System.Threading.Tasks; + + using AzureMapsControl.Components.Atlas; + using AzureMapsControl.Components.Drawing; + using AzureMapsControl.Components.Exceptions; + using AzureMapsControl.Components.Runtime; + + using Microsoft.Extensions.Logging; + + using Moq; + + using Xunit; + + public class DrawingManagerTests + { + private readonly Mock _jsRuntimeMock = new(); + private readonly Mock _loggerMock = new(); + + [Fact] + public void Should_HaveDefaultProperties() + { + var drawingManager = new DrawingManager(); + + Assert.False(drawingManager.Disposed); + Assert.Null(drawingManager.JSRuntime); + Assert.Null(drawingManager.Logger); + } + + [Fact] + public void Should_SetJSRuntimeAndLogger() + { + var drawingManager = CreateInitializedDrawingManager(); + + Assert.Equal(_jsRuntimeMock.Object, drawingManager.JSRuntime); + Assert.Equal(_loggerMock.Object, drawingManager.Logger); + } + + [Fact] + public async Task Should_AddShapes_AllSupportedGeometryTypes_Async() + { + var drawingManager = CreateInitializedDrawingManager(); + + var shapes = new List + { + new Shape(new Point()), + new Shape(new LineString()), + new Shape(new MultiLineString()), + new Shape(new MultiPoint()), + new Shape(new MultiPolygon()), + new Shape(new Polygon()), + new Shape(new RoutePoint()), + }; + + await drawingManager.AddShapesAsync(shapes); + + _jsRuntimeMock.Verify(runtime => runtime.InvokeVoidAsync(Constants.JsConstants.Methods.Source.AddShapes.ToDrawingNamespace(), It.Is>>(s => s.Count() == 1)), Times.Once); + _jsRuntimeMock.Verify(runtime => runtime.InvokeVoidAsync(Constants.JsConstants.Methods.Source.AddShapes.ToDrawingNamespace(), It.Is>>(s => s.Count() == 1)), Times.Once); + _jsRuntimeMock.Verify(runtime => runtime.InvokeVoidAsync(Constants.JsConstants.Methods.Source.AddShapes.ToDrawingNamespace(), It.Is>>(s => s.Count() == 1)), Times.Once); + _jsRuntimeMock.Verify(runtime => runtime.InvokeVoidAsync(Constants.JsConstants.Methods.Source.AddShapes.ToDrawingNamespace(), It.Is>>(s => s.Count() == 1)), Times.Once); + _jsRuntimeMock.Verify(runtime => runtime.InvokeVoidAsync(Constants.JsConstants.Methods.Source.AddShapes.ToDrawingNamespace(), It.Is>>(s => s.Count() == 1)), Times.Once); + _jsRuntimeMock.Verify(runtime => runtime.InvokeVoidAsync(Constants.JsConstants.Methods.Source.AddShapes.ToDrawingNamespace(), It.Is>>(s => s.Count() == 1)), Times.Once); + _jsRuntimeMock.Verify(runtime => runtime.InvokeVoidAsync(Constants.JsConstants.Methods.Source.AddShapes.ToDrawingNamespace(), It.Is>>(s => s.Count() == 1)), Times.Once); + _jsRuntimeMock.Verify(runtime => runtime.InvokeVoidAsync(Constants.JsConstants.Methods.Source.AddShapes.ToDrawingNamespace(), It.IsAny()), Times.Exactly(7)); + + _jsRuntimeMock.VerifyNoOtherCalls(); + } + + [Fact] + public async Task Should_AddMultipleShapesOfSameType_Async() + { + var drawingManager = CreateInitializedDrawingManager(); + + var shapes = new List + { + new Shape(new Point()), + new Shape(new Point()), + new Shape(new Point()) + }; + + await drawingManager.AddShapesAsync(shapes); + + _jsRuntimeMock.Verify(runtime => runtime.InvokeVoidAsync(Constants.JsConstants.Methods.Source.AddShapes.ToDrawingNamespace(), It.Is>>(s => s.Count() == 3)), Times.Once); + _jsRuntimeMock.VerifyNoOtherCalls(); + } + + [Fact] + public async Task Should_HandleMixedGeometryTypes_InSingleCall_Async() + { + var drawingManager = CreateInitializedDrawingManager(); + + var shapes = new List + { + new Shape(new Point()), + new Shape(new Point()), + new Shape(new LineString()), + new Shape(new Polygon()), + new Shape(new Polygon()), + new Shape(new Polygon()) + }; + + await drawingManager.AddShapesAsync(shapes); + + _jsRuntimeMock.Verify(runtime => runtime.InvokeVoidAsync(Constants.JsConstants.Methods.Source.AddShapes.ToDrawingNamespace(), It.Is>>(s => s.Count() == 2)), Times.Once); + _jsRuntimeMock.Verify(runtime => runtime.InvokeVoidAsync(Constants.JsConstants.Methods.Source.AddShapes.ToDrawingNamespace(), It.Is>>(s => s.Count() == 1)), Times.Once); + _jsRuntimeMock.Verify(runtime => runtime.InvokeVoidAsync(Constants.JsConstants.Methods.Source.AddShapes.ToDrawingNamespace(), It.Is>>(s => s.Count() == 3)), Times.Once); + _jsRuntimeMock.Verify(runtime => runtime.InvokeVoidAsync(Constants.JsConstants.Methods.Source.AddShapes.ToDrawingNamespace(), It.IsAny()), Times.Exactly(3)); + + _jsRuntimeMock.VerifyNoOtherCalls(); + } + + [Fact] + public async Task Should_HandleLargeNumberOfShapes_Efficiently_Async() + { + var drawingManager = CreateInitializedDrawingManager(); + + // Create a large collection of shapes + var shapes = new List(); + for (int i = 0; i < 1000; i++) + { + shapes.Add(new Shape(new Point())); + } + + await drawingManager.AddShapesAsync(shapes); + + // Should still only make one call per geometry type + _jsRuntimeMock.Verify(runtime => runtime.InvokeVoidAsync(Constants.JsConstants.Methods.Source.AddShapes.ToDrawingNamespace(), It.Is>>(s => s.Count() == 1000)), Times.Once); + _jsRuntimeMock.VerifyNoOtherCalls(); + } + + [Fact] + public async Task Should_NotAddShapes_WhenNull_Async() + { + var drawingManager = CreateInitializedDrawingManager(); + + await drawingManager.AddShapesAsync(null); + + _jsRuntimeMock.VerifyNoOtherCalls(); + } + + [Fact] + public async Task Should_NotAddShapes_WhenEmpty_Async() + { + var drawingManager = CreateInitializedDrawingManager(); + + await drawingManager.AddShapesAsync(new List()); + + _jsRuntimeMock.VerifyNoOtherCalls(); + } + + [Fact] + public async Task Should_ClearShapes_Async() + { + var drawingManager = CreateInitializedDrawingManager(); + + await drawingManager.ClearAsync(); + + _jsRuntimeMock.Verify(runtime => runtime.InvokeVoidAsync(Constants.JsConstants.Methods.Source.Clear.ToDrawingNamespace()), Times.Once); + _jsRuntimeMock.VerifyNoOtherCalls(); + } + + [Fact] + public async Task Should_AccumulateShapes_InInternalState_Async() + { + var drawingManager = CreateInitializedDrawingManager(); + + var firstBatch = new List { new Shape(new Point()) }; + var secondBatch = new List { new Shape(new LineString()) }; + var thirdBatch = new List { new Shape(new Point()) }; + + await drawingManager.AddShapesAsync(firstBatch); + await drawingManager.AddShapesAsync(secondBatch); + await drawingManager.AddShapesAsync(thirdBatch); + + // Verify internal state accumulates all shapes + var sourceShapes = GetInternalSourceShapes(drawingManager); + Assert.Equal(3, sourceShapes.Count); + Assert.Contains(firstBatch[0], sourceShapes); + Assert.Contains(secondBatch[0], sourceShapes); + Assert.Contains(thirdBatch[0], sourceShapes); + + // Verify correct number of JS calls + _jsRuntimeMock.Verify(runtime => runtime.InvokeVoidAsync( + Constants.JsConstants.Methods.Source.AddShapes.ToDrawingNamespace(), + It.IsAny()), Times.Exactly(3)); // 1 Point call + 1 LineString call + 1 more Point call + } + + [Fact] + public async Task Should_ClearInternalState_WhenCleared_Async() + { + var drawingManager = CreateInitializedDrawingManager(); + var shapes = new List { new Shape(new Point()) }; + + // Add shapes to initialize and populate internal state + await drawingManager.AddShapesAsync(shapes); + var sourceShapes = GetInternalSourceShapes(drawingManager); + Assert.NotNull(sourceShapes); + Assert.Single(sourceShapes); + + // Clear should reset internal state + await drawingManager.ClearAsync(); + sourceShapes = GetInternalSourceShapes(drawingManager); + Assert.Null(sourceShapes); + + // Adding again should reinitialize state + await drawingManager.AddShapesAsync(shapes); + sourceShapes = GetInternalSourceShapes(drawingManager); + Assert.NotNull(sourceShapes); + Assert.Single(sourceShapes); + + _jsRuntimeMock.Verify(runtime => runtime.InvokeVoidAsync(Constants.JsConstants.Methods.Source.AddShapes.ToDrawingNamespace(), It.IsAny()), Times.Exactly(2)); + _jsRuntimeMock.Verify(runtime => runtime.InvokeVoidAsync(Constants.JsConstants.Methods.Source.Clear.ToDrawingNamespace()), Times.Once); + _jsRuntimeMock.VerifyNoOtherCalls(); + } + + [Fact] + public void Should_Dispose_Successfully() + { + var drawingManager = CreateInitializedDrawingManager(); + + Assert.False(drawingManager.Disposed); + + drawingManager.Dispose(); + + Assert.True(drawingManager.Disposed); + } + + [Fact] + public async Task Should_ThrowComponentNotAddedToMapException_WhenJSRuntimeIsNull_Clear_Async() + { + var drawingManager = new DrawingManager(); + + await Assert.ThrowsAsync(async () => await drawingManager.ClearAsync()); + + _jsRuntimeMock.VerifyNoOtherCalls(); + } + + [Fact] + public async Task Should_ThrowComponentNotAddedToMapException_WhenJSRuntimeIsNull_AddShapes_Async() + { + var drawingManager = new DrawingManager(); + var shapes = new List { new Shape(new Point()) }; + + await Assert.ThrowsAsync(async () => await drawingManager.AddShapesAsync(shapes)); + + _jsRuntimeMock.VerifyNoOtherCalls(); + } + + [Fact] + public void Should_ThrowComponentNotAddedToMapException_WhenJSRuntimeIsNull_Dispose() + { + var drawingManager = new DrawingManager(); + + Assert.Throws(() => drawingManager.Dispose()); + } + + [Fact] + public async Task Should_ThrowComponentDisposedException_WhenDisposed_Clear_Async() + { + var drawingManager = CreateInitializedDrawingManager(); + + drawingManager.Dispose(); + + await Assert.ThrowsAsync(async () => await drawingManager.ClearAsync()); + + _jsRuntimeMock.VerifyNoOtherCalls(); + } + + [Fact] + public async Task Should_ThrowComponentDisposedException_WhenDisposed_AddShapes_Async() + { + var drawingManager = CreateInitializedDrawingManager(); + + drawingManager.Dispose(); + var shapes = new List { new Shape(new Point()) }; + + await Assert.ThrowsAsync(async () => await drawingManager.AddShapesAsync(shapes)); + + _jsRuntimeMock.VerifyNoOtherCalls(); + } + + [Fact] + public void Should_ThrowComponentDisposedException_WhenAlreadyDisposed_Dispose() + { + var drawingManager = CreateInitializedDrawingManager(); + drawingManager.Dispose(); + + Assert.Throws(() => drawingManager.Dispose()); + } + + private DrawingManager CreateInitializedDrawingManager() + { + return new DrawingManager() { + JSRuntime = _jsRuntimeMock.Object, + Logger = _loggerMock.Object + }; + } + + /// + /// Uses reflection to access the private _sourceShapes field for testing internal state + /// + private List GetInternalSourceShapes(DrawingManager drawingManager) + { + var field = typeof(DrawingManager).GetField("_sourceShapes", + BindingFlags.NonPublic | BindingFlags.Instance); + return field?.GetValue(drawingManager) as List; + } + } +}