diff --git a/Connectors/ArcGIS/Speckle.Connectors.ArcGIS3/Bindings/ArcGISSelectionBinding.cs b/Connectors/ArcGIS/Speckle.Connectors.ArcGIS3/Bindings/ArcGISSelectionBinding.cs index d243d12b7..180156939 100644 --- a/Connectors/ArcGIS/Speckle.Connectors.ArcGIS3/Bindings/ArcGISSelectionBinding.cs +++ b/Connectors/ArcGIS/Speckle.Connectors.ArcGIS3/Bindings/ArcGISSelectionBinding.cs @@ -12,15 +12,18 @@ public class ArcGISSelectionBinding : ISelectionBinding public string Name => "selectionBinding"; public IBrowserBridge Parent { get; } - public ArcGISSelectionBinding(IBrowserBridge parent, MapMembersUtils mapMemberUtils) + public ArcGISSelectionBinding( + IBrowserBridge parent, + MapMembersUtils mapMemberUtils, + ITopLevelExceptionHandler topLevelExceptionHandler + ) { _mapMemberUtils = mapMemberUtils; Parent = parent; - var topLevelHandler = parent.TopLevelExceptionHandler; // example: https://github.com/Esri/arcgis-pro-sdk-community-samples/blob/master/Map-Authoring/QueryBuilderControl/DefinitionQueryDockPaneViewModel.cs // MapViewEventArgs args = new(MapView.Active); - TOCSelectionChangedEvent.Subscribe(_ => topLevelHandler.CatchUnhandled(OnSelectionChanged), true); + TOCSelectionChangedEvent.Subscribe(_ => topLevelExceptionHandler.CatchUnhandled(OnSelectionChanged), true); } private void OnSelectionChanged() diff --git a/Connectors/ArcGIS/Speckle.Connectors.ArcGIS3/Bindings/ArcGISSendBinding.cs b/Connectors/ArcGIS/Speckle.Connectors.ArcGIS3/Bindings/ArcGISSendBinding.cs index 3114b7c64..ea887b66f 100644 --- a/Connectors/ArcGIS/Speckle.Connectors.ArcGIS3/Bindings/ArcGISSendBinding.cs +++ b/Connectors/ArcGIS/Speckle.Connectors.ArcGIS3/Bindings/ArcGISSendBinding.cs @@ -15,6 +15,7 @@ using Speckle.Connectors.Common.Threading; using Speckle.Connectors.DUI.Bindings; using Speckle.Connectors.DUI.Bridge; +using Speckle.Connectors.DUI.Eventing; using Speckle.Connectors.DUI.Exceptions; using Speckle.Connectors.DUI.Logging; using Speckle.Connectors.DUI.Models; @@ -69,7 +70,9 @@ public ArcGISSendBinding( ILogger logger, IArcGISConversionSettingsFactory arcGisConversionSettingsFactory, MapMembersUtils mapMemberUtils, - IThreadContext threadContext + IThreadContext threadContext, + IEventAggregator eventAggregator, + ITopLevelExceptionHandler topLevelExceptionHandler ) { _store = store; @@ -79,7 +82,7 @@ IThreadContext threadContext _sendConversionCache = sendConversionCache; _operationProgressManager = operationProgressManager; _logger = logger; - _topLevelExceptionHandler = parent.TopLevelExceptionHandler; + _topLevelExceptionHandler = topLevelExceptionHandler; _arcGISConversionSettingsFactory = arcGisConversionSettingsFactory; _mapMemberUtils = mapMemberUtils; _threadContext = threadContext; @@ -87,10 +90,12 @@ IThreadContext threadContext Parent = parent; Commands = new SendBindingUICommands(parent); SubscribeToArcGISEvents(); - _store.DocumentChanged += (_, _) => - { - _sendConversionCache.ClearCache(); - }; + eventAggregator + .GetEvent() + .Subscribe(_ => + { + _sendConversionCache.ClearCache(); + }); } private void SubscribeToArcGISEvents() @@ -201,7 +206,7 @@ private void SubscribeToAnyDataSourceChange(Table layerTable) { RowCreatedEvent.Subscribe( (args) => - Parent.TopLevelExceptionHandler.FireAndForget(async () => + _topLevelExceptionHandler.FireAndForget(async () => { await OnRowChanged(args); }), @@ -209,7 +214,7 @@ private void SubscribeToAnyDataSourceChange(Table layerTable) ); RowChangedEvent.Subscribe( (args) => - Parent.TopLevelExceptionHandler.FireAndForget(async () => + _topLevelExceptionHandler.FireAndForget(async () => { await OnRowChanged(args); }), @@ -217,7 +222,7 @@ private void SubscribeToAnyDataSourceChange(Table layerTable) ); RowDeletedEvent.Subscribe( (args) => - Parent.TopLevelExceptionHandler.FireAndForget(async () => + _topLevelExceptionHandler.FireAndForget(async () => { await OnRowChanged(args); }), diff --git a/Connectors/ArcGIS/Speckle.Connectors.ArcGIS3/Bindings/BasicConnectorBinding.cs b/Connectors/ArcGIS/Speckle.Connectors.ArcGIS3/Bindings/BasicConnectorBinding.cs index d3881899e..49b51a384 100644 --- a/Connectors/ArcGIS/Speckle.Connectors.ArcGIS3/Bindings/BasicConnectorBinding.cs +++ b/Connectors/ArcGIS/Speckle.Connectors.ArcGIS3/Bindings/BasicConnectorBinding.cs @@ -3,6 +3,7 @@ using Speckle.Connectors.ArcGIS.Utils; using Speckle.Connectors.DUI.Bindings; using Speckle.Connectors.DUI.Bridge; +using Speckle.Connectors.DUI.Eventing; using Speckle.Connectors.DUI.Models; using Speckle.Connectors.DUI.Models.Card; using Speckle.Sdk; @@ -21,18 +22,21 @@ public class BasicConnectorBinding : IBasicConnectorBinding private readonly DocumentModelStore _store; private readonly ISpeckleApplication _speckleApplication; - public BasicConnectorBinding(DocumentModelStore store, IBrowserBridge parent, ISpeckleApplication speckleApplication) + public BasicConnectorBinding( + DocumentModelStore store, + IBrowserBridge parent, + ISpeckleApplication speckleApplication, + IEventAggregator eventAggregator + ) { _store = store; _speckleApplication = speckleApplication; Parent = parent; Commands = new BasicConnectorBindingCommands(parent); - _store.DocumentChanged += (_, _) => - parent.TopLevelExceptionHandler.FireAndForget(async () => - { - await Commands.NotifyDocumentChanged(); - }); + eventAggregator + .GetEvent() + .Subscribe(async _ => await Commands.NotifyDocumentChanged().ConfigureAwait(false)); } public string GetSourceApplicationName() => _speckleApplication.Slug; diff --git a/Connectors/ArcGIS/Speckle.Connectors.ArcGIS3/DependencyInjection/ArcGISConnectorModule.cs b/Connectors/ArcGIS/Speckle.Connectors.ArcGIS3/DependencyInjection/ArcGISConnectorModule.cs index 301c13272..08ea8dbd9 100644 --- a/Connectors/ArcGIS/Speckle.Connectors.ArcGIS3/DependencyInjection/ArcGISConnectorModule.cs +++ b/Connectors/ArcGIS/Speckle.Connectors.ArcGIS3/DependencyInjection/ArcGISConnectorModule.cs @@ -37,7 +37,6 @@ public static void AddArcGIS(this IServiceCollection serviceCollection) serviceCollection.AddSingleton(sp => sp.GetRequiredService()); serviceCollection.AddSingleton(); - serviceCollection.RegisterTopLevelExceptionHandler(); serviceCollection.AddSingleton(DefaultTraversal.CreateTraversalFunc()); // register send operation and dependencies diff --git a/Connectors/ArcGIS/Speckle.Connectors.ArcGIS3/SpeckleModule.cs b/Connectors/ArcGIS/Speckle.Connectors.ArcGIS3/SpeckleModule.cs index 1d5eee795..64370fdf2 100644 --- a/Connectors/ArcGIS/Speckle.Connectors.ArcGIS3/SpeckleModule.cs +++ b/Connectors/ArcGIS/Speckle.Connectors.ArcGIS3/SpeckleModule.cs @@ -2,6 +2,7 @@ using Microsoft.Extensions.DependencyInjection; using Speckle.Connectors.ArcGIS.DependencyInjection; using Speckle.Connectors.Common; +using Speckle.Connectors.DUI; using Speckle.Converters.ArcGIS3; using Speckle.Sdk.Host; using Module = ArcGIS.Desktop.Framework.Contracts.Module; @@ -34,6 +35,7 @@ public SpeckleModule() services.AddArcGIS(); services.AddArcGISConverters(); Container = services.BuildServiceProvider(); + Container.UseDUI(); } private HostAppVersion GetVersion() diff --git a/Connectors/ArcGIS/Speckle.Connectors.ArcGIS3/Utils/ArcGISThreadContext.cs b/Connectors/ArcGIS/Speckle.Connectors.ArcGIS3/Utils/ArcGISThreadContext.cs index 42297eb00..8e4e2e239 100644 --- a/Connectors/ArcGIS/Speckle.Connectors.ArcGIS3/Utils/ArcGISThreadContext.cs +++ b/Connectors/ArcGIS/Speckle.Connectors.ArcGIS3/Utils/ArcGISThreadContext.cs @@ -1,4 +1,4 @@ -using ArcGIS.Desktop.Framework.Threading.Tasks; +using ArcGIS.Desktop.Framework.Threading.Tasks; using Speckle.Connectors.Common.Threading; namespace Speckle.Connectors.ArcGIS.Utils; diff --git a/Connectors/ArcGIS/Speckle.Connectors.ArcGIS3/Utils/ArcGisDocumentStore.cs b/Connectors/ArcGIS/Speckle.Connectors.ArcGIS3/Utils/ArcGisDocumentStore.cs index 0e6e5e1da..e64953be5 100644 --- a/Connectors/ArcGIS/Speckle.Connectors.ArcGIS3/Utils/ArcGisDocumentStore.cs +++ b/Connectors/ArcGIS/Speckle.Connectors.ArcGIS3/Utils/ArcGisDocumentStore.cs @@ -4,6 +4,7 @@ using ArcGIS.Desktop.Mapping.Events; using Speckle.Connectors.Common.Threading; using Speckle.Connectors.DUI.Bridge; +using Speckle.Connectors.DUI.Eventing; using Speckle.Connectors.DUI.Models; using Speckle.Connectors.DUI.Utils; @@ -12,15 +13,18 @@ namespace Speckle.Connectors.ArcGIS.Utils; public class ArcGISDocumentStore : DocumentModelStore { private readonly IThreadContext _threadContext; + private readonly IEventAggregator _eventAggregator; public ArcGISDocumentStore( IJsonSerializer jsonSerializer, ITopLevelExceptionHandler topLevelExceptionHandler, - IThreadContext threadContext + IThreadContext threadContext, + IEventAggregator eventAggregator ) : base(jsonSerializer) { _threadContext = threadContext; + _eventAggregator = eventAggregator; ActiveMapViewChangedEvent.Subscribe(a => topLevelExceptionHandler.CatchUnhandled(() => OnMapViewChanged(a)), true); ProjectSavingEvent.Subscribe( _ => @@ -38,13 +42,16 @@ IThreadContext threadContext }, true ); + } + public override async Task OnDocumentStoreInitialized() + { // in case plugin was loaded into already opened Map, read metadata from the current Map if (!IsDocumentInit && MapView.Active != null) { IsDocumentInit = true; LoadState(); - OnDocumentChanged(); + await _eventAggregator.GetEvent().PublishAsync(new object()); } } @@ -69,7 +76,7 @@ private void OnProjectSaving() /// /// On map view switch, this event trigger twice, first for outgoing view, second for incoming view. /// - private void OnMapViewChanged(ActiveMapViewChangedEventArgs args) + private async void OnMapViewChanged(ActiveMapViewChangedEventArgs args) { if (args.IncomingView is null) { @@ -78,7 +85,7 @@ private void OnMapViewChanged(ActiveMapViewChangedEventArgs args) IsDocumentInit = true; LoadState(); - OnDocumentChanged(); + await _eventAggregator.GetEvent().PublishAsync(new object()); } protected override void HostAppSaveState(string modelCardState) => diff --git a/Connectors/Autocad/Speckle.Connectors.AutocadShared/Bindings/AutocadBasicConnectorBinding.cs b/Connectors/Autocad/Speckle.Connectors.AutocadShared/Bindings/AutocadBasicConnectorBinding.cs index e5f8116f8..8053a6e60 100644 --- a/Connectors/Autocad/Speckle.Connectors.AutocadShared/Bindings/AutocadBasicConnectorBinding.cs +++ b/Connectors/Autocad/Speckle.Connectors.AutocadShared/Bindings/AutocadBasicConnectorBinding.cs @@ -4,6 +4,7 @@ using Speckle.Connectors.Common.Threading; using Speckle.Connectors.DUI.Bindings; using Speckle.Connectors.DUI.Bridge; +using Speckle.Connectors.DUI.Eventing; using Speckle.Connectors.DUI.Models; using Speckle.Connectors.DUI.Models.Card; using Speckle.Sdk; @@ -31,6 +32,7 @@ public AutocadBasicConnectorBinding( IAccountManager accountManager, ISpeckleApplication speckleApplication, ILogger logger, + IEventAggregator eventAggregator, IThreadContext threadContext ) { @@ -39,8 +41,9 @@ IThreadContext threadContext _accountManager = accountManager; _speckleApplication = speckleApplication; Commands = new BasicConnectorBindingCommands(parent); - _store.DocumentChanged += (_, _) => - parent.TopLevelExceptionHandler.FireAndForget(async () => + eventAggregator + .GetEvent() + .Subscribe(async _ => { await Commands.NotifyDocumentChanged(); }); diff --git a/Connectors/Autocad/Speckle.Connectors.AutocadShared/Bindings/AutocadSelectionBinding.cs b/Connectors/Autocad/Speckle.Connectors.AutocadShared/Bindings/AutocadSelectionBinding.cs index 104341c33..6d3d5e7d7 100644 --- a/Connectors/Autocad/Speckle.Connectors.AutocadShared/Bindings/AutocadSelectionBinding.cs +++ b/Connectors/Autocad/Speckle.Connectors.AutocadShared/Bindings/AutocadSelectionBinding.cs @@ -18,9 +18,13 @@ public class AutocadSelectionBinding : ISelectionBinding public IBrowserBridge Parent { get; } - public AutocadSelectionBinding(IBrowserBridge parent, IThreadContext threadContext) + public AutocadSelectionBinding( + IBrowserBridge parent, + IThreadContext threadContext, + ITopLevelExceptionHandler topLevelExceptionHandler + ) { - _topLevelExceptionHandler = parent.TopLevelExceptionHandler; + _topLevelExceptionHandler = topLevelExceptionHandler; Parent = parent; _threadContext = threadContext; diff --git a/Connectors/Autocad/Speckle.Connectors.AutocadShared/Bindings/AutocadSendBaseBinding.cs b/Connectors/Autocad/Speckle.Connectors.AutocadShared/Bindings/AutocadSendBaseBinding.cs index 5f556e623..7c3a7e576 100644 --- a/Connectors/Autocad/Speckle.Connectors.AutocadShared/Bindings/AutocadSendBaseBinding.cs +++ b/Connectors/Autocad/Speckle.Connectors.AutocadShared/Bindings/AutocadSendBaseBinding.cs @@ -1,4 +1,4 @@ -using System.Collections.Concurrent; +using System.Collections.Concurrent; using Autodesk.AutoCAD.DatabaseServices; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -11,6 +11,7 @@ using Speckle.Connectors.Common.Threading; using Speckle.Connectors.DUI.Bindings; using Speckle.Connectors.DUI.Bridge; +using Speckle.Connectors.DUI.Eventing; using Speckle.Connectors.DUI.Exceptions; using Speckle.Connectors.DUI.Logging; using Speckle.Connectors.DUI.Models; @@ -60,7 +61,9 @@ protected AutocadSendBaseBinding( IOperationProgressManager operationProgressManager, ILogger logger, ISpeckleApplication speckleApplication, - IThreadContext threadContext + ITopLevelExceptionHandler topLevelExceptionHandler, + IThreadContext threadContext, + IEventAggregator eventAggregator ) { _store = store; @@ -73,7 +76,7 @@ IThreadContext threadContext _logger = logger; _speckleApplication = speckleApplication; _threadContext = threadContext; - _topLevelExceptionHandler = parent.TopLevelExceptionHandler; + _topLevelExceptionHandler = topLevelExceptionHandler; Parent = parent; Commands = new SendBindingUICommands(parent); @@ -86,10 +89,8 @@ IThreadContext threadContext SubscribeToObjectChanges(Application.DocumentManager.CurrentDocument); } // Since ids of the objects generates from same seed, we should clear the cache always whenever doc swapped. - _store.DocumentChanged += (_, _) => - { - _sendConversionCache.ClearCache(); - }; + + eventAggregator.GetEvent().Subscribe(_ => _sendConversionCache.ClearCache()); } private readonly List _docSubsTracker = new(); diff --git a/Connectors/Autocad/Speckle.Connectors.AutocadShared/Bindings/AutocadSendBinding.cs b/Connectors/Autocad/Speckle.Connectors.AutocadShared/Bindings/AutocadSendBinding.cs index ddd5c223c..e222636c6 100644 --- a/Connectors/Autocad/Speckle.Connectors.AutocadShared/Bindings/AutocadSendBinding.cs +++ b/Connectors/Autocad/Speckle.Connectors.AutocadShared/Bindings/AutocadSendBinding.cs @@ -6,6 +6,7 @@ using Speckle.Connectors.Common.Threading; using Speckle.Connectors.DUI.Bindings; using Speckle.Connectors.DUI.Bridge; +using Speckle.Connectors.DUI.Eventing; using Speckle.Connectors.DUI.Models; using Speckle.Connectors.DUI.Models.Card.SendFilter; using Speckle.Converters.Autocad; @@ -30,7 +31,9 @@ public AutocadSendBinding( ILogger logger, IAutocadConversionSettingsFactory autocadConversionSettingsFactory, ISpeckleApplication speckleApplication, - IThreadContext threadContext + ITopLevelExceptionHandler topLevelExceptionHandler, + IThreadContext threadContext, + IEventAggregator eventAggregator ) : base( store, @@ -43,7 +46,9 @@ IThreadContext threadContext operationProgressManager, logger, speckleApplication, - threadContext + topLevelExceptionHandler, + threadContext, + eventAggregator ) { _autocadConversionSettingsFactory = autocadConversionSettingsFactory; diff --git a/Connectors/Autocad/Speckle.Connectors.AutocadShared/DependencyInjection/SharedRegistration.cs b/Connectors/Autocad/Speckle.Connectors.AutocadShared/DependencyInjection/SharedRegistration.cs index 8ce12c190..6e2db8da2 100644 --- a/Connectors/Autocad/Speckle.Connectors.AutocadShared/DependencyInjection/SharedRegistration.cs +++ b/Connectors/Autocad/Speckle.Connectors.AutocadShared/DependencyInjection/SharedRegistration.cs @@ -61,8 +61,6 @@ public static void AddAutocadBase(this IServiceCollection serviceCollection) serviceCollection.AddSingleton(sp => sp.GetRequiredService()); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); - - serviceCollection.RegisterTopLevelExceptionHandler(); } public static void LoadSend(this IServiceCollection serviceCollection) diff --git a/Connectors/Autocad/Speckle.Connectors.AutocadShared/HostApp/AutocadDocumentModelStore.cs b/Connectors/Autocad/Speckle.Connectors.AutocadShared/HostApp/AutocadDocumentModelStore.cs index c8319d638..899954633 100644 --- a/Connectors/Autocad/Speckle.Connectors.AutocadShared/HostApp/AutocadDocumentModelStore.cs +++ b/Connectors/Autocad/Speckle.Connectors.AutocadShared/HostApp/AutocadDocumentModelStore.cs @@ -1,4 +1,5 @@ using Speckle.Connectors.DUI.Bridge; +using Speckle.Connectors.DUI.Eventing; using Speckle.Connectors.DUI.Models; using Speckle.Connectors.DUI.Utils; @@ -9,15 +10,18 @@ public class AutocadDocumentStore : DocumentModelStore private readonly string _nullDocumentName = "Null Doc"; private string _previousDocName; private readonly AutocadDocumentManager _autocadDocumentManager; + private readonly IEventAggregator _eventAggregator; public AutocadDocumentStore( IJsonSerializer jsonSerializer, AutocadDocumentManager autocadDocumentManager, - ITopLevelExceptionHandler topLevelExceptionHandler + ITopLevelExceptionHandler topLevelExceptionHandler, + IEventAggregator eventAggregator ) : base(jsonSerializer) { _autocadDocumentManager = autocadDocumentManager; + _eventAggregator = eventAggregator; _previousDocName = _nullDocumentName; // POC: Will be addressed to move it into AutocadContext! @@ -38,7 +42,7 @@ ITopLevelExceptionHandler topLevelExceptionHandler // OnDocChangeInternal((Document)args.DocumentWindow.Document); } - private void OnDocChangeInternal(Document? doc) + private async void OnDocChangeInternal(Document? doc) { var currentDocName = doc != null ? doc.Name : _nullDocumentName; if (_previousDocName == currentDocName) @@ -48,7 +52,7 @@ private void OnDocChangeInternal(Document? doc) _previousDocName = currentDocName; LoadState(); - OnDocumentChanged(); + await _eventAggregator.GetEvent().PublishAsync(new object()); } protected override void LoadState() diff --git a/Connectors/Autocad/Speckle.Connectors.AutocadShared/Plugin/AutocadCommand.cs b/Connectors/Autocad/Speckle.Connectors.AutocadShared/Plugin/AutocadCommand.cs index cbc69d098..f3c177540 100644 --- a/Connectors/Autocad/Speckle.Connectors.AutocadShared/Plugin/AutocadCommand.cs +++ b/Connectors/Autocad/Speckle.Connectors.AutocadShared/Plugin/AutocadCommand.cs @@ -3,6 +3,7 @@ using Autodesk.AutoCAD.Windows; using Microsoft.Extensions.DependencyInjection; using Speckle.Connectors.Common; +using Speckle.Connectors.DUI; using Speckle.Connectors.DUI.WebView; #if AUTOCAD using Speckle.Connectors.Autocad.DependencyInjection; @@ -47,6 +48,7 @@ public void Command() services.AddCivil3dConverters(); #endif Container = services.BuildServiceProvider(); + Container.UseDUI(); var panelWebView = Container.GetRequiredService(); diff --git a/Connectors/Autocad/Speckle.Connectors.Civil3dShared/Bindings/Civil3dSendBinding.cs b/Connectors/Autocad/Speckle.Connectors.Civil3dShared/Bindings/Civil3dSendBinding.cs index 4b26c74ef..3ff8e014e 100644 --- a/Connectors/Autocad/Speckle.Connectors.Civil3dShared/Bindings/Civil3dSendBinding.cs +++ b/Connectors/Autocad/Speckle.Connectors.Civil3dShared/Bindings/Civil3dSendBinding.cs @@ -7,6 +7,7 @@ using Speckle.Connectors.Common.Threading; using Speckle.Connectors.DUI.Bindings; using Speckle.Connectors.DUI.Bridge; +using Speckle.Connectors.DUI.Eventing; using Speckle.Connectors.DUI.Models; using Speckle.Connectors.DUI.Models.Card.SendFilter; using Speckle.Converters.Autocad; @@ -34,7 +35,9 @@ public Civil3dSendBinding( ICivil3dConversionSettingsFactory civil3dConversionSettingsFactory, IAutocadConversionSettingsFactory autocadConversionSettingsFactory, ISpeckleApplication speckleApplication, - IThreadContext threadContext + ITopLevelExceptionHandler topLevelExceptionHandler, + IThreadContext threadContext, + IEventAggregator eventAggregator ) : base( store, @@ -47,7 +50,9 @@ IThreadContext threadContext operationProgressManager, logger, speckleApplication, - threadContext + topLevelExceptionHandler, + threadContext, + eventAggregator ) { _civil3dConversionSettingsFactory = civil3dConversionSettingsFactory; diff --git a/Connectors/CSi/Speckle.Connectors.CSiShared/Plugin/SpeckleFormBase.cs b/Connectors/CSi/Speckle.Connectors.CSiShared/Plugin/SpeckleFormBase.cs index a3a81cad1..2cf925a82 100644 --- a/Connectors/CSi/Speckle.Connectors.CSiShared/Plugin/SpeckleFormBase.cs +++ b/Connectors/CSi/Speckle.Connectors.CSiShared/Plugin/SpeckleFormBase.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.DependencyInjection; using Speckle.Connectors.Common; using Speckle.Connectors.CSiShared.HostApp; +using Speckle.Connectors.DUI; using Speckle.Connectors.DUI.WebView; using Speckle.Converters.CSiShared; using Speckle.Sdk.Host; @@ -25,6 +26,7 @@ protected SpeckleFormBase() ConfigureServices(services); Container = services.BuildServiceProvider(); + Container.UseDUI(); var webview = Container.GetRequiredService(); Host = new() { Child = webview, Dock = DockStyle.Fill }; diff --git a/Connectors/CSi/Speckle.Connectors.CSiShared/ServiceRegistration.cs b/Connectors/CSi/Speckle.Connectors.CSiShared/ServiceRegistration.cs index 59945da1e..a74afb7d8 100644 --- a/Connectors/CSi/Speckle.Connectors.CSiShared/ServiceRegistration.cs +++ b/Connectors/CSi/Speckle.Connectors.CSiShared/ServiceRegistration.cs @@ -43,8 +43,6 @@ public static IServiceCollection AddCsi(this IServiceCollection services) services.AddScoped, CsiRootObjectBuilder>(); services.AddScoped>(); - services.RegisterTopLevelExceptionHandler(); - return services; } } diff --git a/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/DependencyInjection/NavisworksConnectorServiceRegistration.cs b/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/DependencyInjection/NavisworksConnectorServiceRegistration.cs index b953302de..3dc5c7cbe 100644 --- a/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/DependencyInjection/NavisworksConnectorServiceRegistration.cs +++ b/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/DependencyInjection/NavisworksConnectorServiceRegistration.cs @@ -60,7 +60,6 @@ public static void AddNavisworks(this IServiceCollection serviceCollection) serviceCollection.AddSingleton(); // Register Intercom/interop - serviceCollection.RegisterTopLevelExceptionHandler(); serviceCollection.AddTransient(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); diff --git a/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Plugin/DockableConnectorPane.cs b/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Plugin/DockableConnectorPane.cs index a06842db6..9c4240c81 100644 --- a/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Plugin/DockableConnectorPane.cs +++ b/Connectors/Navisworks/Speckle.Connectors.NavisworksShared/Plugin/DockableConnectorPane.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.DependencyInjection; using Speckle.Connector.Navisworks.DependencyInjection; using Speckle.Connectors.Common; +using Speckle.Connectors.DUI; using Speckle.Connectors.DUI.WebView; using Speckle.Converter.Navisworks.DependencyInjection; using Speckle.Sdk.Host; @@ -42,6 +43,7 @@ public override Control CreateControlPane() services.AddNavisworksConverter(); Container = services.BuildServiceProvider(); + Container.UseDUI(); var u = Container.GetRequiredService(); diff --git a/Connectors/Revit/Speckle.Connectors.RevitShared/Bindings/BasicConnectorBindingRevit.cs b/Connectors/Revit/Speckle.Connectors.RevitShared/Bindings/BasicConnectorBindingRevit.cs index c4ce8e66c..9f8d681e9 100644 --- a/Connectors/Revit/Speckle.Connectors.RevitShared/Bindings/BasicConnectorBindingRevit.cs +++ b/Connectors/Revit/Speckle.Connectors.RevitShared/Bindings/BasicConnectorBindingRevit.cs @@ -1,5 +1,6 @@ using Autodesk.Revit.DB; using Speckle.Connectors.DUI.Bridge; +using Speckle.Connectors.DUI.Eventing; using Speckle.Connectors.DUI.Models; using Speckle.Connectors.DUI.Models.Card; using Speckle.Connectors.RevitShared; @@ -26,7 +27,8 @@ public BasicConnectorBindingRevit( DocumentModelStore store, IBrowserBridge parent, RevitContext revitContext, - ISpeckleApplication speckleApplication + ISpeckleApplication speckleApplication, + IEventAggregator eventAggregator ) { Name = "baseBinding"; @@ -37,8 +39,9 @@ ISpeckleApplication speckleApplication Commands = new BasicConnectorBindingCommands(parent); // POC: event binding? - _store.DocumentChanged += (_, _) => - parent.TopLevelExceptionHandler.FireAndForget(async () => + eventAggregator + .GetEvent() + .Subscribe(async _ => { await Commands.NotifyDocumentChanged(); }); diff --git a/Connectors/Revit/Speckle.Connectors.RevitShared/Bindings/RevitSendBinding.cs b/Connectors/Revit/Speckle.Connectors.RevitShared/Bindings/RevitSendBinding.cs index 27cb51e56..17b4901e3 100644 --- a/Connectors/Revit/Speckle.Connectors.RevitShared/Bindings/RevitSendBinding.cs +++ b/Connectors/Revit/Speckle.Connectors.RevitShared/Bindings/RevitSendBinding.cs @@ -8,6 +8,7 @@ using Speckle.Connectors.Common.Operations; using Speckle.Connectors.DUI.Bindings; using Speckle.Connectors.DUI.Bridge; +using Speckle.Connectors.DUI.Eventing; using Speckle.Connectors.DUI.Exceptions; using Speckle.Connectors.DUI.Logging; using Speckle.Connectors.DUI.Models; @@ -59,7 +60,9 @@ public RevitSendBinding( ILogger logger, ElementUnpacker elementUnpacker, IRevitConversionSettingsFactory revitConversionSettingsFactory, - ISpeckleApplication speckleApplication + ISpeckleApplication speckleApplication, + IEventAggregator eventAggregator, + ITopLevelExceptionHandler topLevelExceptionHandler ) : base("sendBinding", store, bridge, revitContext) { @@ -73,7 +76,6 @@ ISpeckleApplication speckleApplication _elementUnpacker = elementUnpacker; _revitConversionSettingsFactory = revitConversionSettingsFactory; _speckleApplication = speckleApplication; - var topLevelExceptionHandler = Parent.TopLevelExceptionHandler; Commands = new SendBindingUICommands(bridge); // TODO expiry events @@ -81,7 +83,12 @@ ISpeckleApplication speckleApplication revitContext.UIApplication.NotNull().Application.DocumentChanged += (_, e) => topLevelExceptionHandler.CatchUnhandled(() => DocChangeHandler(e)); - Store.DocumentChanged += (_, _) => topLevelExceptionHandler.FireAndForget(async () => await OnDocumentChanged()); + eventAggregator + .GetEvent() + .Subscribe(async _ => + { + await OnDocumentChanged().ConfigureAwait(false); + }); } public List GetSendFilters() => diff --git a/Connectors/Revit/Speckle.Connectors.RevitShared/Bindings/SelectionBinding.cs b/Connectors/Revit/Speckle.Connectors.RevitShared/Bindings/SelectionBinding.cs index eabb03f7f..a5903928f 100644 --- a/Connectors/Revit/Speckle.Connectors.RevitShared/Bindings/SelectionBinding.cs +++ b/Connectors/Revit/Speckle.Connectors.RevitShared/Bindings/SelectionBinding.cs @@ -17,6 +17,7 @@ public SelectionBinding( RevitContext revitContext, DocumentModelStore store, IAppIdleManager revitIdleManager, + ITopLevelExceptionHandler topLevelExceptionHandler, IBrowserBridge parent ) : base("selectionBinding", store, parent, revitContext) @@ -24,7 +25,7 @@ IBrowserBridge parent #if REVIT2022 // NOTE: getting the selection data should be a fast function all, even for '000s of elements - and having a timer hitting it every 1s is ok. _selectionTimer = new System.Timers.Timer(1000); - _selectionTimer.Elapsed += (_, _) => parent.TopLevelExceptionHandler.CatchUnhandled(OnSelectionChanged); + _selectionTimer.Elapsed += (_, _) => topLevelExceptionHandler.CatchUnhandled(OnSelectionChanged); _selectionTimer.Start(); #else diff --git a/Connectors/Revit/Speckle.Connectors.RevitShared/DependencyInjection/RevitConnectorModule.cs b/Connectors/Revit/Speckle.Connectors.RevitShared/DependencyInjection/RevitConnectorModule.cs index b719cc2d4..59dc9da5f 100644 --- a/Connectors/Revit/Speckle.Connectors.RevitShared/DependencyInjection/RevitConnectorModule.cs +++ b/Connectors/Revit/Speckle.Connectors.RevitShared/DependencyInjection/RevitConnectorModule.cs @@ -43,8 +43,6 @@ public static void AddRevit(this IServiceCollection serviceCollection) serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); - serviceCollection.RegisterTopLevelExceptionHandler(); - serviceCollection.AddSingleton(sp => sp.GetRequiredService()); serviceCollection.AddSingleton(); diff --git a/Connectors/Revit/Speckle.Connectors.RevitShared/HostApp/RevitDocumentStore.cs b/Connectors/Revit/Speckle.Connectors.RevitShared/HostApp/RevitDocumentStore.cs index 5b8629853..f1b8d5e89 100644 --- a/Connectors/Revit/Speckle.Connectors.RevitShared/HostApp/RevitDocumentStore.cs +++ b/Connectors/Revit/Speckle.Connectors.RevitShared/HostApp/RevitDocumentStore.cs @@ -3,6 +3,7 @@ using Autodesk.Revit.UI; using Autodesk.Revit.UI.Events; using Speckle.Connectors.DUI.Bridge; +using Speckle.Connectors.DUI.Eventing; using Speckle.Connectors.DUI.Models; using Speckle.Connectors.DUI.Utils; using Speckle.Converters.RevitShared.Helpers; @@ -20,6 +21,7 @@ internal sealed class RevitDocumentStore : DocumentModelStore private readonly IAppIdleManager _idleManager; private readonly DocumentModelStorageSchema _documentModelStorageSchema; private readonly IdStorageSchema _idStorageSchema; + private readonly IEventAggregator _eventAggregator; public RevitDocumentStore( IAppIdleManager idleManager, @@ -27,6 +29,7 @@ public RevitDocumentStore( IJsonSerializer jsonSerializer, DocumentModelStorageSchema documentModelStorageSchema, IdStorageSchema idStorageSchema, + IEventAggregator eventAggregator, ITopLevelExceptionHandler topLevelExceptionHandler ) : base(jsonSerializer) @@ -35,6 +38,7 @@ ITopLevelExceptionHandler topLevelExceptionHandler _revitContext = revitContext; _documentModelStorageSchema = documentModelStorageSchema; _idStorageSchema = idStorageSchema; + _eventAggregator = eventAggregator; UIApplication uiApplication = _revitContext.UIApplication.NotNull(); @@ -49,9 +53,11 @@ ITopLevelExceptionHandler topLevelExceptionHandler // There is no event that we can hook here for double-click file open... // It is kind of harmless since we create this object as "SingleInstance". LoadState(); - OnDocumentChanged(); } + public override Task OnDocumentStoreInitialized() => + _eventAggregator.GetEvent().PublishAsync(new object()); + /// /// This is the place where we track document switch for new document -> Responsible to Read from new doc /// @@ -71,10 +77,10 @@ private void OnViewActivated(object? _, ViewActivatedEventArgs e) IsDocumentInit = true; _idleManager.SubscribeToIdle( nameof(RevitDocumentStore), - () => + async () => { LoadState(); - OnDocumentChanged(); + await _eventAggregator.GetEvent().PublishAsync(new object()); } ); } diff --git a/Connectors/Revit/Speckle.Connectors.RevitShared/Plugin/RevitExternalApplication.cs b/Connectors/Revit/Speckle.Connectors.RevitShared/Plugin/RevitExternalApplication.cs index 3eb15365b..0cd5c1b60 100644 --- a/Connectors/Revit/Speckle.Connectors.RevitShared/Plugin/RevitExternalApplication.cs +++ b/Connectors/Revit/Speckle.Connectors.RevitShared/Plugin/RevitExternalApplication.cs @@ -2,6 +2,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Speckle.Connectors.Common; +using Speckle.Connectors.DUI; using Speckle.Connectors.Revit.DependencyInjection; using Speckle.Converters.RevitShared; using Speckle.Sdk; @@ -47,6 +48,7 @@ public Result OnStartup(UIControlledApplication application) services.AddRevitConverters(); services.AddSingleton(application); _container = services.BuildServiceProvider(); + _container.UseDUI(); // resolve root object _revitPlugin = _container.GetRequiredService(); diff --git a/Connectors/Revit/Speckle.Connectors.RevitShared/Plugin/RevitThreadContext.cs b/Connectors/Revit/Speckle.Connectors.RevitShared/Plugin/RevitThreadContext.cs index c64c86c5a..3d0cd8216 100644 --- a/Connectors/Revit/Speckle.Connectors.RevitShared/Plugin/RevitThreadContext.cs +++ b/Connectors/Revit/Speckle.Connectors.RevitShared/Plugin/RevitThreadContext.cs @@ -1,4 +1,4 @@ -using Revit.Async; +using Revit.Async; using Speckle.Connectors.Common.Threading; namespace Speckle.Connectors.Revit.Plugin; diff --git a/Connectors/Rhino/Speckle.Connectors.RhinoShared/Bindings/RhinoBasicConnectorBinding.cs b/Connectors/Rhino/Speckle.Connectors.RhinoShared/Bindings/RhinoBasicConnectorBinding.cs index b34e002d0..1cdc94ed1 100644 --- a/Connectors/Rhino/Speckle.Connectors.RhinoShared/Bindings/RhinoBasicConnectorBinding.cs +++ b/Connectors/Rhino/Speckle.Connectors.RhinoShared/Bindings/RhinoBasicConnectorBinding.cs @@ -4,6 +4,7 @@ using Speckle.Connectors.Common.Caching; using Speckle.Connectors.DUI.Bindings; using Speckle.Connectors.DUI.Bridge; +using Speckle.Connectors.DUI.Eventing; using Speckle.Connectors.DUI.Models; using Speckle.Connectors.DUI.Models.Card; using Speckle.Connectors.Rhino.Extensions; @@ -26,7 +27,8 @@ public RhinoBasicConnectorBinding( DocumentModelStore store, IBrowserBridge parent, ISendConversionCache sendConversionCache, - ISpeckleApplication speckleApplication + ISpeckleApplication speckleApplication, + IEventAggregator eventAggregator ) { _store = store; @@ -35,8 +37,9 @@ ISpeckleApplication speckleApplication _speckleApplication = speckleApplication; Commands = new BasicConnectorBindingCommands(parent); - _store.DocumentChanged += (_, _) => - parent.TopLevelExceptionHandler.FireAndForget(async () => + eventAggregator + .GetEvent() + .Subscribe(async _ => { await Commands.NotifyDocumentChanged(); // Note: this prevents scaling issues when copy-pasting from one rhino doc to another in the same session. diff --git a/Connectors/Rhino/Speckle.Connectors.RhinoShared/Bindings/RhinoSelectionBinding.cs b/Connectors/Rhino/Speckle.Connectors.RhinoShared/Bindings/RhinoSelectionBinding.cs index d2679b8b3..f91a37ba3 100644 --- a/Connectors/Rhino/Speckle.Connectors.RhinoShared/Bindings/RhinoSelectionBinding.cs +++ b/Connectors/Rhino/Speckle.Connectors.RhinoShared/Bindings/RhinoSelectionBinding.cs @@ -2,29 +2,30 @@ using Rhino.DocObjects; using Speckle.Connectors.DUI.Bindings; using Speckle.Connectors.DUI.Bridge; +using Speckle.Connectors.DUI.Eventing; +using Speckle.Connectors.RhinoShared; namespace Speckle.Connectors.Rhino.Bindings; public class RhinoSelectionBinding : ISelectionBinding { - private readonly IAppIdleManager _idleManager; private const string SELECTION_EVENT = "setSelection"; + private readonly IEventAggregator _eventAggregator; public string Name => "selectionBinding"; public IBrowserBridge Parent { get; } - public RhinoSelectionBinding(IAppIdleManager idleManager, IBrowserBridge parent) + public RhinoSelectionBinding(IBrowserBridge parent, IEventAggregator eventAggregator) { - _idleManager = idleManager; Parent = parent; - - RhinoDoc.SelectObjects += OnSelectionChange; - RhinoDoc.DeselectObjects += OnSelectionChange; - RhinoDoc.DeselectAllObjects += OnSelectionChange; + _eventAggregator = eventAggregator; + eventAggregator.GetEvent().Subscribe(OnSelectionChange); + eventAggregator.GetEvent().Subscribe(OnSelectionChange); + eventAggregator.GetEvent().Subscribe(OnSelectionChange); } - private void OnSelectionChange(object? o, EventArgs eventArgs) => - _idleManager.SubscribeToIdle(nameof(RhinoSelectionBinding), UpdateSelection); + private void OnSelectionChange(EventArgs eventArgs) => + _eventAggregator.GetEvent().OneTimeSubscribe(nameof(RhinoSelectionBinding), _ => UpdateSelection()); private void UpdateSelection() { diff --git a/Connectors/Rhino/Speckle.Connectors.RhinoShared/Bindings/RhinoSendBinding.cs b/Connectors/Rhino/Speckle.Connectors.RhinoShared/Bindings/RhinoSendBinding.cs index 61e389faa..9e9a27fa2 100644 --- a/Connectors/Rhino/Speckle.Connectors.RhinoShared/Bindings/RhinoSendBinding.cs +++ b/Connectors/Rhino/Speckle.Connectors.RhinoShared/Bindings/RhinoSendBinding.cs @@ -10,12 +10,14 @@ using Speckle.Connectors.Common.Operations; using Speckle.Connectors.DUI.Bindings; using Speckle.Connectors.DUI.Bridge; +using Speckle.Connectors.DUI.Eventing; using Speckle.Connectors.DUI.Exceptions; using Speckle.Connectors.DUI.Logging; using Speckle.Connectors.DUI.Models; using Speckle.Connectors.DUI.Models.Card; using Speckle.Connectors.DUI.Models.Card.SendFilter; using Speckle.Connectors.DUI.Settings; +using Speckle.Connectors.RhinoShared; using Speckle.Converters.Common; using Speckle.Converters.Rhino; using Speckle.Sdk; @@ -31,14 +33,12 @@ public sealed class RhinoSendBinding : ISendBinding public IBrowserBridge Parent { get; } private readonly DocumentModelStore _store; - private readonly IAppIdleManager _idleManager; private readonly IServiceProvider _serviceProvider; private readonly List _sendFilters; private readonly CancellationManager _cancellationManager; private readonly ISendConversionCache _sendConversionCache; private readonly IOperationProgressManager _operationProgressManager; private readonly ILogger _logger; - private readonly ITopLevelExceptionHandler _topLevelExceptionHandler; private readonly IRhinoConversionSettingsFactory _rhinoConversionSettingsFactory; private readonly ISpeckleApplication _speckleApplication; private readonly ISdkActivityFactory _activityFactory; @@ -63,7 +63,6 @@ public sealed class RhinoSendBinding : ISendBinding public RhinoSendBinding( DocumentModelStore store, - IAppIdleManager idleManager, IBrowserBridge parent, IEnumerable sendFilters, IServiceProvider serviceProvider, @@ -73,11 +72,11 @@ public RhinoSendBinding( ILogger logger, IRhinoConversionSettingsFactory rhinoConversionSettingsFactory, ISpeckleApplication speckleApplication, - ISdkActivityFactory activityFactory + ISdkActivityFactory activityFactory, + IEventAggregator eventAggregator ) { _store = store; - _idleManager = idleManager; _serviceProvider = serviceProvider; _sendFilters = sendFilters.ToList(); _cancellationManager = cancellationManager; @@ -86,15 +85,14 @@ ISdkActivityFactory activityFactory _logger = logger; _rhinoConversionSettingsFactory = rhinoConversionSettingsFactory; _speckleApplication = speckleApplication; - _topLevelExceptionHandler = parent.TopLevelExceptionHandler.Parent.TopLevelExceptionHandler; Parent = parent; Commands = new SendBindingUICommands(parent); // POC: Commands are tightly coupled with their bindings, at least for now, saves us injecting a factory. _activityFactory = activityFactory; PreviousUnitSystem = RhinoDoc.ActiveDoc.ModelUnitSystem; - SubscribeToRhinoEvents(); + SubscribeToRhinoEvents(eventAggregator); } - private void SubscribeToRhinoEvents() + private void SubscribeToRhinoEvents(IEventAggregator eventAggregator) { Command.BeginCommand += (_, e) => { @@ -110,29 +108,33 @@ private void SubscribeToRhinoEvents() { ChangedObjectIdsInGroupsOrLayers[selectedObject.Id.ToString()] = 1; } - _idleManager.SubscribeToIdle(nameof(RhinoSendBinding), RunExpirationChecks); + eventAggregator.GetEvent().OneTimeSubscribe(nameof(RhinoSendBinding), _ => RunExpirationChecks()); } }; - - RhinoDoc.ActiveDocumentChanged += (_, e) => - { - PreviousUnitSystem = e.Document.ModelUnitSystem; - }; + eventAggregator + .GetEvent() + .Subscribe(e => + { + PreviousUnitSystem = e.Document.ModelUnitSystem; + }); // NOTE: BE CAREFUL handling things in this event handler since it is triggered whenever we save something into file! - RhinoDoc.DocumentPropertiesChanged += async (_, e) => - { - var newUnit = e.Document.ModelUnitSystem; - if (newUnit != PreviousUnitSystem) + eventAggregator + .GetEvent() + .Subscribe(async e => { - PreviousUnitSystem = newUnit; + var newUnit = e.Document.ModelUnitSystem; + if (newUnit != PreviousUnitSystem) + { + PreviousUnitSystem = newUnit; - await InvalidateAllSender(); - } - }; + await InvalidateAllSender(); + } + }); - RhinoDoc.AddRhinoObject += (_, e) => - _topLevelExceptionHandler.CatchUnhandled(() => + eventAggregator + .GetEvent() + .Subscribe(e => { if (!_store.IsDocumentInit) { @@ -140,11 +142,12 @@ private void SubscribeToRhinoEvents() } ChangedObjectIds[e.ObjectId.ToString()] = 1; - _idleManager.SubscribeToIdle(nameof(RhinoSendBinding), RunExpirationChecks); + eventAggregator.GetEvent().OneTimeSubscribe(nameof(RhinoSendBinding), _ => RunExpirationChecks()); }); - RhinoDoc.DeleteRhinoObject += (_, e) => - _topLevelExceptionHandler.CatchUnhandled(() => + eventAggregator + .GetEvent() + .Subscribe(e => { if (!_store.IsDocumentInit) { @@ -152,12 +155,13 @@ private void SubscribeToRhinoEvents() } ChangedObjectIds[e.ObjectId.ToString()] = 1; - _idleManager.SubscribeToIdle(nameof(RhinoSendBinding), RunExpirationChecks); + eventAggregator.GetEvent().OneTimeSubscribe(nameof(RhinoSendBinding), _ => RunExpirationChecks()); }); // NOTE: Catches an object's material change from one user defined doc material to another. Does not catch (as the top event is not triggered) swapping material sources for an object or moving to/from the default material (this is handled below)! - RhinoDoc.RenderMaterialsTableEvent += (_, args) => - _topLevelExceptionHandler.CatchUnhandled(() => + eventAggregator + .GetEvent() + .Subscribe(args => { if (!_store.IsDocumentInit) { @@ -167,12 +171,13 @@ private void SubscribeToRhinoEvents() if (args is RhinoDoc.RenderMaterialAssignmentChangedEventArgs changedEventArgs) { ChangedObjectIds[changedEventArgs.ObjectId.ToString()] = 1; - _idleManager.SubscribeToIdle(nameof(RhinoSendBinding), RunExpirationChecks); + eventAggregator.GetEvent().OneTimeSubscribe(nameof(RhinoSendBinding), _ => RunExpirationChecks()); } }); - RhinoDoc.GroupTableEvent += (_, args) => - _topLevelExceptionHandler.CatchUnhandled(() => + eventAggregator + .GetEvent() + .Subscribe(args => { if (!_store.IsDocumentInit) { @@ -183,11 +188,12 @@ private void SubscribeToRhinoEvents() { ChangedObjectIdsInGroupsOrLayers[obj.Id.ToString()] = 1; } - _idleManager.SubscribeToIdle(nameof(RhinoSendBinding), RunExpirationChecks); + eventAggregator.GetEvent().OneTimeSubscribe(nameof(RhinoSendBinding), _ => RunExpirationChecks()); }); - RhinoDoc.LayerTableEvent += (_, args) => - _topLevelExceptionHandler.CatchUnhandled(() => + eventAggregator + .GetEvent() + .Subscribe(args => { if (!_store.IsDocumentInit) { @@ -215,12 +221,13 @@ private void SubscribeToRhinoEvents() ChangedObjectIdsInGroupsOrLayers[obj.Id.ToString()] = 1; } } - _idleManager.SubscribeToIdle(nameof(RhinoSendBinding), RunExpirationChecks); + eventAggregator.GetEvent().OneTimeSubscribe(nameof(RhinoSendBinding), _ => RunExpirationChecks()); }); // Catches and stores changed material ids. These are then used in the expiry checks to invalidate all objects that have assigned any of those material ids. - RhinoDoc.MaterialTableEvent += (_, args) => - _topLevelExceptionHandler.CatchUnhandled(() => + eventAggregator + .GetEvent() + .Subscribe(args => { if (!_store.IsDocumentInit) { @@ -230,12 +237,13 @@ private void SubscribeToRhinoEvents() if (args.EventType == MaterialTableEventType.Modified) { ChangedMaterialIndexes[args.Index] = 1; - _idleManager.SubscribeToIdle(nameof(RhinoSendBinding), RunExpirationChecks); + eventAggregator.GetEvent().OneTimeSubscribe(nameof(RhinoSendBinding), _ => RunExpirationChecks()); } }); - RhinoDoc.ModifyObjectAttributes += (_, e) => - _topLevelExceptionHandler.CatchUnhandled(() => + eventAggregator + .GetEvent() + .Subscribe(e => { if (!_store.IsDocumentInit) { @@ -251,12 +259,13 @@ private void SubscribeToRhinoEvents() ) { ChangedObjectIds[e.RhinoObject.Id.ToString()] = 1; - _idleManager.SubscribeToIdle(nameof(RhinoSendBinding), RunExpirationChecks); + eventAggregator.GetEvent().OneTimeSubscribe(nameof(RhinoSendBinding), _ => RunExpirationChecks()); } }); - RhinoDoc.ReplaceRhinoObject += (_, e) => - _topLevelExceptionHandler.CatchUnhandled(() => + eventAggregator + .GetEvent() + .Subscribe(e => { if (!_store.IsDocumentInit) { @@ -265,7 +274,7 @@ private void SubscribeToRhinoEvents() ChangedObjectIds[e.NewRhinoObject.Id.ToString()] = 1; ChangedObjectIds[e.OldRhinoObject.Id.ToString()] = 1; - _idleManager.SubscribeToIdle(nameof(RhinoSendBinding), RunExpirationChecks); + eventAggregator.GetEvent().OneTimeSubscribe(nameof(RhinoSendBinding), _ => RunExpirationChecks()); }); } diff --git a/Connectors/Rhino/Speckle.Connectors.RhinoShared/Events.cs b/Connectors/Rhino/Speckle.Connectors.RhinoShared/Events.cs new file mode 100644 index 000000000..0e7a68021 --- /dev/null +++ b/Connectors/Rhino/Speckle.Connectors.RhinoShared/Events.cs @@ -0,0 +1,81 @@ +using Rhino; +using Rhino.DocObjects; +using Rhino.DocObjects.Tables; +using Speckle.Connectors.Common.Threading; +using Speckle.Connectors.DUI.Bridge; +using Speckle.Connectors.DUI.Eventing; + +namespace Speckle.Connectors.RhinoShared; + +public class BeginOpenDocument(IThreadContext threadContext, ITopLevelExceptionHandler exceptionHandler) + : ThreadedEvent(threadContext, exceptionHandler); + +public class EndOpenDocument(IThreadContext threadContext, ITopLevelExceptionHandler exceptionHandler) + : ThreadedEvent(threadContext, exceptionHandler); + +public class SelectObjects(IThreadContext threadContext, ITopLevelExceptionHandler exceptionHandler) + : ThreadedEvent(threadContext, exceptionHandler); + +public class DeselectObjects(IThreadContext threadContext, ITopLevelExceptionHandler exceptionHandler) + : ThreadedEvent(threadContext, exceptionHandler); + +public class DeselectAllObjects(IThreadContext threadContext, ITopLevelExceptionHandler exceptionHandler) + : ThreadedEvent(threadContext, exceptionHandler); + +public class ActiveDocumentChanged(IThreadContext threadContext, ITopLevelExceptionHandler exceptionHandler) + : ThreadedEvent(threadContext, exceptionHandler); + +public class DocumentPropertiesChanged(IThreadContext threadContext, ITopLevelExceptionHandler exceptionHandler) + : ThreadedEvent(threadContext, exceptionHandler); + +public class AddRhinoObject(IThreadContext threadContext, ITopLevelExceptionHandler exceptionHandler) + : ThreadedEvent(threadContext, exceptionHandler); + +public class DeleteRhinoObject(IThreadContext threadContext, ITopLevelExceptionHandler exceptionHandler) + : ThreadedEvent(threadContext, exceptionHandler); + +public class RenderMaterialsTableEvent(IThreadContext threadContext, ITopLevelExceptionHandler exceptionHandler) + : ThreadedEvent(threadContext, exceptionHandler); + +public class MaterialTableEvent(IThreadContext threadContext, ITopLevelExceptionHandler exceptionHandler) + : ThreadedEvent(threadContext, exceptionHandler); + +public class ModifyObjectAttributes(IThreadContext threadContext, ITopLevelExceptionHandler exceptionHandler) + : ThreadedEvent(threadContext, exceptionHandler); + +public class ReplaceRhinoObject(IThreadContext threadContext, ITopLevelExceptionHandler exceptionHandler) + : ThreadedEvent(threadContext, exceptionHandler); + +public class GroupTableEvent(IThreadContext threadContext, ITopLevelExceptionHandler exceptionHandler) + : ThreadedEvent(threadContext, exceptionHandler); + +public class LayerTableEvent(IThreadContext threadContext, ITopLevelExceptionHandler exceptionHandler) + : ThreadedEvent(threadContext, exceptionHandler); + +public static class RhinoEvents +{ + public static void Register(IEventAggregator eventAggregator) + { + RhinoApp.Idle += async (_, e) => await eventAggregator.GetEvent().PublishAsync(e); + + RhinoDoc.BeginOpenDocument += async (_, e) => await eventAggregator.GetEvent().PublishAsync(e); + RhinoDoc.EndOpenDocument += async (_, e) => await eventAggregator.GetEvent().PublishAsync(e); + RhinoDoc.SelectObjects += async (_, e) => await eventAggregator.GetEvent().PublishAsync(e); + RhinoDoc.DeselectObjects += async (_, e) => await eventAggregator.GetEvent().PublishAsync(e); + RhinoDoc.DeselectAllObjects += async (_, e) => await eventAggregator.GetEvent().PublishAsync(e); + RhinoDoc.ActiveDocumentChanged += async (_, e) => + await eventAggregator.GetEvent().PublishAsync(e); + RhinoDoc.DocumentPropertiesChanged += async (_, e) => + await eventAggregator.GetEvent().PublishAsync(e); + RhinoDoc.AddRhinoObject += async (_, e) => await eventAggregator.GetEvent().PublishAsync(e); + RhinoDoc.DeleteRhinoObject += async (_, e) => await eventAggregator.GetEvent().PublishAsync(e); + RhinoDoc.RenderMaterialsTableEvent += async (_, e) => + await eventAggregator.GetEvent().PublishAsync(e); + RhinoDoc.MaterialTableEvent += async (_, e) => await eventAggregator.GetEvent().PublishAsync(e); + RhinoDoc.ModifyObjectAttributes += async (_, e) => + await eventAggregator.GetEvent().PublishAsync(e); + RhinoDoc.ReplaceRhinoObject += async (_, e) => await eventAggregator.GetEvent().PublishAsync(e); + RhinoDoc.GroupTableEvent += async (_, e) => await eventAggregator.GetEvent().PublishAsync(e); + RhinoDoc.LayerTableEvent += async (_, e) => await eventAggregator.GetEvent().PublishAsync(e); + } +} diff --git a/Connectors/Rhino/Speckle.Connectors.RhinoShared/HostApp/RhinoDocumentStore.cs b/Connectors/Rhino/Speckle.Connectors.RhinoShared/HostApp/RhinoDocumentStore.cs index a921eb153..00bd73746 100644 --- a/Connectors/Rhino/Speckle.Connectors.RhinoShared/HostApp/RhinoDocumentStore.cs +++ b/Connectors/Rhino/Speckle.Connectors.RhinoShared/HostApp/RhinoDocumentStore.cs @@ -1,7 +1,8 @@ using Rhino; -using Speckle.Connectors.DUI.Bridge; +using Speckle.Connectors.DUI.Eventing; using Speckle.Connectors.DUI.Models; using Speckle.Connectors.DUI.Utils; +using Speckle.Connectors.RhinoShared; namespace Speckle.Connectors.Rhino.HostApp; @@ -10,12 +11,13 @@ public class RhinoDocumentStore : DocumentModelStore private const string SPECKLE_KEY = "Speckle_DUI3"; public override bool IsDocumentInit { get; set; } = true; // Note: because of rhino implementation details regarding expiry checking of sender cards. - public RhinoDocumentStore(IJsonSerializer jsonSerializer, ITopLevelExceptionHandler topLevelExceptionHandler) + public RhinoDocumentStore(IJsonSerializer jsonSerializer, IEventAggregator eventAggregator) : base(jsonSerializer) { - RhinoDoc.BeginOpenDocument += (_, _) => topLevelExceptionHandler.CatchUnhandled(() => IsDocumentInit = false); - RhinoDoc.EndOpenDocument += (_, e) => - topLevelExceptionHandler.CatchUnhandled(() => + eventAggregator.GetEvent().Subscribe(_ => IsDocumentInit = false); + eventAggregator + .GetEvent() + .Subscribe(async e => { if (e.Merge) { @@ -29,7 +31,7 @@ public RhinoDocumentStore(IJsonSerializer jsonSerializer, ITopLevelExceptionHand IsDocumentInit = true; LoadState(); - OnDocumentChanged(); + await eventAggregator.GetEvent().PublishAsync(new object()); }); } diff --git a/Connectors/Rhino/Speckle.Connectors.RhinoShared/HostApp/RhinoIdleManager.cs b/Connectors/Rhino/Speckle.Connectors.RhinoShared/HostApp/RhinoIdleManager.cs deleted file mode 100644 index f536f5786..000000000 --- a/Connectors/Rhino/Speckle.Connectors.RhinoShared/HostApp/RhinoIdleManager.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Rhino; -using Speckle.Connectors.DUI.Bridge; - -namespace Speckle.Connectors.Rhino.HostApp; - -/// -/// Rhino Idle Manager is a helper util to manage deferred actions. -/// -public sealed class RhinoIdleManager(IIdleCallManager idleCallManager) : AppIdleManager(idleCallManager) -{ - private readonly IIdleCallManager _idleCallManager = idleCallManager; - - protected override void AddEvent() - { - RhinoApp.Idle += RhinoAppOnIdle; - } - - private void RhinoAppOnIdle(object? sender, EventArgs e) => - _idleCallManager.AppOnIdle(() => RhinoApp.Idle -= RhinoAppOnIdle); -} diff --git a/Connectors/Rhino/Speckle.Connectors.RhinoShared/Plugin/RhinoPlugin.cs b/Connectors/Rhino/Speckle.Connectors.RhinoShared/Plugin/RhinoPlugin.cs deleted file mode 100644 index 3a060c253..000000000 --- a/Connectors/Rhino/Speckle.Connectors.RhinoShared/Plugin/RhinoPlugin.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Rhino; -using Speckle.Connectors.DUI.Bridge; -using Speckle.Connectors.Rhino.Plugin; -using Speckle.InterfaceGenerator; - -namespace Speckle.Connectors.Rhino.DependencyInjection; - -[GenerateAutoInterface] -public class RhinoPlugin : IRhinoPlugin -{ - private readonly IAppIdleManager _idleManager; - - public RhinoPlugin(IAppIdleManager idleManager) - { - _idleManager = idleManager; - } - - public void Initialise() => - _idleManager.SubscribeToIdle( - nameof(RhinoPlugin), - () => RhinoApp.RunScript(SpeckleConnectorsRhinoCommand.Instance.EnglishName, false) - ); - - public void Shutdown() { } -} diff --git a/Connectors/Rhino/Speckle.Connectors.RhinoShared/Plugin/Speckle.Connectors.RhinoPlugin.cs b/Connectors/Rhino/Speckle.Connectors.RhinoShared/Plugin/Speckle.Connectors.RhinoPlugin.cs index a1158578e..105b74ce9 100644 --- a/Connectors/Rhino/Speckle.Connectors.RhinoShared/Plugin/Speckle.Connectors.RhinoPlugin.cs +++ b/Connectors/Rhino/Speckle.Connectors.RhinoShared/Plugin/Speckle.Connectors.RhinoPlugin.cs @@ -1,7 +1,10 @@ using Microsoft.Extensions.DependencyInjection; using Rhino.PlugIns; using Speckle.Connectors.Common; +using Speckle.Connectors.DUI; +using Speckle.Connectors.DUI.Eventing; using Speckle.Connectors.Rhino.DependencyInjection; +using Speckle.Connectors.RhinoShared; using Speckle.Converters.Rhino; using Speckle.Sdk; using Speckle.Sdk.Host; @@ -19,7 +22,6 @@ namespace Speckle.Connectors.Rhino.Plugin; /// public class SpeckleConnectorsRhinoPlugin : PlugIn { - private IRhinoPlugin? _rhinoPlugin; private IDisposable? _disposableLogger; protected override string LocalPlugInName => "Speckle (Beta) for Rhino"; @@ -51,10 +53,8 @@ protected override LoadReturnCode OnLoad(ref string errorMessage) // but the Rhino connector has `.rhp` as it is extension. Container = services.BuildServiceProvider(); - - // Resolve root plugin object and initialise. - _rhinoPlugin = Container.GetRequiredService(); - _rhinoPlugin.Initialise(); + RhinoEvents.Register(Container.GetRequiredService()); + Container.UseDUI(); return LoadReturnCode.Success; } @@ -78,7 +78,6 @@ private HostAppVersion GetVersion() protected override void OnShutdown() { - _rhinoPlugin?.Shutdown(); _disposableLogger?.Dispose(); Container?.Dispose(); base.OnShutdown(); diff --git a/Connectors/Rhino/Speckle.Connectors.RhinoShared/Registration/ServiceRegistration.cs b/Connectors/Rhino/Speckle.Connectors.RhinoShared/Registration/ServiceRegistration.cs index a5ad1c12d..82bf48c49 100644 --- a/Connectors/Rhino/Speckle.Connectors.RhinoShared/Registration/ServiceRegistration.cs +++ b/Connectors/Rhino/Speckle.Connectors.RhinoShared/Registration/ServiceRegistration.cs @@ -11,7 +11,6 @@ using Speckle.Connectors.Common.Threading; using Speckle.Connectors.DUI; using Speckle.Connectors.DUI.Bindings; -using Speckle.Connectors.DUI.Bridge; using Speckle.Connectors.DUI.Models.Card.SendFilter; using Speckle.Connectors.DUI.WebView; using Speckle.Connectors.Rhino.Bindings; @@ -36,17 +35,11 @@ public static void AddRhino(this IServiceCollection serviceCollection) serviceCollection.AddDUI(); serviceCollection.AddDUIView(); - // Register other connector specific types - serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); - // Register bindings serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); // POC: Easier like this for now, should be cleaned up later serviceCollection.AddSingleton(); - serviceCollection.RegisterTopLevelExceptionHandler(); - serviceCollection.AddSingleton(sp => sp.GetRequiredService()); serviceCollection.AddSingleton(); diff --git a/Connectors/Rhino/Speckle.Connectors.RhinoShared/Speckle.Connectors.RhinoShared.projitems b/Connectors/Rhino/Speckle.Connectors.RhinoShared/Speckle.Connectors.RhinoShared.projitems index 42d4d5842..766cef7d8 100644 --- a/Connectors/Rhino/Speckle.Connectors.RhinoShared/Speckle.Connectors.RhinoShared.projitems +++ b/Connectors/Rhino/Speckle.Connectors.RhinoShared/Speckle.Connectors.RhinoShared.projitems @@ -21,6 +21,7 @@ + @@ -32,7 +33,6 @@ - @@ -43,7 +43,6 @@ - diff --git a/Connectors/Tekla/Speckle.Connector.TeklaShared/Bindings/TeklaBasicConnectorBinding.cs b/Connectors/Tekla/Speckle.Connector.TeklaShared/Bindings/TeklaBasicConnectorBinding.cs index 519ac132e..a1e9d2c54 100644 --- a/Connectors/Tekla/Speckle.Connector.TeklaShared/Bindings/TeklaBasicConnectorBinding.cs +++ b/Connectors/Tekla/Speckle.Connector.TeklaShared/Bindings/TeklaBasicConnectorBinding.cs @@ -2,6 +2,7 @@ using Microsoft.Extensions.Logging; using Speckle.Connectors.DUI.Bindings; using Speckle.Connectors.DUI.Bridge; +using Speckle.Connectors.DUI.Eventing; using Speckle.Connectors.DUI.Models; using Speckle.Connectors.DUI.Models.Card; using Speckle.Sdk; @@ -25,6 +26,7 @@ public TeklaBasicConnectorBinding( ISpeckleApplication speckleApplication, DocumentModelStore store, ILogger logger, + IEventAggregator eventAggregator, TSM.Model model ) { @@ -34,8 +36,9 @@ TSM.Model model _logger = logger; _model = model; Commands = new BasicConnectorBindingCommands(parent); - _store.DocumentChanged += (_, _) => - parent.TopLevelExceptionHandler.FireAndForget(async () => + eventAggregator + .GetEvent() + .Subscribe(async _ => { await Commands.NotifyDocumentChanged(); }); diff --git a/Connectors/Tekla/Speckle.Connector.TeklaShared/Bindings/TeklaSelectionBinding.cs b/Connectors/Tekla/Speckle.Connector.TeklaShared/Bindings/TeklaSelectionBinding.cs index 710d0d5b5..5439a2edb 100644 --- a/Connectors/Tekla/Speckle.Connector.TeklaShared/Bindings/TeklaSelectionBinding.cs +++ b/Connectors/Tekla/Speckle.Connector.TeklaShared/Bindings/TeklaSelectionBinding.cs @@ -1,41 +1,35 @@ using Speckle.Connectors.DUI.Bindings; using Speckle.Connectors.DUI.Bridge; +using Speckle.Connectors.DUI.Eventing; using Tekla.Structures.Model; namespace Speckle.Connectors.TeklaShared.Bindings; public class TeklaSelectionBinding : ISelectionBinding { - private readonly IAppIdleManager _idleManager; private const string SELECTION_EVENT = "setSelection"; - private readonly Events _events; - private readonly object _selectionEventHandlerLock = new object(); + private readonly object _selectionEventHandlerLock = new(); private readonly Tekla.Structures.Model.UI.ModelObjectSelector _selector; public string Name => "selectionBinding"; public IBrowserBridge Parent { get; } public TeklaSelectionBinding( - IAppIdleManager idleManager, IBrowserBridge parent, - Events events, - Tekla.Structures.Model.UI.ModelObjectSelector selector + Tekla.Structures.Model.UI.ModelObjectSelector selector, + IEventAggregator eventAggregator ) { - _idleManager = idleManager; Parent = parent; - _events = events; _selector = selector; - _events.SelectionChange += Events_SelectionChangeEvent; - _events.Register(); + eventAggregator.GetEvent().Subscribe(_ => Events_SelectionChangeEvent()); } private void Events_SelectionChangeEvent() { lock (_selectionEventHandlerLock) { - _idleManager.SubscribeToIdle(nameof(TeklaSelectionBinding), UpdateSelection); UpdateSelection(); } } @@ -43,16 +37,11 @@ private void Events_SelectionChangeEvent() private void UpdateSelection() { SelectionInfo selInfo = GetSelection(); - Parent.Send(SELECTION_EVENT, selInfo); + Parent.Send2(SELECTION_EVENT, selInfo); } public SelectionInfo GetSelection() { - if (_selector == null) - { - return new SelectionInfo(new List(), "No objects selected."); - } - var objectIds = new List(); var objectTypes = new List(); diff --git a/Connectors/Tekla/Speckle.Connector.TeklaShared/Bindings/TeklaSendBinding.cs b/Connectors/Tekla/Speckle.Connector.TeklaShared/Bindings/TeklaSendBinding.cs index cff0f1198..f8d0d4990 100644 --- a/Connectors/Tekla/Speckle.Connector.TeklaShared/Bindings/TeklaSendBinding.cs +++ b/Connectors/Tekla/Speckle.Connector.TeklaShared/Bindings/TeklaSendBinding.cs @@ -6,6 +6,7 @@ using Speckle.Connectors.Common.Operations; using Speckle.Connectors.DUI.Bindings; using Speckle.Connectors.DUI.Bridge; +using Speckle.Connectors.DUI.Eventing; using Speckle.Connectors.DUI.Exceptions; using Speckle.Connectors.DUI.Logging; using Speckle.Connectors.DUI.Models; @@ -24,14 +25,13 @@ namespace Speckle.Connectors.TeklaShared.Bindings; -public sealed class TeklaSendBinding : ISendBinding, IDisposable +public sealed class TeklaSendBinding : ISendBinding { public string Name => "sendBinding"; public SendBindingUICommands Commands { get; } public IBrowserBridge Parent { get; } private readonly DocumentModelStore _store; - private readonly IAppIdleManager _idleManager; private readonly IServiceProvider _serviceProvider; private readonly List _sendFilters; private readonly CancellationManager _cancellationManager; @@ -42,14 +42,12 @@ public sealed class TeklaSendBinding : ISendBinding, IDisposable private readonly ISpeckleApplication _speckleApplication; private readonly ISdkActivityFactory _activityFactory; private readonly Model _model; - private readonly Events _events; private readonly ToSpeckleSettingsManager _toSpeckleSettingsManager; private ConcurrentDictionary ChangedObjectIds { get; set; } = new(); public TeklaSendBinding( DocumentModelStore store, - IAppIdleManager idleManager, IBrowserBridge parent, IEnumerable sendFilters, IServiceProvider serviceProvider, @@ -60,11 +58,11 @@ public TeklaSendBinding( ITeklaConversionSettingsFactory teklaConversionSettingsFactory, ISpeckleApplication speckleApplication, ISdkActivityFactory activityFactory, - ToSpeckleSettingsManager toSpeckleSettingsManager + ToSpeckleSettingsManager toSpeckleSettingsManager, + IEventAggregator eventAggregator ) { _store = store; - _idleManager = idleManager; _serviceProvider = serviceProvider; _sendFilters = sendFilters.ToList(); _cancellationManager = cancellationManager; @@ -79,14 +77,7 @@ ToSpeckleSettingsManager toSpeckleSettingsManager _toSpeckleSettingsManager = toSpeckleSettingsManager; _model = new Model(); - _events = new Events(); - SubscribeToTeklaEvents(); - } - - private void SubscribeToTeklaEvents() - { - _events.ModelObjectChanged += ModelHandler_OnChange; - _events.Register(); + eventAggregator.GetEvent().Subscribe(ModelHandler_OnChange); } // subscribes the all changes in a modelobject @@ -195,15 +186,4 @@ private async Task RunExpirationChecks() ChangedObjectIds = new ConcurrentDictionary(); } - - private bool _disposed; - - public void Dispose() - { - if (!_disposed) - { - _events.UnRegister(); - _disposed = true; - } - } } diff --git a/Connectors/Tekla/Speckle.Connector.TeklaShared/Events.cs b/Connectors/Tekla/Speckle.Connector.TeklaShared/Events.cs new file mode 100644 index 000000000..c9999f26c --- /dev/null +++ b/Connectors/Tekla/Speckle.Connector.TeklaShared/Events.cs @@ -0,0 +1,27 @@ +using Speckle.Connectors.Common.Threading; +using Speckle.Connectors.DUI.Bridge; +using Speckle.Connectors.DUI.Eventing; + +namespace Speckle.Connectors.TeklaShared; + +public class SelectionChangeEvent(IThreadContext threadContext, ITopLevelExceptionHandler exceptionHandler) + : ThreadedEvent(threadContext, exceptionHandler); + +public class ModelObjectChangedEvent(IThreadContext threadContext, ITopLevelExceptionHandler exceptionHandler) + : ThreadedEvent>(threadContext, exceptionHandler); + +public class ModelLoadEvent(IThreadContext threadContext, ITopLevelExceptionHandler exceptionHandler) + : ThreadedEvent(threadContext, exceptionHandler); + +public static class TeklaEvents +{ + public static void Register(Tekla.Structures.Model.Events events, IEventAggregator eventAggregator) + { + events.UnRegister(); + events.SelectionChange += async () => + await eventAggregator.GetEvent().PublishAsync(new object()); + events.ModelObjectChanged += async x => await eventAggregator.GetEvent().PublishAsync(x); + events.ModelLoad += async () => await eventAggregator.GetEvent().PublishAsync(new object()); + events.Register(); + } +} diff --git a/Connectors/Tekla/Speckle.Connector.TeklaShared/HostApp/TeklaDocumentModelStore.cs b/Connectors/Tekla/Speckle.Connector.TeklaShared/HostApp/TeklaDocumentModelStore.cs index 5f75cb37e..dd7c59dd7 100644 --- a/Connectors/Tekla/Speckle.Connector.TeklaShared/HostApp/TeklaDocumentModelStore.cs +++ b/Connectors/Tekla/Speckle.Connector.TeklaShared/HostApp/TeklaDocumentModelStore.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.Logging; +using Speckle.Connectors.DUI.Eventing; using Speckle.Connectors.DUI.Models; using Speckle.Connectors.DUI.Utils; using Speckle.Sdk; @@ -10,34 +11,40 @@ namespace Speckle.Connectors.TeklaShared.HostApp; public class TeklaDocumentModelStore : DocumentModelStore { private readonly ILogger _logger; + private readonly IEventAggregator _eventAggregator; private readonly ISqLiteJsonCacheManager _jsonCacheManager; - private readonly TSM.Events _events; private readonly TSM.Model _model; private string? _modelKey; public TeklaDocumentModelStore( IJsonSerializer jsonSerializer, ILogger logger, - ISqLiteJsonCacheManagerFactory jsonCacheManagerFactory + ISqLiteJsonCacheManagerFactory jsonCacheManagerFactory, + IEventAggregator eventAggregator ) : base(jsonSerializer) { _logger = logger; + _eventAggregator = eventAggregator; _jsonCacheManager = jsonCacheManagerFactory.CreateForUser("ConnectorsFileData"); - _events = new TSM.Events(); _model = new TSM.Model(); GenerateKey(); - _events.ModelLoad += () => - { - GenerateKey(); - LoadState(); - OnDocumentChanged(); - }; - _events.Register(); + eventAggregator + .GetEvent() + .Subscribe(async _ => + { + GenerateKey(); + LoadState(); + await eventAggregator.GetEvent().PublishAsync(new object()); + }); + } + + public override async Task OnDocumentStoreInitialized() + { if (SpeckleTeklaPanelHost.IsInitialized) { LoadState(); - OnDocumentChanged(); + await _eventAggregator.GetEvent().PublishAsync(new object()); } } diff --git a/Connectors/Tekla/Speckle.Connector.TeklaShared/HostApp/TeklaIdleManager.cs b/Connectors/Tekla/Speckle.Connector.TeklaShared/HostApp/TeklaIdleManager.cs deleted file mode 100644 index 851020e61..000000000 --- a/Connectors/Tekla/Speckle.Connector.TeklaShared/HostApp/TeklaIdleManager.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Speckle.Connectors.DUI.Bridge; -using Tekla.Structures.Model; - -namespace Speckle.Connectors.TeklaShared.HostApp; - -public sealed class TeklaIdleManager : AppIdleManager -{ - private readonly IIdleCallManager _idleCallManager; - private readonly Events _events; - - public TeklaIdleManager(IIdleCallManager idleCallManager, Events events) - : base(idleCallManager) - { - _idleCallManager = idleCallManager; - _events = events; - } - - protected override void AddEvent() - { - _events.ModelSave += TeklaEventsOnIdle; - _events.Register(); - } - - private void TeklaEventsOnIdle() - { - _idleCallManager.AppOnIdle(() => - { - _events.ModelSave -= TeklaEventsOnIdle; - _events.UnRegister(); - }); - } -} diff --git a/Connectors/Tekla/Speckle.Connector.TeklaShared/ServiceRegistration.cs b/Connectors/Tekla/Speckle.Connector.TeklaShared/ServiceRegistration.cs index d9d42e0cf..f058061e8 100644 --- a/Connectors/Tekla/Speckle.Connector.TeklaShared/ServiceRegistration.cs +++ b/Connectors/Tekla/Speckle.Connector.TeklaShared/ServiceRegistration.cs @@ -35,15 +35,11 @@ public static IServiceCollection AddTekla(this IServiceCollection services) services.AddDUI(); services.AddDUIView(); - services.AddSingleton(); - services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); - services.RegisterTopLevelExceptionHandler(); - services.AddSingleton(sp => sp.GetRequiredService()); services.AddSingleton(); services.AddSingleton(); diff --git a/Connectors/Tekla/Speckle.Connector.TeklaShared/Speckle.Connectors.TeklaShared.projitems b/Connectors/Tekla/Speckle.Connector.TeklaShared/Speckle.Connectors.TeklaShared.projitems index e9782ec4d..330527ed7 100644 --- a/Connectors/Tekla/Speckle.Connector.TeklaShared/Speckle.Connectors.TeklaShared.projitems +++ b/Connectors/Tekla/Speckle.Connector.TeklaShared/Speckle.Connectors.TeklaShared.projitems @@ -17,13 +17,13 @@ + - diff --git a/Connectors/Tekla/Speckle.Connector.TeklaShared/SpeckleTeklaPanelHost.cs b/Connectors/Tekla/Speckle.Connector.TeklaShared/SpeckleTeklaPanelHost.cs index e957a4d17..c39e243af 100644 --- a/Connectors/Tekla/Speckle.Connector.TeklaShared/SpeckleTeklaPanelHost.cs +++ b/Connectors/Tekla/Speckle.Connector.TeklaShared/SpeckleTeklaPanelHost.cs @@ -5,6 +5,8 @@ using System.Windows.Forms.Integration; using Microsoft.Extensions.DependencyInjection; using Speckle.Connectors.Common; +using Speckle.Connectors.DUI; +using Speckle.Connectors.DUI.Eventing; using Speckle.Connectors.DUI.WebView; using Speckle.Converters.TeklaShared; using Speckle.Sdk.Host; @@ -89,6 +91,8 @@ private void InitializeInstance() services.AddTeklaConverters(); Container = services.BuildServiceProvider(); + TeklaEvents.Register(Container.GetRequiredService(), Container.GetRequiredService()); + Container.UseDUI(); Model = new Model(); if (!Model.GetConnectionStatus()) diff --git a/DUI3/Speckle.Connectors.DUI.Tests/Bridge/IdleCallManagerTests.cs b/DUI3/Speckle.Connectors.DUI.Tests/Bridge/IdleCallManagerTests.cs index 9c8397023..611f6af7f 100644 --- a/DUI3/Speckle.Connectors.DUI.Tests/Bridge/IdleCallManagerTests.cs +++ b/DUI3/Speckle.Connectors.DUI.Tests/Bridge/IdleCallManagerTests.cs @@ -15,7 +15,7 @@ public void SubscribeToIdleTest() var sut = new IdleCallManager(handler.Object); var action = Create(); var addEvent = Create(); - handler.Setup(x => x.CatchUnhandled(It.IsAny())); + handler.Setup(x => x.CatchUnhandled(It.IsAny())).Returns(new Result()); sut.SubscribeToIdle("id", action.Object, addEvent.Object); } diff --git a/DUI3/Speckle.Connectors.DUI.Tests/Bridge/TopLevelExceptionHandlerTests.cs b/DUI3/Speckle.Connectors.DUI.Tests/Bridge/TopLevelExceptionHandlerTests.cs index 8ba16dc14..cfa7deebb 100644 --- a/DUI3/Speckle.Connectors.DUI.Tests/Bridge/TopLevelExceptionHandlerTests.cs +++ b/DUI3/Speckle.Connectors.DUI.Tests/Bridge/TopLevelExceptionHandlerTests.cs @@ -2,8 +2,9 @@ using Microsoft.Extensions.Logging; using Moq; using NUnit.Framework; -using Speckle.Connectors.DUI.Bindings; +using Speckle.Connectors.Common.Threading; using Speckle.Connectors.DUI.Bridge; +using Speckle.Connectors.DUI.Eventing; using Speckle.Testing; namespace Speckle.Connectors.DUI.Tests.Bridge; @@ -14,8 +15,8 @@ public class TopLevelExceptionHandlerTests : MoqTest public void CatchUnhandledAction_Happy() { var logger = Create>(MockBehavior.Loose); - var bridge = Create(); - var sut = new TopLevelExceptionHandler(logger.Object, bridge.Object); + var eventAggregator = Create(); + var sut = new TopLevelExceptionHandler(logger.Object, eventAggregator.Object); sut.CatchUnhandled(() => { }); } @@ -24,13 +25,13 @@ public void CatchUnhandledAction_Happy() public void CatchUnhandledAction_Exception() { var logger = Create>(MockBehavior.Loose); - var bridge = Create(); + var eventAggregator = Create(); - bridge - .Setup(x => x.Send(BasicConnectorBindingCommands.SET_GLOBAL_NOTIFICATION, It.IsAny(), default)) - .Returns(Task.CompletedTask); + eventAggregator + .Setup(x => x.GetEvent()) + .Returns(new ExceptionEvent(Create().Object, Create().Object)); - var sut = new TopLevelExceptionHandler(logger.Object, bridge.Object); + var sut = new TopLevelExceptionHandler(logger.Object, eventAggregator.Object); sut.CatchUnhandled(() => throw new InvalidOperationException()); } @@ -40,8 +41,8 @@ public void CatchUnhandledFunc_Happy() { var val = 2; var logger = Create>(MockBehavior.Loose); - var bridge = Create(); - var sut = new TopLevelExceptionHandler(logger.Object, bridge.Object); + var eventAggregator = Create(); + var sut = new TopLevelExceptionHandler(logger.Object, eventAggregator.Object); var returnVal = sut.CatchUnhandled(() => val); returnVal.Value.Should().Be(val); @@ -53,13 +54,13 @@ public void CatchUnhandledFunc_Happy() public void CatchUnhandledFunc_Exception() { var logger = Create>(MockBehavior.Loose); - var bridge = Create(); + var eventAggregator = Create(); - bridge - .Setup(x => x.Send(BasicConnectorBindingCommands.SET_GLOBAL_NOTIFICATION, It.IsAny(), default)) - .Returns(Task.CompletedTask); + eventAggregator + .Setup(x => x.GetEvent()) + .Returns(new ExceptionEvent(Create().Object, Create().Object)); - var sut = new TopLevelExceptionHandler(logger.Object, bridge.Object); + var sut = new TopLevelExceptionHandler(logger.Object, eventAggregator.Object); var returnVal = sut.CatchUnhandled((Func)(() => throw new InvalidOperationException())); returnVal.Value.Should().BeNull(); @@ -71,13 +72,12 @@ public void CatchUnhandledFunc_Exception() public void CatchUnhandledFunc_Exception_Fatal() { var logger = Create>(MockBehavior.Loose); - var bridge = Create(); - var sut = new TopLevelExceptionHandler(logger.Object, bridge.Object); + var eventAggregator = Create(); + var sut = new TopLevelExceptionHandler(logger.Object, eventAggregator.Object); - var exception = Assert.Throws( + Assert.Throws( () => sut.CatchUnhandled(new Func(() => throw new AppDomainUnloadedException())) ); - exception.InnerExceptions.Single().Should().BeOfType(); } [Test] @@ -85,8 +85,8 @@ public async Task CatchUnhandledFuncAsync_Happy() { var val = 2; var logger = Create>(MockBehavior.Loose); - var bridge = Create(); - var sut = new TopLevelExceptionHandler(logger.Object, bridge.Object); + var eventAggregator = Create(); + var sut = new TopLevelExceptionHandler(logger.Object, eventAggregator.Object); var returnVal = await sut.CatchUnhandledAsync(() => Task.FromResult(val)); returnVal.Value.Should().Be(val); @@ -98,13 +98,13 @@ public async Task CatchUnhandledFuncAsync_Happy() public async Task CatchUnhandledFuncAsync_Exception() { var logger = Create>(MockBehavior.Loose); - var bridge = Create(); + var eventAggregator = Create(); - bridge - .Setup(x => x.Send(BasicConnectorBindingCommands.SET_GLOBAL_NOTIFICATION, It.IsAny(), default)) - .Returns(Task.CompletedTask); + eventAggregator + .Setup(x => x.GetEvent()) + .Returns(new ExceptionEvent(Create().Object, Create().Object)); - var sut = new TopLevelExceptionHandler(logger.Object, bridge.Object); + var sut = new TopLevelExceptionHandler(logger.Object, eventAggregator.Object); var returnVal = await sut.CatchUnhandledAsync(new Func>(() => throw new InvalidOperationException())); returnVal.Value.Should().BeNull(); @@ -116,8 +116,8 @@ public async Task CatchUnhandledFuncAsync_Exception() public void CatchUnhandledFuncAsync_Exception_Fatal() { var logger = Create>(MockBehavior.Loose); - var bridge = Create(); - var sut = new TopLevelExceptionHandler(logger.Object, bridge.Object); + var eventAggregator = Create(); + var sut = new TopLevelExceptionHandler(logger.Object, eventAggregator.Object); var exception = Assert.ThrowsAsync( async () => await sut.CatchUnhandledAsync(new Func>(() => throw new AppDomainUnloadedException())) diff --git a/DUI3/Speckle.Connectors.DUI.Tests/Eventing/EventAggregatorTests.cs b/DUI3/Speckle.Connectors.DUI.Tests/Eventing/EventAggregatorTests.cs new file mode 100644 index 000000000..0441bb05d --- /dev/null +++ b/DUI3/Speckle.Connectors.DUI.Tests/Eventing/EventAggregatorTests.cs @@ -0,0 +1,283 @@ +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using NUnit.Framework; +using Speckle.Connectors.Common.Threading; +using Speckle.Connectors.DUI.Bridge; +using Speckle.Connectors.DUI.Eventing; +using Speckle.Testing; + +namespace Speckle.Connectors.DUI.Tests.Eventing; + +public class TestEvent(IThreadContext threadContext, ITopLevelExceptionHandler exceptionHandler) + : ThreadedEvent(threadContext, exceptionHandler); + +public class TestOneTimeEvent(IThreadContext threadContext, ITopLevelExceptionHandler exceptionHandler) + : OneTimeThreadedEvent(threadContext, exceptionHandler); + +public class EventAggregatorTests : MoqTest +{ + [Test] + public async Task Sub_Async_DisposeToken() + { + s_val = false; + var services = new ServiceCollection(); + var exceptionHandler = new TopLevelExceptionHandler( + Create>().Object, + Create().Object + ); + services.AddSingleton(Create().Object); + services.AddSingleton(exceptionHandler); + services.AddTransient(); + services.AddSingleton(); + var serviceProvider = services.BuildServiceProvider(); + + var subscriptionToken = Test_Sub_Async_DisposeToken(serviceProvider); + var eventAggregator = serviceProvider.GetRequiredService(); + await eventAggregator.GetEvent().PublishAsync(new object()); + + s_val.Should().BeTrue(); + + GC.Collect(); + GC.WaitForPendingFinalizers(); + subscriptionToken.IsActive.Should().BeTrue(); + subscriptionToken.Dispose(); + GC.Collect(); + GC.WaitForPendingFinalizers(); + subscriptionToken.IsActive.Should().BeFalse(); + } + + private static SubscriptionToken Test_Sub_Async_DisposeToken(IServiceProvider serviceProvider) + { + var eventAggregator = serviceProvider.GetRequiredService(); + var subscriptionToken = eventAggregator + .GetEvent() + .Subscribe(_ => + { + s_val = true; + return Task.CompletedTask; + }); + return subscriptionToken; + } + + [Test] + public async Task Sub_Async_SubscribeToken() + { + s_val = false; + var services = new ServiceCollection(); + var exceptionHandler = new TopLevelExceptionHandler( + Create>().Object, + Create().Object + ); + services.AddSingleton(Create().Object); + services.AddSingleton(exceptionHandler); + services.AddTransient(); + services.AddSingleton(); + var serviceProvider = services.BuildServiceProvider(); + + var subscriptionToken = Test_Sub_Async_DisposeToken(serviceProvider); + var eventAggregator = serviceProvider.GetRequiredService(); + await eventAggregator.GetEvent().PublishAsync(new object()); + + s_val.Should().BeTrue(); + + GC.Collect(); + GC.WaitForPendingFinalizers(); + subscriptionToken.IsActive.Should().BeTrue(); + eventAggregator.GetEvent().Unsubscribe(subscriptionToken); + GC.Collect(); + GC.WaitForPendingFinalizers(); + subscriptionToken.IsActive.Should().BeFalse(); + } + + private static SubscriptionToken Test_Sub_Sync(IServiceProvider serviceProvider) + { + var eventAggregator = serviceProvider.GetRequiredService(); + var subscriptionToken = eventAggregator + .GetEvent() + .Subscribe(_ => + { + s_val = true; + }); + return subscriptionToken; + } + + [Test] + public async Task Sub_Sync() + { + s_val = false; + var services = new ServiceCollection(); + var exceptionHandler = new TopLevelExceptionHandler( + Create>().Object, + Create().Object + ); + services.AddSingleton(Create().Object); + services.AddSingleton(exceptionHandler); + services.AddTransient(); + + services.AddSingleton(); + var serviceProvider = services.BuildServiceProvider(); + + var subscriptionToken = Test_Sub_Sync(serviceProvider); + var eventAggregator = serviceProvider.GetRequiredService(); + await eventAggregator.GetEvent().PublishAsync(new object()); + + GC.Collect(); + GC.WaitForPendingFinalizers(); + subscriptionToken.IsActive.Should().BeTrue(); + s_val.Should().BeTrue(); + eventAggregator.GetEvent().Unsubscribe(subscriptionToken); + GC.Collect(); + GC.WaitForPendingFinalizers(); + subscriptionToken.IsActive.Should().BeFalse(); + } + + private static SubscriptionToken Test_Onetime_Sub_Async(IServiceProvider serviceProvider) + { + var eventAggregator = serviceProvider.GetRequiredService(); + var subscriptionToken = eventAggregator + .GetEvent() + .OneTimeSubscribe( + "test", + _ => + { + s_val = true; + return Task.CompletedTask; + } + ); + return subscriptionToken; + } + + [Test] + public async Task Onetime_Async() + { + s_val = false; + var services = new ServiceCollection(); + var exceptionHandler = new TopLevelExceptionHandler( + Create>().Object, + Create().Object + ); + services.AddSingleton(Create().Object); + services.AddSingleton(exceptionHandler); + services.AddTransient(); + + services.AddSingleton(); + var serviceProvider = services.BuildServiceProvider(); + + var subscriptionToken = Test_Onetime_Sub_Async(serviceProvider); + var eventAggregator = serviceProvider.GetRequiredService(); + GC.Collect(); + GC.WaitForPendingFinalizers(); + subscriptionToken.IsActive.Should().BeTrue(); + + await eventAggregator.GetEvent().PublishAsync(new object()); + + s_val.Should().BeTrue(); + + GC.Collect(); + GC.WaitForPendingFinalizers(); + subscriptionToken.IsActive.Should().BeFalse(); + subscriptionToken.Dispose(); + GC.Collect(); + GC.WaitForPendingFinalizers(); + subscriptionToken.IsActive.Should().BeFalse(); + } + + private static SubscriptionToken Test_Onetime_Sub_Sync(IServiceProvider serviceProvider) + { + var eventAggregator = serviceProvider.GetRequiredService(); + var subscriptionToken = eventAggregator + .GetEvent() + .OneTimeSubscribe( + "test", + _ => + { + s_val = true; + } + ); + return subscriptionToken; + } + + [Test] + public async Task Onetime_Sync() + { + var services = new ServiceCollection(); + var exceptionHandler = new TopLevelExceptionHandler( + Create>().Object, + Create().Object + ); + services.AddSingleton(Create().Object); + services.AddSingleton(exceptionHandler); + services.AddTransient(); + services.AddSingleton(); + var serviceProvider = services.BuildServiceProvider(); + + var subscriptionToken = Test_Onetime_Sub_Sync(serviceProvider); + var eventAggregator = serviceProvider.GetRequiredService(); + + GC.Collect(); + GC.WaitForPendingFinalizers(); + subscriptionToken.IsActive.Should().BeTrue(); + + await eventAggregator.GetEvent().PublishAsync(new object()); + + GC.Collect(); + GC.WaitForPendingFinalizers(); + subscriptionToken.IsActive.Should().BeFalse(); + s_val.Should().BeTrue(); + eventAggregator.GetEvent().Unsubscribe(subscriptionToken); + GC.Collect(); + GC.WaitForPendingFinalizers(); + subscriptionToken.IsActive.Should().BeFalse(); + } + + private static bool s_val; + + [Test] + public async Task Sub_WeakReference() + { + s_val = false; + var services = new ServiceCollection(); + var exceptionHandler = new TopLevelExceptionHandler( + Create>().Object, + Create().Object + ); + services.AddSingleton(Create().Object); + services.AddSingleton(exceptionHandler); + services.AddTransient(); + services.AddSingleton(); + var serviceProvider = services.BuildServiceProvider(); + TestWeakReference(serviceProvider); + + s_val.Should().BeFalse(); + + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + + await serviceProvider.GetRequiredService().GetEvent().PublishAsync(new object()); + s_val.Should().BeFalse(); + } + + //keep in a separate method to avoid referencing ObjectWithAction + private static void TestWeakReference(IServiceProvider serviceProvider) + { + var x = new ObjectWithAction( + new Action(() => + { + s_val = true; + }) + ); + var eventAggregator = serviceProvider.GetRequiredService(); + eventAggregator.GetEvent().Subscribe(x.Test1); + x = null; + } + + private sealed class ObjectWithAction(Action action) + { + public void Test1(object _) + { + action(); + } + } +} diff --git a/DUI3/Speckle.Connectors.DUI/Bindings/OperationProgressManager.cs b/DUI3/Speckle.Connectors.DUI/Bindings/OperationProgressManager.cs index 3724eed44..d5923dc30 100644 --- a/DUI3/Speckle.Connectors.DUI/Bindings/OperationProgressManager.cs +++ b/DUI3/Speckle.Connectors.DUI/Bindings/OperationProgressManager.cs @@ -1,4 +1,4 @@ -using System.Collections.Concurrent; +using System.Collections.Concurrent; using Speckle.Connectors.Common.Operations; using Speckle.Connectors.DUI.Bridge; using Speckle.Connectors.DUI.Models.Card; diff --git a/DUI3/Speckle.Connectors.DUI/Bindings/TopLevelExceptionHandlerBinding.cs b/DUI3/Speckle.Connectors.DUI/Bindings/TopLevelExceptionHandlerBinding.cs index 617722d69..2fec4a7ca 100644 --- a/DUI3/Speckle.Connectors.DUI/Bindings/TopLevelExceptionHandlerBinding.cs +++ b/DUI3/Speckle.Connectors.DUI/Bindings/TopLevelExceptionHandlerBinding.cs @@ -2,10 +2,6 @@ namespace Speckle.Connectors.DUI.Bindings; -/// -/// Simple binding that can be injected into non- services to get access to the -/// -/// public sealed class TopLevelExceptionHandlerBinding(IBrowserBridge parent) : IBinding { public string Name => "topLevelExceptionHandlerBinding"; diff --git a/DUI3/Speckle.Connectors.DUI/Bridge/BrowserBridge.cs b/DUI3/Speckle.Connectors.DUI/Bridge/BrowserBridge.cs index f692e41c3..3ecaefd26 100644 --- a/DUI3/Speckle.Connectors.DUI/Bridge/BrowserBridge.cs +++ b/DUI3/Speckle.Connectors.DUI/Bridge/BrowserBridge.cs @@ -6,6 +6,7 @@ using Microsoft.Extensions.Logging; using Speckle.Connectors.Common.Threading; using Speckle.Connectors.DUI.Bindings; +using Speckle.Connectors.DUI.Eventing; using Speckle.Connectors.DUI.Utils; using Speckle.Newtonsoft.Json; using Speckle.Sdk.Common; @@ -27,7 +28,8 @@ public sealed class BrowserBridge : IBrowserBridge /// private readonly ConcurrentDictionary _resultsStore = new(); - public ITopLevelExceptionHandler TopLevelExceptionHandler { get; } + + private readonly ITopLevelExceptionHandler _topLevelExceptionHandler; private readonly IThreadContext _threadContext; private readonly IThreadOptions _threadOptions; @@ -60,18 +62,38 @@ public BrowserBridge( IThreadContext threadContext, IJsonSerializer jsonSerializer, ILogger logger, - ILogger topLogger, IBrowserScriptExecutor browserScriptExecutor, - IThreadOptions threadOptions + IThreadOptions threadOptions, + IEventAggregator eventAggregator, + ITopLevelExceptionHandler topLevelExceptionHandler ) { _threadContext = threadContext; _jsonSerializer = jsonSerializer; _logger = logger; - TopLevelExceptionHandler = new TopLevelExceptionHandler(topLogger, this); // Capture the main thread's SynchronizationContext _browserScriptExecutor = browserScriptExecutor; _threadOptions = threadOptions; + _topLevelExceptionHandler = topLevelExceptionHandler; + eventAggregator + .GetEvent() + .Subscribe( + async ex => + { + await Send( + BasicConnectorBindingCommands.SET_GLOBAL_NOTIFICATION, + new + { + type = ToastNotificationType.DANGER, + title = "Unhandled Exception Occurred", + description = ex.ToFormattedString(), + autoClose = false + } + ) + .ConfigureAwait(false); + }, + ThreadOption.MainThread + ); } public void AssociateWithBinding(IBinding binding) @@ -110,12 +132,14 @@ public void RunMethod(string methodName, string requestId, string methodArgs) => .RunOnThreadAsync( async () => { - var task = await TopLevelExceptionHandler.CatchUnhandledAsync(async () => - { - var result = await ExecuteMethod(methodName, methodArgs); - string resultJson = _jsonSerializer.Serialize(result); - NotifyUIMethodCallResultReady(requestId, resultJson); - }); + var task = await _topLevelExceptionHandler + .CatchUnhandledAsync(async () => + { + var result = await ExecuteMethod(methodName, methodArgs).ConfigureAwait(false); + string resultJson = _jsonSerializer.Serialize(result); + NotifyUIMethodCallResultReady(requestId, resultJson); + }) + .ConfigureAwait(false); if (task.Exception is not null) { string resultJson = SerializeFormattedException(task.Exception); diff --git a/DUI3/Speckle.Connectors.DUI/Bridge/IBrowserBridge.cs b/DUI3/Speckle.Connectors.DUI/Bridge/IBrowserBridge.cs index 65ec766df..20c85708f 100644 --- a/DUI3/Speckle.Connectors.DUI/Bridge/IBrowserBridge.cs +++ b/DUI3/Speckle.Connectors.DUI/Bridge/IBrowserBridge.cs @@ -42,5 +42,4 @@ public Task Send(string eventName, T data, CancellationToken cancellationToke public void Send2(string eventName, T data) where T : class; - public ITopLevelExceptionHandler TopLevelExceptionHandler { get; } } diff --git a/DUI3/Speckle.Connectors.DUI/Bridge/TopLevelExceptionHandler.cs b/DUI3/Speckle.Connectors.DUI/Bridge/TopLevelExceptionHandler.cs index 1684e3c81..7699552a8 100644 --- a/DUI3/Speckle.Connectors.DUI/Bridge/TopLevelExceptionHandler.cs +++ b/DUI3/Speckle.Connectors.DUI/Bridge/TopLevelExceptionHandler.cs @@ -1,8 +1,7 @@ using Microsoft.Extensions.Logging; -using Speckle.Connectors.DUI.Bindings; +using Speckle.Connectors.DUI.Eventing; using Speckle.InterfaceGenerator; using Speckle.Sdk; -using Speckle.Sdk.Models.Extensions; namespace Speckle.Connectors.DUI.Bridge; @@ -23,15 +22,15 @@ namespace Speckle.Connectors.DUI.Bridge; public sealed class TopLevelExceptionHandler : ITopLevelExceptionHandler { private readonly ILogger _logger; - public IBrowserBridge Parent { get; } + private readonly IEventAggregator _eventAggregator; public string Name => nameof(TopLevelExceptionHandler); private const string UNHANDLED_LOGGER_TEMPLATE = "An unhandled Exception occured"; - internal TopLevelExceptionHandler(ILogger logger, IBrowserBridge bridge) + public TopLevelExceptionHandler(ILogger logger, IEventAggregator eventAggregator) { _logger = logger; - Parent = bridge; + _eventAggregator = eventAggregator; } /// @@ -41,43 +40,34 @@ internal TopLevelExceptionHandler(ILogger logger, IBro /// The function to invoke and provide error handling for /// will be rethrown, these should be allowed to bubble up to the host app /// - public void CatchUnhandled(Action function) + public Result CatchUnhandled(Action function) { - _ = CatchUnhandled(() => + var r = CatchUnhandled(() => { function(); - return null; + return true; }); + if (r.IsSuccess) + { + return new Result(); + } + return new Result(r.Exception); } /// /// return type /// A result pattern struct (where exceptions have been handled) - public Result CatchUnhandled(Func function) => - CatchUnhandledAsync(() => Task.FromResult(function.Invoke())).Result; //Safe to do a .Result because this as an already completed and non-async Task from the Task.FromResult - - /// - /// A result pattern struct (where exceptions have been handled) - public async Task CatchUnhandledAsync(Func function) + public Result CatchUnhandled(Func function) { try { - try - { - await function(); - return new Result(); - } - catch (Exception ex) when (!ex.IsFatal()) - { - _logger.LogError(ex, UNHANDLED_LOGGER_TEMPLATE); - await SetGlobalNotification( - ToastNotificationType.DANGER, - "Unhandled Exception Occured", - ex.ToFormattedString(), - false - ); - return new(ex); - } + return new Result(function()); + } + catch (Exception ex) when (!ex.IsFatal()) + { + _logger.LogError(ex, UNHANDLED_LOGGER_TEMPLATE); + _eventAggregator.GetEvent().PublishAsync(ex).Wait(); + return new(ex); } catch (Exception ex) { @@ -86,6 +76,22 @@ await SetGlobalNotification( } } + /// + /// A result pattern struct (where exceptions have been handled) + public async Task CatchUnhandledAsync(Func function) + { + var r = await CatchUnhandledAsync(async () => + { + await function(); + return true; + }); + if (r.IsSuccess) + { + return new Result(); + } + return new Result(r.Exception); + } + /// public async Task> CatchUnhandledAsync(Func> function) { @@ -97,7 +103,8 @@ public async Task> CatchUnhandledAsync(Func> function) } catch (Exception ex) when (!ex.IsFatal()) { - await HandleException(ex); + _logger.LogError(ex, UNHANDLED_LOGGER_TEMPLATE); + await _eventAggregator.GetEvent().PublishAsync(ex); return new(ex); } } @@ -108,32 +115,6 @@ public async Task> CatchUnhandledAsync(Func> function) } } - private async Task HandleException(Exception ex) - { - _logger.LogError(ex, UNHANDLED_LOGGER_TEMPLATE); - - try - { - await SetGlobalNotification( - ToastNotificationType.DANGER, - "Unhandled Exception Occured", - ex.ToFormattedString(), - false - ); - } - catch (Exception toastEx) - { - // Not only was a top level exception caught, but our attempt to display a toast failed! - // Toasts can fail if the BrowserBridge is not yet associated with a binding - // For this reason, binding authors should avoid doing anything in - // the constructors of bindings that may try and use the bridge! - AggregateException aggregateException = - new("An Unhandled top level exception was caught, and the toast failed to display it!", [toastEx, ex]); - - throw aggregateException; - } - } - /// /// Triggers an async action without explicitly needing to await it.
/// Any thrown by invoking will be handled by the
@@ -143,17 +124,5 @@ await SetGlobalNotification( /// In cases where you can use keyword, you should prefer using /// /// - public async void FireAndForget(Func function) => await CatchUnhandledAsync(function); - - private async Task SetGlobalNotification(ToastNotificationType type, string title, string message, bool autoClose) => - await Parent.Send( - BasicConnectorBindingCommands.SET_GLOBAL_NOTIFICATION, //TODO: We could move these constants into a DUI3 constants static class - new - { - type, - title, - description = message, - autoClose - } - ); + public async void FireAndForget(Func function) => await CatchUnhandledAsync(function).ConfigureAwait(false); } diff --git a/DUI3/Speckle.Connectors.DUI/ContainerRegistration.cs b/DUI3/Speckle.Connectors.DUI/ContainerRegistration.cs index ce594de8a..dbda73c28 100644 --- a/DUI3/Speckle.Connectors.DUI/ContainerRegistration.cs +++ b/DUI3/Speckle.Connectors.DUI/ContainerRegistration.cs @@ -1,8 +1,11 @@ using System.Reflection; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging; using Speckle.Connectors.Common.Threading; using Speckle.Connectors.DUI.Bindings; using Speckle.Connectors.DUI.Bridge; +using Speckle.Connectors.DUI.Eventing; using Speckle.Connectors.DUI.Models; using Speckle.Sdk; using Speckle.Sdk.Transports; @@ -23,16 +26,45 @@ public static void AddDUI(this IServiceCollectio serviceCollection.AddMatchingInterfacesAsTransient(Assembly.GetAssembly(typeof(IdleCallManager))); serviceCollection.AddMatchingInterfacesAsTransient(Assembly.GetAssembly(typeof(IServerTransportFactory))); - } + serviceCollection.AddEventsAsTransient(Assembly.GetAssembly(typeof(TDocumentStore))); + serviceCollection.AddEventsAsTransient(Assembly.GetAssembly(typeof(IdleCallManager))); + serviceCollection.AddSingleton(); - public static void RegisterTopLevelExceptionHandler(this IServiceCollection serviceCollection) - { serviceCollection.AddSingleton(sp => sp.GetRequiredService() ); serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(c => - c.GetRequiredService().Parent.TopLevelExceptionHandler - ); + serviceCollection.AddSingleton(); + serviceCollection.AddTransient(); + } + + public static IServiceCollection AddEventsAsTransient(this IServiceCollection serviceCollection, Assembly assembly) + { + foreach (var type in assembly.ExportedTypes.Where(t => t.IsNonAbstractClass())) + { + if (type.FindInterfaces((i, _) => i == typeof(ISpeckleEvent), null).Length != 0) + { + serviceCollection.TryAddTransient(type); + } + } + + return serviceCollection; + } + + public static IServiceProvider UseDUI(this IServiceProvider serviceProvider) + { + //observe the unobserved! + TaskScheduler.UnobservedTaskException += async (_, args) => + { + await serviceProvider + .GetRequiredService() + .GetEvent() + .PublishAsync(args.Exception); + serviceProvider.GetRequiredService().LogError(args.Exception, "Unobserved task exception"); + args.SetObserved(); + }; + + serviceProvider.GetRequiredService().OnDocumentStoreInitialized().Wait(); + return serviceProvider; } } diff --git a/DUI3/Speckle.Connectors.DUI/Eventing/DelegateReference.cs b/DUI3/Speckle.Connectors.DUI/Eventing/DelegateReference.cs new file mode 100644 index 000000000..c268769f8 --- /dev/null +++ b/DUI3/Speckle.Connectors.DUI/Eventing/DelegateReference.cs @@ -0,0 +1,73 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using Speckle.Sdk.Common; + +namespace Speckle.Connectors.DUI.Eventing; + +public class DelegateReference +{ + private readonly WeakOrStrongReference? _weakReference; + private readonly MethodInfo _method; + private readonly Type? _delegateType; + + public DelegateReference(Delegate @delegate, EventFeatures features) + { + var target = @delegate.Target; + _method = @delegate.Method; + if (target != null) + { + //anonymous methods are always strong....should we do this? - doing a brief search says yes + if ( + features.HasFlag(EventFeatures.ForceStrongReference) + || Attribute.IsDefined(_method.DeclaringType.NotNull(), typeof(CompilerGeneratedAttribute)) + ) + { + _weakReference = WeakOrStrongReference.CreateStrong(target); + } + else + { + _weakReference = WeakOrStrongReference.CreateWeak(target); + } + + var messageType = @delegate.Method.GetParameters()[0].ParameterType; + if (features.HasFlag(EventFeatures.IsAsync)) + { + _delegateType = typeof(Func<,>).MakeGenericType(messageType, typeof(Task)); + } + else + { + _delegateType = typeof(Action<>).MakeGenericType(messageType); + } + } + else + { + _weakReference = null; + } + } + + public bool IsAlive => _weakReference == null || _weakReference.IsAlive; + + public async Task Invoke(object message) + { + if (!IsAlive) + { + return false; + } + + object? target = null; + if (_weakReference != null) + { + target = _weakReference.Target; + } + var method = Delegate.CreateDelegate(_delegateType.NotNull(), target, _method); + + var task = method.DynamicInvoke(message) as Task; + + if (task is not null) + { + await task; + } + + return true; + } +} diff --git a/DUI3/Speckle.Connectors.DUI/Eventing/EventAggregator.cs b/DUI3/Speckle.Connectors.DUI/Eventing/EventAggregator.cs new file mode 100644 index 000000000..eea2f87ac --- /dev/null +++ b/DUI3/Speckle.Connectors.DUI/Eventing/EventAggregator.cs @@ -0,0 +1,31 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace Speckle.Connectors.DUI.Eventing; + +public interface IEventAggregator +{ + TEventType GetEvent() + where TEventType : EventBase; +} + +//based on Prism.Events at verison 8 +// which was MIT https://github.com/PrismLibrary/Prism/tree/952e343f585b068ccb7d3478d3982485253a0508/src/Prism.Events +// License https://github.com/PrismLibrary/Prism/blob/952e343f585b068ccb7d3478d3982485253a0508/LICENSE +public class EventAggregator(IServiceProvider serviceProvider) : IEventAggregator +{ + private readonly Dictionary _events = new(); + + public TEventType GetEvent() + where TEventType : EventBase + { + lock (_events) + { + if (!_events.TryGetValue(typeof(TEventType), out var existingEvent)) + { + existingEvent = (TEventType)serviceProvider.GetRequiredService(typeof(TEventType)); + _events[typeof(TEventType)] = existingEvent; + } + return (TEventType)existingEvent; + } + } +} diff --git a/DUI3/Speckle.Connectors.DUI/Eventing/EventBase.cs b/DUI3/Speckle.Connectors.DUI/Eventing/EventBase.cs new file mode 100644 index 000000000..09911fbb0 --- /dev/null +++ b/DUI3/Speckle.Connectors.DUI/Eventing/EventBase.cs @@ -0,0 +1,84 @@ +namespace Speckle.Connectors.DUI.Eventing; + +/// +/// Defines a base class to publish and subscribe to events. +/// +public abstract class EventBase +{ + private readonly List _subscriptions = new(); + + protected SubscriptionToken InternalSubscribe(IEventSubscription eventSubscription) + { + lock (_subscriptions) + { + _subscriptions.Add(eventSubscription); + } + return eventSubscription.SubscriptionToken; + } + + protected async Task InternalPublish(params object[] arguments) + { + var executionStrategies = PruneAndReturnStrategies(); + foreach (var executionStrategy in executionStrategies) + { + await executionStrategy(arguments); + } + } + + private IEnumerable> PruneAndReturnStrategies() + { + lock (_subscriptions) + { + for (var i = _subscriptions.Count - 1; i >= 0; i--) + { + var listItem = _subscriptions[i].GetExecutionStrategy(); + + if (listItem == null) + { + // Prune from main list. Log? + _subscriptions.RemoveAt(i); + } + else + { + yield return listItem; + } + } + } + } + + public void Unsubscribe(SubscriptionToken token) + { + lock (_subscriptions) + { + IEventSubscription? subscription = _subscriptions.FirstOrDefault(evt => evt.SubscriptionToken.Equals(token)); + if (subscription != null) + { + _subscriptions.Remove(subscription); + token.Unsubscribe(); //calling dispose is circular + } + } + } + + public bool Contains(SubscriptionToken token) + { + lock (_subscriptions) + { + IEventSubscription subscription = _subscriptions.FirstOrDefault(evt => evt.SubscriptionToken == token); + return subscription != null; + } + } + + public void Prune() + { + lock (_subscriptions) + { + for (var i = _subscriptions.Count - 1; i >= 0; i--) + { + if (_subscriptions[i].GetExecutionStrategy() == null) + { + _subscriptions.RemoveAt(i); + } + } + } + } +} diff --git a/DUI3/Speckle.Connectors.DUI/Eventing/EventFeatures.cs b/DUI3/Speckle.Connectors.DUI/Eventing/EventFeatures.cs new file mode 100644 index 000000000..95b9cb6bc --- /dev/null +++ b/DUI3/Speckle.Connectors.DUI/Eventing/EventFeatures.cs @@ -0,0 +1,11 @@ +// ReSharper disable InconsistentNaming +namespace Speckle.Connectors.DUI.Eventing; + +[Flags] +public enum EventFeatures +{ + None = 0, + OneTime = 1, + IsAsync = 2, + ForceStrongReference = 4 +} diff --git a/DUI3/Speckle.Connectors.DUI/Eventing/EventSubscription.cs b/DUI3/Speckle.Connectors.DUI/Eventing/EventSubscription.cs new file mode 100644 index 000000000..a05fde4d5 --- /dev/null +++ b/DUI3/Speckle.Connectors.DUI/Eventing/EventSubscription.cs @@ -0,0 +1,56 @@ +using Speckle.Connectors.Common.Threading; +using Speckle.Connectors.DUI.Bridge; + +namespace Speckle.Connectors.DUI.Eventing; + +public class EventSubscription( + DelegateReference actionReference, + IThreadContext threadContext, + ITopLevelExceptionHandler exceptionHandler, + SubscriptionToken token, + ThreadOption threadOption, + EventFeatures features +) : IEventSubscription + where TPayload : notnull +{ + public SubscriptionToken SubscriptionToken => token; + + public Func? GetExecutionStrategy() + { + if (!actionReference.IsAlive) + { + return null; + } + return async arguments => + { + TPayload argument = (TPayload)arguments[0]; + await InvokeAction(argument); + }; + } + + private async Task InvokeAction(TPayload argument) + { + switch (threadOption) + { + case ThreadOption.MainThread: + await threadContext.RunOnMainAsync(() => Invoke(argument)); + break; + case ThreadOption.WorkerThread: + await threadContext.RunOnWorkerAsync(() => Invoke(argument)); + break; + case ThreadOption.PublisherThread: + default: + await Invoke(argument); + break; + } + } + + private async Task Invoke(TPayload argument) + { + await exceptionHandler.CatchUnhandledAsync(() => actionReference.Invoke(argument)); + if (features.HasFlag(EventFeatures.OneTime)) + { + SubscriptionToken.Dispose(); + } + } +} diff --git a/DUI3/Speckle.Connectors.DUI/Eventing/Events.cs b/DUI3/Speckle.Connectors.DUI/Eventing/Events.cs new file mode 100644 index 000000000..61354309f --- /dev/null +++ b/DUI3/Speckle.Connectors.DUI/Eventing/Events.cs @@ -0,0 +1,13 @@ +using Speckle.Connectors.Common.Threading; +using Speckle.Connectors.DUI.Bridge; + +namespace Speckle.Connectors.DUI.Eventing; + +public class ExceptionEvent(IThreadContext threadContext, ITopLevelExceptionHandler exceptionHandler) + : ThreadedEvent(threadContext, exceptionHandler); + +public class DocumentChangedEvent(IThreadContext threadContext, ITopLevelExceptionHandler exceptionHandler) + : ThreadedEvent(threadContext, exceptionHandler); + +public class IdleEvent(IThreadContext threadContext, ITopLevelExceptionHandler exceptionHandler) + : OneTimeThreadedEvent(threadContext, exceptionHandler); diff --git a/DUI3/Speckle.Connectors.DUI/Eventing/IEventSubscription.cs b/DUI3/Speckle.Connectors.DUI/Eventing/IEventSubscription.cs new file mode 100644 index 000000000..64286c1d9 --- /dev/null +++ b/DUI3/Speckle.Connectors.DUI/Eventing/IEventSubscription.cs @@ -0,0 +1,8 @@ +namespace Speckle.Connectors.DUI.Eventing; + +public interface IEventSubscription +{ + SubscriptionToken SubscriptionToken { get; } + + Func? GetExecutionStrategy(); +} diff --git a/DUI3/Speckle.Connectors.DUI/Eventing/ISpeckleEvent.cs b/DUI3/Speckle.Connectors.DUI/Eventing/ISpeckleEvent.cs new file mode 100644 index 000000000..0d8e00cc1 --- /dev/null +++ b/DUI3/Speckle.Connectors.DUI/Eventing/ISpeckleEvent.cs @@ -0,0 +1,6 @@ +namespace Speckle.Connectors.DUI.Eventing; + +public interface ISpeckleEvent +{ + string Name { get; } +} diff --git a/DUI3/Speckle.Connectors.DUI/Eventing/OneTimeThreadedEvent.cs b/DUI3/Speckle.Connectors.DUI/Eventing/OneTimeThreadedEvent.cs new file mode 100644 index 000000000..75e41531b --- /dev/null +++ b/DUI3/Speckle.Connectors.DUI/Eventing/OneTimeThreadedEvent.cs @@ -0,0 +1,102 @@ +using Speckle.Connectors.Common.Threading; +using Speckle.Connectors.DUI.Bridge; + +namespace Speckle.Connectors.DUI.Eventing; + +public abstract class OneTimeThreadedEvent(IThreadContext threadContext, ITopLevelExceptionHandler exceptionHandler) + : SpeckleEvent(threadContext, exceptionHandler), + IDisposable + where T : notnull +{ + private readonly SemaphoreSlim _semaphore = new(1, 1); + private readonly Dictionary _activeTokens = new(); + + protected virtual void Dispose(bool isDisposing) + { + if (isDisposing) + { + _semaphore.Dispose(); + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + ~OneTimeThreadedEvent() => Dispose(false); + + public SubscriptionToken OneTimeSubscribe( + string id, + Func action, + ThreadOption threadOption = ThreadOption.PublisherThread + ) + { + _semaphore.Wait(); + try + { + if (_activeTokens.TryGetValue(id, out var token)) + { + if (token.IsActive) + { + return token; + } + _activeTokens.Remove(id); + } + token = Subscribe(action, threadOption, EventFeatures.OneTime); + _activeTokens.Add(id, token); + return token; + } + finally + { + _semaphore.Release(); + } + } + + public SubscriptionToken OneTimeSubscribe( + string id, + Action action, + ThreadOption threadOption = ThreadOption.PublisherThread + ) + { + _semaphore.Wait(); + try + { + if (_activeTokens.TryGetValue(id, out var token)) + { + if (token.IsActive) + { + return token; + } + _activeTokens.Remove(id); + } + token = Subscribe(action, threadOption, EventFeatures.OneTime); + _activeTokens.Add(id, token); + return token; + } + finally + { + _semaphore.Release(); + } + } + + public override async Task PublishAsync(T payload) + { + await _semaphore.WaitAsync(); + try + { + await base.PublishAsync(payload); + foreach (var token in _activeTokens.Values) + { + token.Dispose(); + } + + _activeTokens.Clear(); + } + finally + { + _semaphore.Release(); + } + } +} diff --git a/DUI3/Speckle.Connectors.DUI/Eventing/SpeckleEvent.cs b/DUI3/Speckle.Connectors.DUI/Eventing/SpeckleEvent.cs new file mode 100644 index 000000000..9414c00dd --- /dev/null +++ b/DUI3/Speckle.Connectors.DUI/Eventing/SpeckleEvent.cs @@ -0,0 +1,40 @@ +using System.Diagnostics.CodeAnalysis; +using Speckle.Connectors.Common.Threading; +using Speckle.Connectors.DUI.Bridge; + +namespace Speckle.Connectors.DUI.Eventing; + +public abstract class SpeckleEvent(IThreadContext threadContext, ITopLevelExceptionHandler exceptionHandler) + : EventBase, + ISpeckleEvent + where T : notnull +{ + public string Name { get; } = typeof(T).Name; + + public virtual Task PublishAsync(T payload) => InternalPublish(payload); + + protected SubscriptionToken Subscribe(Func action, ThreadOption threadOption, EventFeatures features) + { + features |= EventFeatures.IsAsync; + var actionReference = new DelegateReference(action, features); + return Subscribe(actionReference, threadOption, features); + } + + protected SubscriptionToken Subscribe(Action action, ThreadOption threadOption, EventFeatures features) + { + var actionReference = new DelegateReference(action, features); + return Subscribe(actionReference, threadOption, features); + } + + [SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope")] + private SubscriptionToken Subscribe( + DelegateReference actionReference, + ThreadOption threadOption, + EventFeatures features + ) + { + EventSubscription subscription = + new(actionReference, threadContext, exceptionHandler, new(Unsubscribe), threadOption, features); + return InternalSubscribe(subscription); + } +} diff --git a/DUI3/Speckle.Connectors.DUI/Eventing/SubscriptionToken.cs b/DUI3/Speckle.Connectors.DUI/Eventing/SubscriptionToken.cs new file mode 100644 index 000000000..72ba66afc --- /dev/null +++ b/DUI3/Speckle.Connectors.DUI/Eventing/SubscriptionToken.cs @@ -0,0 +1,58 @@ +namespace Speckle.Connectors.DUI.Eventing; + +//based on Prism.Events +public sealed class SubscriptionToken(Action unsubscribeAction) + : IEquatable, + IDisposable +{ + private readonly Guid _token = Guid.NewGuid(); + private Action? _unsubscribeAction = unsubscribeAction; + + public bool Equals(SubscriptionToken? other) + { + if (other == null) + { + return false; + } + + return Equals(_token, other._token); + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(this, obj)) + { + return true; + } + + return Equals(obj as SubscriptionToken); + } + + public override int GetHashCode() => _token.GetHashCode(); + + public bool IsActive => _unsubscribeAction != null; + + public void Unsubscribe() => _unsubscribeAction = null; + + ~SubscriptionToken() => Dispose(false); + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool isDisposing) + { + // While the SubscriptionToken class implements IDisposable, in the case of weak subscriptions + // (i.e. keepSubscriberReferenceAlive set to false in the Subscribe method) it's not necessary to unsubscribe, + // as no resources should be kept alive by the event subscription. + // In such cases, if a warning is issued, it could be suppressed. + + if (isDisposing && _unsubscribeAction != null) + { + _unsubscribeAction(this); + Unsubscribe(); + } + } +} diff --git a/DUI3/Speckle.Connectors.DUI/Eventing/ThreadOption.cs b/DUI3/Speckle.Connectors.DUI/Eventing/ThreadOption.cs new file mode 100644 index 000000000..6866be71e --- /dev/null +++ b/DUI3/Speckle.Connectors.DUI/Eventing/ThreadOption.cs @@ -0,0 +1,8 @@ +namespace Speckle.Connectors.DUI.Eventing; + +public enum ThreadOption +{ + PublisherThread, + MainThread, + WorkerThread +} diff --git a/DUI3/Speckle.Connectors.DUI/Eventing/ThreadedEvent.cs b/DUI3/Speckle.Connectors.DUI/Eventing/ThreadedEvent.cs new file mode 100644 index 000000000..95f714191 --- /dev/null +++ b/DUI3/Speckle.Connectors.DUI/Eventing/ThreadedEvent.cs @@ -0,0 +1,15 @@ +using Speckle.Connectors.Common.Threading; +using Speckle.Connectors.DUI.Bridge; + +namespace Speckle.Connectors.DUI.Eventing; + +public abstract class ThreadedEvent(IThreadContext threadContext, ITopLevelExceptionHandler exceptionHandler) + : SpeckleEvent(threadContext, exceptionHandler) + where T : notnull +{ + public SubscriptionToken Subscribe(Func action, ThreadOption threadOption = ThreadOption.PublisherThread) => + Subscribe(action, threadOption, EventFeatures.None); + + public SubscriptionToken Subscribe(Action action, ThreadOption threadOption = ThreadOption.PublisherThread) => + Subscribe(action, threadOption, EventFeatures.None); +} diff --git a/DUI3/Speckle.Connectors.DUI/Eventing/WeakOrStrongReference.cs b/DUI3/Speckle.Connectors.DUI/Eventing/WeakOrStrongReference.cs new file mode 100644 index 000000000..98d20ea20 --- /dev/null +++ b/DUI3/Speckle.Connectors.DUI/Eventing/WeakOrStrongReference.cs @@ -0,0 +1,24 @@ +using Speckle.Sdk.Common; + +namespace Speckle.Connectors.DUI.Eventing; + +public class WeakOrStrongReference(WeakReference? weakReference, object? strongReference) +{ + public static WeakOrStrongReference CreateWeak(object? reference) => new(new WeakReference(reference), null); + + public static WeakOrStrongReference CreateStrong(object? reference) => new(null, reference); + + public bool IsAlive => weakReference?.IsAlive ?? true; + + public object Target + { + get + { + if (strongReference is not null) + { + return strongReference; + } + return (weakReference?.Target).NotNull(); + } + } +} diff --git a/DUI3/Speckle.Connectors.DUI/Models/DocumentModelStore.cs b/DUI3/Speckle.Connectors.DUI/Models/DocumentModelStore.cs index 62c4a7984..b42cd2a0e 100644 --- a/DUI3/Speckle.Connectors.DUI/Models/DocumentModelStore.cs +++ b/DUI3/Speckle.Connectors.DUI/Models/DocumentModelStore.cs @@ -13,12 +13,6 @@ public abstract class DocumentModelStore(IJsonSerializer serializer) { private readonly List _models = new(); - /// - /// This event is triggered by each specific host app implementation of the document model store. - /// - // POC: unsure about the PublicAPI annotation, unsure if this changed handle should live here on the store... :/ - public event EventHandler? DocumentChanged; - //needed for javascript UI public IReadOnlyList Models { @@ -31,6 +25,8 @@ public IReadOnlyList Models } } + public virtual Task OnDocumentStoreInitialized() => Task.CompletedTask; + public virtual bool IsDocumentInit { get; set; } // TODO: not sure about this, throwing an exception, needs some thought... @@ -88,8 +84,6 @@ public void RemoveModel(ModelCard model) } } - protected void OnDocumentChanged() => DocumentChanged?.Invoke(this, EventArgs.Empty); - public IEnumerable GetSenders() { lock (_models) diff --git a/Sdk/Speckle.Connectors.Common/Builders/IRootObjectBuilder.cs b/Sdk/Speckle.Connectors.Common/Builders/IRootObjectBuilder.cs index f1d9acff4..1a728fb44 100644 --- a/Sdk/Speckle.Connectors.Common/Builders/IRootObjectBuilder.cs +++ b/Sdk/Speckle.Connectors.Common/Builders/IRootObjectBuilder.cs @@ -1,4 +1,4 @@ -using Speckle.Connectors.Common.Conversion; +using Speckle.Connectors.Common.Conversion; using Speckle.Connectors.Common.Operations; using Speckle.Sdk.Models; diff --git a/Sdk/Speckle.Connectors.Common/Operations/SendOperation.cs b/Sdk/Speckle.Connectors.Common/Operations/SendOperation.cs index c131d0058..357e82566 100644 --- a/Sdk/Speckle.Connectors.Common/Operations/SendOperation.cs +++ b/Sdk/Speckle.Connectors.Common/Operations/SendOperation.cs @@ -48,7 +48,7 @@ public async Task Execute( return new(rootObjId, convertedReferences, buildResult.ConversionResults); } - public async Task Send( + private async Task Send( Base commitObject, SendInfo sendInfo, IProgress onOperationProgressed, diff --git a/Sdk/Speckle.Connectors.Common/Threading/DefaultThreadContext.cs b/Sdk/Speckle.Connectors.Common/Threading/DefaultThreadContext.cs index 1a56531ec..92c22902e 100644 --- a/Sdk/Speckle.Connectors.Common/Threading/DefaultThreadContext.cs +++ b/Sdk/Speckle.Connectors.Common/Threading/DefaultThreadContext.cs @@ -1,4 +1,4 @@ -namespace Speckle.Connectors.Common.Threading; +namespace Speckle.Connectors.Common.Threading; public class DefaultThreadContext : ThreadContext { diff --git a/Sdk/Speckle.Connectors.Common/Threading/ThreadContextExtensions.cs b/Sdk/Speckle.Connectors.Common/Threading/ThreadContextExtensions.cs index b6dbf2c59..d0cb56029 100644 --- a/Sdk/Speckle.Connectors.Common/Threading/ThreadContextExtensions.cs +++ b/Sdk/Speckle.Connectors.Common/Threading/ThreadContextExtensions.cs @@ -1,4 +1,4 @@ -namespace Speckle.Connectors.Common.Threading; +namespace Speckle.Connectors.Common.Threading; public static class ThreadContextExtensions { diff --git a/Speckle.Connectors.sln b/Speckle.Connectors.sln index a767365d0..1e177afbc 100644 --- a/Speckle.Connectors.sln +++ b/Speckle.Connectors.sln @@ -14,8 +14,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Config", "Config", "{85A13E README.md = README.md EndProjectSection EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "DUI", "DUI", "{FD4D6594-D81E-456F-8F2E-35B09E04A755}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Revit", "Revit", "{D92751C8-1039-4005-90B2-913E55E0B8BD}" ProjectSection(SolutionItems) = preProject Connectors\Revit\Directory.Build.targets = Connectors\Revit\Directory.Build.targets @@ -692,7 +690,7 @@ Global {DC570FFF-6FE5-47BD-8BC1-B471A6067786} = {FC224610-32D3-454E-9BC1-1219FE8ACD5F} {E1C43415-3200-45F4-8BF9-A4DD7D7F2ED6} = {FC224610-32D3-454E-9BC1-1219FE8ACD5F} {26391930-F86F-47E0-A5F6-B89919E38CE1} = {E9DEBA00-50A4-485D-BA65-D8AB3E3467AB} - {D81C0B87-F0C1-4297-A147-02F001FB7E1E} = {FD4D6594-D81E-456F-8F2E-35B09E04A755} + {D81C0B87-F0C1-4297-A147-02F001FB7E1E} = {2E00592E-558D-492D-88F9-3ECEE4C0C7DA} {7291B93C-615D-42DE-B8C1-3F9DF643E0FC} = {2E00592E-558D-492D-88F9-3ECEE4C0C7DA} {8AEF06C0-CA5C-4460-BC2D-ADE5F35D0434} = {2E00592E-558D-492D-88F9-3ECEE4C0C7DA} {9584AEE5-CD59-46E6-93E6-2DC2B5285B75} = {42826721-9A18-4762-8BA9-F1429DD5C5B1} @@ -706,7 +704,7 @@ Global {804E065F-914C-414A-AF84-009312C3CFF6} = {42826721-9A18-4762-8BA9-F1429DD5C5B1} {9ADD1B7A-6401-4202-8613-F668E2FBC0A4} = {4721AA15-AF6E-4A62-A2C3-65564DC563E6} {631C295A-7CCF-4B42-8686-7034E31469E7} = {2D5AE63D-85C0-43D1-84BF-04418ED93F63} - {7420652C-3046-4F38-BE64-9B9E69D76FA2} = {FD4D6594-D81E-456F-8F2E-35B09E04A755} + {7420652C-3046-4F38-BE64-9B9E69D76FA2} = {2E00592E-558D-492D-88F9-3ECEE4C0C7DA} {C50AA3E3-8C31-4131-9DEC-1D8B377D5A89} = {59E8E8F3-4E42-4E92-83B3-B1C2AB901D18} {CA8EAE01-AB9F-4EC1-B6F3-73721487E9E1} = {2F45036E-D817-41E9-B82F-DBE013EC95D0} {35175682-DA83-4C0A-A49D-B191F5885D8E} = {4721AA15-AF6E-4A62-A2C3-65564DC563E6} @@ -733,7 +731,7 @@ Global {7F1FDCF2-0CE8-4119-B3C1-F2CC6D7E1C36} = {0AF38BA3-65A0-481B-8CBB-B82E406E1575} {19424B55-058C-4E9C-B86F-700AEF9EAEC3} = {0AF38BA3-65A0-481B-8CBB-B82E406E1575} {0AF38BA3-65A0-481B-8CBB-B82E406E1575} = {D92751C8-1039-4005-90B2-913E55E0B8BD} - {EB83A3A3-F9B6-4281-8EBF-F7289FB5D885} = {FD4D6594-D81E-456F-8F2E-35B09E04A755} + {EB83A3A3-F9B6-4281-8EBF-F7289FB5D885} = {2E00592E-558D-492D-88F9-3ECEE4C0C7DA} {D8069A23-AD2E-4C9E-8574-7E8C45296A46} = {0AF38BA3-65A0-481B-8CBB-B82E406E1575} {2D5AE63D-85C0-43D1-84BF-04418ED93F63} = {804E065F-914C-414A-AF84-009312C3CFF6} {2F45036E-D817-41E9-B82F-DBE013EC95D0} = {804E065F-914C-414A-AF84-009312C3CFF6} @@ -826,6 +824,7 @@ Global Converters\Civil3d\Speckle.Converters.Civil3dShared\Speckle.Converters.Civil3dShared.projitems*{25172c49-7aa4-4739-bb07-69785094c379}*SharedItemsImports = 5 Converters\Revit\Speckle.Converters.RevitShared\Speckle.Converters.RevitShared.projitems*{26391930-f86f-47e0-a5f6-b89919e38ce1}*SharedItemsImports = 5 Converters\Civil3d\Speckle.Converters.Civil3dShared\Speckle.Converters.Civil3dShared.projitems*{35175682-da83-4c0a-a49d-b191f5885d8e}*SharedItemsImports = 13 + Converters\CSi\Speckle.Converters.ETABSShared\Speckle.Converters.ETABSShared.projitems*{36377858-d696-4567-ab05-637f4ec841f5}*SharedItemsImports = 13 Connectors\Tekla\Speckle.Connector.TeklaShared\Speckle.Connectors.TeklaShared.projitems*{3ab9028b-b2d2-464b-9ba3-39c192441e50}*SharedItemsImports = 13 Connectors\Autocad\Speckle.Connectors.AutocadShared\Speckle.Connectors.AutocadShared.projitems*{41bc679f-887f-44cf-971d-a5502ee87db0}*SharedItemsImports = 13 Connectors\Autocad\Speckle.Connectors.AutocadShared\Speckle.Connectors.AutocadShared.projitems*{4459f2b1-a340-488e-a856-eb2ae9c72ad4}*SharedItemsImports = 5