Files
speckle-sharp-connectors/Connectors/CSi/Speckle.Connectors.CSiShared/Bindings/CsiSharedSelectionBinding.cs
T
Björn Steinhagen f882b0f5bd fix(etabs): ensure csi operations run on main thread to prevent etabs exceptions (#589)
* fix: ensure operations run on main thread to prevent etabsexceptions

* pr comments

* fix: ensure consistent exception handler usage in selection binding
2025-02-17 11:53:21 +01:00

109 lines
3.4 KiB
C#

using Speckle.Connectors.Common.Threading;
using Speckle.Connectors.CSiShared.HostApp;
using Speckle.Connectors.CSiShared.Utils;
using Speckle.Connectors.DUI.Bindings;
using Speckle.Connectors.DUI.Bridge;
using Speckle.Converters.CSiShared.Utils;
using Timer = System.Timers.Timer;
namespace Speckle.Connectors.CSiShared.Bindings;
public class CsiSharedSelectionBinding : ISelectionBinding, IDisposable
{
private bool _disposed;
private readonly Timer _selectionTimer;
private readonly ICsiApplicationService _csiApplicationService;
private readonly IThreadContext _threadContext;
private HashSet<string> _lastSelection = new();
private readonly ITopLevelExceptionHandler _topLevelExceptionHandler;
public IBrowserBridge Parent { get; }
public string Name => "selectionBinding";
public CsiSharedSelectionBinding(
IBrowserBridge parent,
ICsiApplicationService csiApplicationService,
ITopLevelExceptionHandler topLevelExceptionHandler,
IThreadContext threadContext
)
{
_threadContext = threadContext;
Parent = parent;
_csiApplicationService = csiApplicationService;
_topLevelExceptionHandler = topLevelExceptionHandler;
_selectionTimer = new Timer(1000);
_selectionTimer.Elapsed += (_, _) =>
_topLevelExceptionHandler.CatchUnhandled(() => _threadContext.RunOnMain(CheckSelectionChanged));
_selectionTimer.Start();
}
private void CheckSelectionChanged()
{
// timer callbacks are on a background thread, but CSI API calls must be on main thread
var currentSelection = GetSelection();
var currentIds = new HashSet<string>(currentSelection.SelectedObjectIds);
if (!_lastSelection.SetEquals(currentIds))
{
_lastSelection = currentIds;
// ensure UI updates also run on main thread
_threadContext.RunOnMain(
() =>
_topLevelExceptionHandler.CatchUnhandled(
() => Parent.Send(SelectionBindingEvents.SET_SELECTION, currentSelection)
)
);
}
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
_selectionTimer?.Dispose();
}
_disposed = true;
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Gets the selection and creates an encoded ID (objectType and objectName).
/// </summary>
/// <remarks>
/// Refer to ObjectIdentifier.cs for more info.
/// </remarks>
public SelectionInfo GetSelection()
{
int numberItems = 0;
int[] objectType = [];
string[] objectName = [];
_csiApplicationService.SapModel.SelectObj.GetSelected(ref numberItems, ref objectType, ref objectName);
var encodedIds = new List<string>(numberItems);
var typeCounts = new Dictionary<string, int>();
for (int i = 0; i < numberItems; i++)
{
var typeKey = (ModelObjectType)objectType[i];
var typeName = typeKey.ToString();
encodedIds.Add(ObjectIdentifier.Encode(objectType[i], objectName[i]));
typeCounts[typeName] = (typeCounts.TryGetValue(typeName, out var count) ? count : 0) + 1; // NOTE: Cross-framework compatibility (net 48 and net8)
}
var summary =
encodedIds.Count == 0
? "No objects selected."
: $"{encodedIds.Count} objects ({string.Join(", ",
typeCounts.Select(kv => $"{kv.Value} {kv.Key}"))})";
return new SelectionInfo(encodedIds, summary);
}
}