From 0dd8f6f317997bc9d65c5750b5e5a46d6d16a2d9 Mon Sep 17 00:00:00 2001 From: Kebechet Date: Sat, 25 Jul 2026 19:30:53 +0200 Subject: [PATCH 1/2] Destroy the Sortable instance when the component is disposed SortableList never released its Sortable instance, JS module reference or DotNetObjectReference. --- BlazorSortableList.Lib/SortableList.razor | 2 ++ BlazorSortableList.Lib/SortableList.razor.cs | 27 +++++++++++++++++--- BlazorSortableList.Lib/SortableList.razor.js | 15 +++++++++++ 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/BlazorSortableList.Lib/SortableList.razor b/BlazorSortableList.Lib/SortableList.razor index f0b13a4..2bcfec2 100644 --- a/BlazorSortableList.Lib/SortableList.razor +++ b/BlazorSortableList.Lib/SortableList.razor @@ -4,6 +4,8 @@ @inject IJSRuntime JS +@implements IAsyncDisposable + @typeparam T
diff --git a/BlazorSortableList.Lib/SortableList.razor.cs b/BlazorSortableList.Lib/SortableList.razor.cs index 313991f..3e01e69 100644 --- a/BlazorSortableList.Lib/SortableList.razor.cs +++ b/BlazorSortableList.Lib/SortableList.razor.cs @@ -13,6 +13,8 @@ public partial class SortableList private DotNetObjectReference>? selfReference; + private IJSObjectReference? _module; + private string _cssForSelection; private string? _multiDragKey = string.Empty; @@ -68,7 +70,26 @@ public partial class SortableList [Parameter] public RenderFragment? SortableItemTemplate { get; set; } - public void Dispose() => selfReference?.Dispose(); + public async ValueTask DisposeAsync() + { + if (_module != null) + { + try + { + await _module.InvokeVoidAsync("destroy", Id); + await _module.DisposeAsync(); + } + catch (JSDisconnectedException) + { + // The circuit is already gone, so there is nothing left to clean up on the JS side. + } + + _module = null; + } + + selfReference?.Dispose(); + selfReference = null; + } [JSInvokable] public void OnDeselectJS(string fromId, int index) @@ -146,10 +167,10 @@ protected override async Task OnAfterRenderAsync(bool firstRender) if (firstRender) { selfReference = DotNetObjectReference.Create(this); - var module = await JS.InvokeAsync("import", "./_content/BlazorSortableList/SortableList.razor.js"); + _module = await JS.InvokeAsync("import", "./_content/BlazorSortableList/SortableList.razor.js"); //await JS.InvokeVoidAsync("console.log", $"***id:{Id}, group:{Group},pull: {Pull},put:{Put},sort:{Sort}, handle:{Handle}, filter:{Filter}, forceFallback:{ForceFallback}"); - await module.InvokeAsync( + await _module.InvokeAsync( "init", Id, Group, diff --git a/BlazorSortableList.Lib/SortableList.razor.js b/BlazorSortableList.Lib/SortableList.razor.js index eb13009..f5271fb 100644 --- a/BlazorSortableList.Lib/SortableList.razor.js +++ b/BlazorSortableList.Lib/SortableList.razor.js @@ -1,3 +1,13 @@ +const instances = new Map(); + +export function destroy(id) { + const sortable = instances.get(id); + if (sortable) { + sortable.destroy(); + instances.delete(id); + } +} + export function init(id, group, pull, put, sort, handle, filter, component, forceFallback, cssForSelection, multiDragKey, avoidImplicitDeselect, fallbackOnBody, swapThreshold) { const DEBUG_MODE = true; @@ -51,6 +61,9 @@ export function init(id, group, pull, put, sort, handle, filter, component, forc // } // }); + // A component re-initialising under an id we already track would otherwise orphan the previous instance. + destroy(id); + var sortable = new SortableCtor(htmlElement, { animation: 200, group: { @@ -187,4 +200,6 @@ export function init(id, group, pull, put, sort, handle, filter, component, forc } } }); + + instances.set(id, sortable); } From f0a5d69310bafa18f4e0d6b245a13ec0bf44344f Mon Sep 17 00:00:00 2001 From: Kebechet Date: Sat, 25 Jul 2026 19:38:54 +0200 Subject: [PATCH 2/2] Address review: guarantee managed cleanup, destroy the initialized id --- BlazorSortableList.Lib/SortableList.razor.cs | 49 ++++++++++++++------ 1 file changed, 35 insertions(+), 14 deletions(-) diff --git a/BlazorSortableList.Lib/SortableList.razor.cs b/BlazorSortableList.Lib/SortableList.razor.cs index 3e01e69..a466504 100644 --- a/BlazorSortableList.Lib/SortableList.razor.cs +++ b/BlazorSortableList.Lib/SortableList.razor.cs @@ -15,6 +15,12 @@ public partial class SortableList private IJSObjectReference? _module; + /// + /// The id the JS instance was registered under. is a mutable parameter, so it + /// cannot be trusted to still hold the key the instance is stored against by disposal time. + /// + private string? _initializedId; + private string _cssForSelection; private string? _multiDragKey = string.Empty; @@ -72,23 +78,37 @@ public partial class SortableList public async ValueTask DisposeAsync() { - if (_module != null) + try { - try - { - await _module.InvokeVoidAsync("destroy", Id); - await _module.DisposeAsync(); - } - catch (JSDisconnectedException) + if (_module != null) { - // The circuit is already gone, so there is nothing left to clean up on the JS side. + try + { + if (_initializedId != null) + { + await _module.InvokeVoidAsync("destroy", _initializedId); + } + } + finally + { + await _module.DisposeAsync(); + } } - + } + catch (JSDisconnectedException) + { + // The circuit is already gone, so there is nothing left to clean up on the JS side. + } + catch (JSException) + { + // A failing teardown on the JS side must not stop the managed references from being released. + } + finally + { _module = null; + selfReference?.Dispose(); + selfReference = null; } - - selfReference?.Dispose(); - selfReference = null; } [JSInvokable] @@ -170,9 +190,10 @@ protected override async Task OnAfterRenderAsync(bool firstRender) _module = await JS.InvokeAsync("import", "./_content/BlazorSortableList/SortableList.razor.js"); //await JS.InvokeVoidAsync("console.log", $"***id:{Id}, group:{Group},pull: {Pull},put:{Put},sort:{Sort}, handle:{Handle}, filter:{Filter}, forceFallback:{ForceFallback}"); - await _module.InvokeAsync( + _initializedId = Id; + await _module.InvokeVoidAsync( "init", - Id, + _initializedId, Group, Pull, Put,