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 1b3dcb613..2d54c184a 100644 --- a/Connectors/ArcGIS/Speckle.Connectors.ArcGIS3/Bindings/ArcGISSendBinding.cs +++ b/Connectors/ArcGIS/Speckle.Connectors.ArcGIS3/Bindings/ArcGISSendBinding.cs @@ -3,7 +3,6 @@ using ArcGIS.Core.Data; using ArcGIS.Desktop.Core; using ArcGIS.Desktop.Editing.Events; -using ArcGIS.Desktop.Framework.Threading.Tasks; using ArcGIS.Desktop.Mapping; using ArcGIS.Desktop.Mapping.Events; using Microsoft.Extensions.DependencyInjection; @@ -15,6 +14,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; @@ -67,7 +67,9 @@ public ArcGISSendBinding( IOperationProgressManager operationProgressManager, ILogger logger, IArcGISConversionSettingsFactory arcGisConversionSettingsFactory, - MapMembersUtils mapMemberUtils + ITopLevelExceptionHandler topLevelExceptionHandler, + MapMembersUtils mapMemberUtils, + IEventAggregator eventAggregator ) { _store = store; @@ -77,17 +79,19 @@ MapMembersUtils mapMemberUtils _sendConversionCache = sendConversionCache; _operationProgressManager = operationProgressManager; _logger = logger; - _topLevelExceptionHandler = parent.TopLevelExceptionHandler; + _topLevelExceptionHandler = topLevelExceptionHandler; _arcGISConversionSettingsFactory = arcGisConversionSettingsFactory; _mapMemberUtils = mapMemberUtils; Parent = parent; Commands = new SendBindingUICommands(parent); SubscribeToArcGISEvents(); - _store.DocumentChanged += (_, _) => - { - _sendConversionCache.ClearCache(); - }; + eventAggregator + .GetEvent() + .Subscribe(_ => + { + _sendConversionCache.ClearCache(); + }); } private void SubscribeToArcGISEvents() @@ -139,28 +143,24 @@ private void SubscribeToArcGISEvents() private void SubscribeToMapMembersDataSourceChange() { - var task = QueuedTask.Run(() => + if (MapView.Active == null) { - if (MapView.Active == null) - { - return; - } + return; + } - // subscribe to layers - foreach (Layer layer in MapView.Active.Map.Layers) - { - if (layer is FeatureLayer featureLayer) - { - SubscribeToFeatureLayerDataSourceChange(featureLayer); - } - } - // subscribe to tables - foreach (StandaloneTable table in MapView.Active.Map.StandaloneTables) + // subscribe to layers + foreach (Layer layer in MapView.Active.Map.Layers) + { + if (layer is FeatureLayer featureLayer) { - SubscribeToTableDataSourceChange(table); + SubscribeToFeatureLayerDataSourceChange(featureLayer); } - }); - task.Wait(); + } + // subscribe to tables + foreach (StandaloneTable table in MapView.Active.Map.StandaloneTables) + { + SubscribeToTableDataSourceChange(table); + } } private void SubscribeToFeatureLayerDataSourceChange(FeatureLayer layer) @@ -195,7 +195,7 @@ private void SubscribeToAnyDataSourceChange(Table layerTable) { RowCreatedEvent.Subscribe( (args) => - Parent.TopLevelExceptionHandler.FireAndForget(async () => + _topLevelExceptionHandler.FireAndForget(async () => { await OnRowChanged(args).ConfigureAwait(false); }), @@ -203,7 +203,7 @@ private void SubscribeToAnyDataSourceChange(Table layerTable) ); RowChangedEvent.Subscribe( (args) => - Parent.TopLevelExceptionHandler.FireAndForget(async () => + _topLevelExceptionHandler.FireAndForget(async () => { await OnRowChanged(args).ConfigureAwait(false); }), @@ -211,7 +211,7 @@ private void SubscribeToAnyDataSourceChange(Table layerTable) ); RowDeletedEvent.Subscribe( (args) => - Parent.TopLevelExceptionHandler.FireAndForget(async () => + _topLevelExceptionHandler.FireAndForget(async () => { await OnRowChanged(args).ConfigureAwait(false); }), @@ -366,59 +366,50 @@ public async Task Send(string modelCardId) CancellationToken cancellationToken = _cancellationManager.InitCancellationTokenSource(modelCardId); - var sendResult = await QueuedTask - .Run(async () => + using var scope = _serviceProvider.CreateScope(); + scope + .ServiceProvider.GetRequiredService>() + .Initialize( + _arcGISConversionSettingsFactory.Create( + Project.Current, + MapView.Active.Map, + new CRSoffsetRotation(MapView.Active.Map) + ) + ); + List mapMembers = modelCard + .SendFilter.NotNull() + .RefreshObjectIds() + .Select(id => (MapMember)MapView.Active.Map.FindLayer(id) ?? MapView.Active.Map.FindStandaloneTable(id)) + .Where(obj => obj != null) + .ToList(); + + if (mapMembers.Count == 0) + { + // Handle as CARD ERROR in this function + throw new SpeckleSendFilterException("No objects were found to convert. Please update your publish filter!"); + } + + // subscribe to the selected layer events + foreach (MapMember mapMember in mapMembers) + { + if (mapMember is FeatureLayer featureLayer) + { + SubscribeToFeatureLayerDataSourceChange(featureLayer); + } + else if (mapMember is StandaloneTable table) { - using var scope = _serviceProvider.CreateScope(); - scope - .ServiceProvider.GetRequiredService>() - .Initialize( - _arcGISConversionSettingsFactory.Create( - Project.Current, - MapView.Active.Map, - new CRSoffsetRotation(MapView.Active.Map) - ) - ); - List mapMembers = modelCard - .SendFilter.NotNull() - .RefreshObjectIds() - .Select(id => (MapMember)MapView.Active.Map.FindLayer(id) ?? MapView.Active.Map.FindStandaloneTable(id)) - .Where(obj => obj != null) - .ToList(); - - if (mapMembers.Count == 0) - { - // Handle as CARD ERROR in this function - throw new SpeckleSendFilterException( - "No objects were found to convert. Please update your publish filter!" - ); - } - - // subscribe to the selected layer events - foreach (MapMember mapMember in mapMembers) - { - if (mapMember is FeatureLayer featureLayer) - { - SubscribeToFeatureLayerDataSourceChange(featureLayer); - } - else if (mapMember is StandaloneTable table) - { - SubscribeToTableDataSourceChange(table); - } - } - - var result = await scope - .ServiceProvider.GetRequiredService>() - .Execute( - mapMembers, - modelCard.GetSendInfo("ArcGIS"), // POC: get host app name from settings? same for GetReceiveInfo - _operationProgressManager.CreateOperationProgressEventHandler(Parent, modelCardId, cancellationToken), - cancellationToken - ) - .ConfigureAwait(false); - - return result; - }) + SubscribeToTableDataSourceChange(table); + } + } + + var sendResult = await scope + .ServiceProvider.GetRequiredService>() + .Execute( + mapMembers, + modelCard.GetSendInfo("ArcGIS"), // POC: get host app name from settings? same for GetReceiveInfo + _operationProgressManager.CreateOperationProgressEventHandler(Parent, modelCardId, cancellationToken), + cancellationToken + ) .ConfigureAwait(false); await Commands diff --git a/Connectors/ArcGIS/Speckle.Connectors.ArcGIS3/Bindings/BasicConnectorBinding.cs b/Connectors/ArcGIS/Speckle.Connectors.ArcGIS3/Bindings/BasicConnectorBinding.cs index 13e0d315c..49b51a384 100644 --- a/Connectors/ArcGIS/Speckle.Connectors.ArcGIS3/Bindings/BasicConnectorBinding.cs +++ b/Connectors/ArcGIS/Speckle.Connectors.ArcGIS3/Bindings/BasicConnectorBinding.cs @@ -1,9 +1,9 @@ using ArcGIS.Core.Data; -using ArcGIS.Desktop.Framework.Threading.Tasks; using ArcGIS.Desktop.Mapping; 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; @@ -22,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().ConfigureAwait(false); - }); + eventAggregator + .GetEvent() + .Subscribe(async _ => await Commands.NotifyDocumentChanged().ConfigureAwait(false)); } public string GetSourceApplicationName() => _speckleApplication.Slug; @@ -60,16 +63,19 @@ public BasicConnectorBinding(DocumentModelStore store, IBrowserBridge parent, IS public void RemoveModel(ModelCard model) => _store.RemoveModel(model); - public async Task HighlightObjects(IReadOnlyList objectIds) => - await HighlightObjectsOnView(objectIds.Select(x => new ObjectID(x)).ToList()).ConfigureAwait(false); + public Task HighlightObjects(IReadOnlyList objectIds) + { + HighlightObjectsOnView(objectIds.Select(x => new ObjectID(x)).ToList()); + return Task.CompletedTask; + } - public async Task HighlightModel(string modelCardId) + public Task HighlightModel(string modelCardId) { var model = _store.GetModelById(modelCardId); if (model is null) { - return; + return Task.CompletedTask; } var objectIds = new List(); @@ -86,26 +92,22 @@ public async Task HighlightModel(string modelCardId) if (objectIds is null) { - return; + return Task.CompletedTask; } - await HighlightObjectsOnView(objectIds).ConfigureAwait(false); + HighlightObjectsOnView(objectIds); + return Task.CompletedTask; } - private async Task HighlightObjectsOnView(IReadOnlyList objectIds) + private void HighlightObjectsOnView(IReadOnlyList objectIds) { MapView mapView = MapView.Active; - await QueuedTask - .Run(async () => - { - List mapMembersFeatures = GetMapMembers(objectIds, mapView); - ClearSelectionInTOC(); - ClearSelection(); - await SelectMapMembersInTOC(mapMembersFeatures).ConfigureAwait(false); - SelectMapMembersAndFeatures(mapMembersFeatures); - mapView.ZoomToSelected(); - }) - .ConfigureAwait(false); + List mapMembersFeatures = GetMapMembers(objectIds, mapView); + ClearSelectionInTOC(); + ClearSelection(); + SelectMapMembersInTOC(mapMembersFeatures); + SelectMapMembersAndFeatures(mapMembersFeatures); + mapView.ZoomToSelected(); } private List GetMapMembers(IReadOnlyList objectIds, MapView mapView) @@ -171,7 +173,7 @@ private void SelectMapMembersAndFeatures(IReadOnlyList mapMemb } } - private async Task SelectMapMembersInTOC(IReadOnlyList mapMembersFeatures) + private void SelectMapMembersInTOC(IReadOnlyList mapMembersFeatures) { List layers = new(); List tables = new(); @@ -187,7 +189,7 @@ private async Task SelectMapMembersInTOC(IReadOnlyList mapMemb } else { - await QueuedTask.Run(() => layer.SetExpanded(true)).ConfigureAwait(false); + layer.SetExpanded(true); } } else if (member is StandaloneTable table) diff --git a/Connectors/ArcGIS/Speckle.Connectors.ArcGIS3/DependencyInjection/ArcGISConnectorModule.cs b/Connectors/ArcGIS/Speckle.Connectors.ArcGIS3/DependencyInjection/ArcGISConnectorModule.cs index dc352d00e..6af5dd36a 100644 --- a/Connectors/ArcGIS/Speckle.Connectors.ArcGIS3/DependencyInjection/ArcGISConnectorModule.cs +++ b/Connectors/ArcGIS/Speckle.Connectors.ArcGIS3/DependencyInjection/ArcGISConnectorModule.cs @@ -27,15 +27,13 @@ public static class ArcGISConnectorModule public static void AddArcGIS(this IServiceCollection serviceCollection) { serviceCollection.AddConnectorUtils(); - serviceCollection.AddDUI(); + serviceCollection.AddDUI(); serviceCollection.AddDUIView(); // Register bindings serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); - serviceCollection.RegisterTopLevelExceptionHandler(); - serviceCollection.AddSingleton(sp => sp.GetRequiredService()); serviceCollection.AddSingleton(); diff --git a/Connectors/ArcGIS/Speckle.Connectors.ArcGIS3/HostApp/ArcGISColorManager.cs b/Connectors/ArcGIS/Speckle.Connectors.ArcGIS3/HostApp/ArcGISColorManager.cs index 36e34c8f9..afa769f8e 100644 --- a/Connectors/ArcGIS/Speckle.Connectors.ArcGIS3/HostApp/ArcGISColorManager.cs +++ b/Connectors/ArcGIS/Speckle.Connectors.ArcGIS3/HostApp/ArcGISColorManager.cs @@ -57,7 +57,7 @@ public List UnpackColors(List<(MapMember, int)> mapMembersWithDispla /// /// /// - public async Task ParseColors(List colorProxies, IProgress onOperationProgressed) + public void ParseColors(List colorProxies, IProgress onOperationProgressed) { // injected as Singleton, so we need to clean existing proxies first ObjectColorsIdMap = new(); @@ -65,7 +65,6 @@ public async Task ParseColors(List colorProxies, IProgress colorProxies, IProgress /// /// - public async Task ParseMaterials( - List materialProxies, - IProgress onOperationProgressed - ) + public void ParseMaterials(List materialProxies, IProgress onOperationProgressed) { // injected as Singleton, so we need to clean existing proxies first ObjectMaterialsIdMap = new(); @@ -90,7 +86,6 @@ IProgress onOperationProgressed foreach (RenderMaterialProxy colorProxy in materialProxies) { onOperationProgressed.Report(new("Converting materials", (double)++count / materialProxies.Count)); - await Task.Yield(); foreach (string objectId in colorProxy.objects) { Color convertedColor = Color.FromArgb(colorProxy.value.diffuse); diff --git a/Connectors/ArcGIS/Speckle.Connectors.ArcGIS3/Operations/Receive/ArcGISHostObjectBuilder.cs b/Connectors/ArcGIS/Speckle.Connectors.ArcGIS3/Operations/Receive/ArcGISHostObjectBuilder.cs index 69cbeaee3..e74b27684 100644 --- a/Connectors/ArcGIS/Speckle.Connectors.ArcGIS3/Operations/Receive/ArcGISHostObjectBuilder.cs +++ b/Connectors/ArcGIS/Speckle.Connectors.ArcGIS3/Operations/Receive/ArcGISHostObjectBuilder.cs @@ -1,7 +1,6 @@ using System.Diagnostics.Contracts; using ArcGIS.Core.CIM; using ArcGIS.Core.Geometry; -using ArcGIS.Desktop.Framework.Threading.Tasks; using ArcGIS.Desktop.Mapping; using Speckle.Connectors.ArcGIS.HostApp; using Speckle.Connectors.ArcGIS.Utils; @@ -58,7 +57,7 @@ ArcGISColorManager colorManager _crsUtils = crsUtils; } - public async Task Build( + public HostObjectBuilderResult Build( Base rootObject, string projectName, string modelName, @@ -78,14 +77,14 @@ CancellationToken cancellationToken .ToList(); if (materials != null) { - await _colorManager.ParseMaterials(materials, onOperationProgressed).ConfigureAwait(false); + _colorManager.ParseMaterials(materials, onOperationProgressed); } // get colors List? colors = (rootObject[ProxyKeys.COLOR] as List)?.Cast().ToList(); if (colors != null) { - await _colorManager.ParseColors(colors, onOperationProgressed).ConfigureAwait(false); + _colorManager.ParseColors(colors, onOperationProgressed); } int count = 0; @@ -104,10 +103,7 @@ CancellationToken cancellationToken try { obj = _localToGlobalConverterUtils.TransformObjects(objectToConvert.AtomicObject, objectToConvert.Matrix); - object? conversionResult = - obj is GisNonGeometricFeature - ? null - : await QueuedTask.Run(() => _converter.Convert(obj)).ConfigureAwait(false); + object? conversionResult = obj is GisNonGeometricFeature ? null : _converter.Convert(obj); string nestedLayerPath = $"{string.Join("\\", path)}"; if (objectToConvert.TraversalContext.Parent?.Current is not VectorLayer) @@ -130,29 +126,20 @@ obj is GisNonGeometricFeature // 2.1. Group conversionTrackers (to write into datasets) onOperationProgressed.Report(new("Grouping features into layers", null)); - Dictionary> convertedGroups = await QueuedTask - .Run(async () => - { - return await _featureClassUtils - .GroupConversionTrackers(conversionTracker, (s, progres) => onOperationProgressed.Report(new(s, progres))) - .ConfigureAwait(false); - }) - .ConfigureAwait(false); + Dictionary> convertedGroups = + _featureClassUtils.GroupConversionTrackers( + conversionTracker, + (s, progres) => onOperationProgressed.Report(new(s, progres)) + ); // 2.2. Write groups of objects to Datasets onOperationProgressed.Report(new("Writing to Database", null)); - await QueuedTask - .Run(async () => - { - await _featureClassUtils - .CreateDatasets( - conversionTracker, - convertedGroups, - (s, progres) => onOperationProgressed.Report(new(s, progres)) - ) - .ConfigureAwait(false); - }) - .ConfigureAwait(false); + + _featureClassUtils.CreateDatasets( + conversionTracker, + convertedGroups, + (s, progres) => onOperationProgressed.Report(new(s, progres)) + ); // 3. add layer and tables to the Map and Table Of Content @@ -204,8 +191,7 @@ await _featureClassUtils else { // no layer yet, create and add layer to Map - MapMember mapMember = await AddDatasetsToMap(trackerItem, createdLayerGroups, projectName, modelName) - .ConfigureAwait(false); + MapMember mapMember = AddDatasetsToMap(trackerItem, createdLayerGroups, projectName, modelName); // add layer and layer URI to tracker trackerItem.AddConvertedMapMember(mapMember); @@ -233,7 +219,7 @@ await _featureClassUtils if (bakedMember.Value.Item1 is FeatureLayer fLayer) { // Set the feature layer's renderer. - await QueuedTask.Run(() => fLayer.SetRenderer(bakedMember.Value.Item2)).ConfigureAwait(false); + fLayer.SetRenderer(bakedMember.Value.Item2); } } bakedObjectIds.AddRange(createdLayerGroups.Values.Select(x => x.URI)); @@ -304,80 +290,72 @@ private void AddResultsFromTracker(ObjectConversionTracker trackerItem, List AddDatasetsToMap( + private MapMember AddDatasetsToMap( ObjectConversionTracker trackerItem, Dictionary createdLayerGroups, string projectName, string modelName ) { - return await QueuedTask - .Run(() => - { - // get layer details - string? datasetId = trackerItem.DatasetId; // should not be null here - Uri uri = new($"{_settingsStore.Current.SpeckleDatabasePath.AbsolutePath.Replace('/', '\\')}\\{datasetId}"); - string nestedLayerName = trackerItem.NestedLayerName; + // get layer details + string? datasetId = trackerItem.DatasetId; // should not be null here + Uri uri = new($"{_settingsStore.Current.SpeckleDatabasePath.AbsolutePath.Replace('/', '\\')}\\{datasetId}"); + string nestedLayerName = trackerItem.NestedLayerName; - // add group for the current layer - string shortName = nestedLayerName.Split("\\")[^1]; - string nestedLayerPath = string.Join("\\", nestedLayerName.Split("\\").SkipLast(1)); + // add group for the current layer + string shortName = nestedLayerName.Split("\\")[^1]; + string nestedLayerPath = string.Join("\\", nestedLayerName.Split("\\").SkipLast(1)); - // if no general group layer found - if (createdLayerGroups.Count == 0) - { - Map map = _settingsStore.Current.Map; - GroupLayer mainGroupLayer = LayerFactory.Instance.CreateGroupLayer(map, 0, $"{projectName}: {modelName}"); - mainGroupLayer.SetExpanded(true); - createdLayerGroups["Basic Speckle Group"] = mainGroupLayer; // key doesn't really matter here - } + // if no general group layer found + if (createdLayerGroups.Count == 0) + { + Map map = _settingsStore.Current.Map; + GroupLayer mainGroupLayer = LayerFactory.Instance.CreateGroupLayer(map, 0, $"{projectName}: {modelName}"); + mainGroupLayer.SetExpanded(true); + createdLayerGroups["Basic Speckle Group"] = mainGroupLayer; // key doesn't really matter here + } - var groupLayer = CreateNestedGroupLayer(nestedLayerPath, createdLayerGroups); + var groupLayer = CreateNestedGroupLayer(nestedLayerPath, createdLayerGroups); - // Most of the Speckle-written datasets will be containing geometry and added as Layers - // although, some datasets might be just tables (e.g. native GIS Tables, in the future maybe Revit schedules etc. - // We can create a connection to the dataset in advance and determine its type, but this will be more - // expensive, than assuming by default that it's a layer with geometry (which in most cases it's expected to be) - try - { - var layer = LayerFactory.Instance.CreateLayer(uri, groupLayer, layerName: shortName); - if (layer == null) - { - throw new SpeckleException($"Layer '{shortName}' was not created"); - } - layer.SetExpanded(false); - - // if Scene - // https://community.esri.com/t5/arcgis-pro-sdk-questions/sdk-equivalent-to-changing-layer-s-elevation/td-p/1346139 - if (_settingsStore.Current.Map.IsScene) - { - var groundSurfaceLayer = _settingsStore.Current.Map.GetGroundElevationSurfaceLayer(); - var layerElevationSurface = new CIMLayerElevationSurface - { - ElevationSurfaceLayerURI = groundSurfaceLayer.URI, - }; - - // for Feature Layers - if (layer.GetDefinition() is CIMFeatureLayer cimLyr) - { - cimLyr.LayerElevation = layerElevationSurface; - layer.SetDefinition(cimLyr); - } - } - - return (MapMember)layer; - } - catch (ArgumentException) + // Most of the Speckle-written datasets will be containing geometry and added as Layers + // although, some datasets might be just tables (e.g. native GIS Tables, in the future maybe Revit schedules etc. + // We can create a connection to the dataset in advance and determine its type, but this will be more + // expensive, than assuming by default that it's a layer with geometry (which in most cases it's expected to be) + try + { + var layer = LayerFactory.Instance.CreateLayer(uri, groupLayer, layerName: shortName); + if (layer == null) + { + throw new SpeckleException($"Layer '{shortName}' was not created"); + } + layer.SetExpanded(false); + + // if Scene + // https://community.esri.com/t5/arcgis-pro-sdk-questions/sdk-equivalent-to-changing-layer-s-elevation/td-p/1346139 + if (_settingsStore.Current.Map.IsScene) + { + var groundSurfaceLayer = _settingsStore.Current.Map.GetGroundElevationSurfaceLayer(); + var layerElevationSurface = new CIMLayerElevationSurface { ElevationSurfaceLayerURI = groundSurfaceLayer.URI, }; + + // for Feature Layers + if (layer.GetDefinition() is CIMFeatureLayer cimLyr) { - StandaloneTable table = StandaloneTableFactory.Instance.CreateStandaloneTable( - uri, - groupLayer, - tableName: shortName - ); - return table; + cimLyr.LayerElevation = layerElevationSurface; + layer.SetDefinition(cimLyr); } - }) - .ConfigureAwait(false); + } + + return layer; + } + catch (ArgumentException) + { + StandaloneTable table = StandaloneTableFactory.Instance.CreateStandaloneTable( + uri, + groupLayer, + tableName: shortName + ); + return table; + } } private GroupLayer CreateNestedGroupLayer(string nestedLayerPath, Dictionary createdLayerGroups) diff --git a/Connectors/ArcGIS/Speckle.Connectors.ArcGIS3/Operations/Send/ArcGISRootObjectBuilder.cs b/Connectors/ArcGIS/Speckle.Connectors.ArcGIS3/Operations/Send/ArcGISRootObjectBuilder.cs index 08f3e2d0e..b0c3456d1 100644 --- a/Connectors/ArcGIS/Speckle.Connectors.ArcGIS3/Operations/Send/ArcGISRootObjectBuilder.cs +++ b/Connectors/ArcGIS/Speckle.Connectors.ArcGIS3/Operations/Send/ArcGISRootObjectBuilder.cs @@ -1,5 +1,4 @@ using System.Diagnostics; -using ArcGIS.Desktop.Framework.Threading.Tasks; using ArcGIS.Desktop.Mapping; using Microsoft.Extensions.Logging; using Speckle.Connectors.ArcGIS.HostApp; @@ -53,12 +52,11 @@ ISdkActivityFactory activityFactory } #pragma warning disable CA1506 - public async Task Build( + public RootObjectBuilderResult Build( #pragma warning restore CA1506 IReadOnlyList objects, SendInfo sendInfo, - IProgress onOperationProgressed, - CancellationToken ct = default + IProgress onOperationProgressed ) { // TODO: add a warning if Geographic CRS is set @@ -84,8 +82,6 @@ public async Task Build( { foreach ((MapMember mapMember, _) in layersWithDisplayPriority) { - ct.ThrowIfCancellationRequested(); - using (var convertingActivity = _activityFactory.Start("Converting object")) { var collectionHost = rootObjectCollection; @@ -130,9 +126,7 @@ mapMember is not ILayerContainer } else { - converted = await QueuedTask - .Run(() => (Collection)_rootToSpeckleConverter.Convert(mapMember)) - .ConfigureAwait(false); + converted = (Collection)_rootToSpeckleConverter.Convert(mapMember); // get units & Active CRS (for writing geometry coords) converted["units"] = _converterSettings.Current.SpeckleUnits; diff --git a/Connectors/ArcGIS/Speckle.Connectors.ArcGIS3/SpeckleModule.cs b/Connectors/ArcGIS/Speckle.Connectors.ArcGIS3/SpeckleModule.cs index 64370fdf2..1d5eee795 100644 --- a/Connectors/ArcGIS/Speckle.Connectors.ArcGIS3/SpeckleModule.cs +++ b/Connectors/ArcGIS/Speckle.Connectors.ArcGIS3/SpeckleModule.cs @@ -2,7 +2,6 @@ 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; @@ -35,7 +34,6 @@ 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 new file mode 100644 index 000000000..112ef1448 --- /dev/null +++ b/Connectors/ArcGIS/Speckle.Connectors.ArcGIS3/Utils/ArcGISThreadContext.cs @@ -0,0 +1,37 @@ +using ArcGIS.Desktop.Framework.Threading.Tasks; +using Speckle.Connectors.Common.Threading; + +namespace Speckle.Connectors.ArcGIS.Utils; + +//don't check for GUI as it's the same check we do in ThreadContext +public class ArcGISThreadContext : ThreadContext +{ + protected override ValueTask MainToWorkerAsync(Func> action) + { + if (QueuedTask.OnWorker) + { + return action(); + } + else + { + return QueuedTask.Run(async () => await action().BackToCurrent()).AsValueTask(); + } + } + + protected override ValueTask WorkerToMainAsync(Func> action) => + QueuedTask.Run(async () => await action().BackToCurrent()).AsValueTask(); + + protected override ValueTask MainToWorker(Func action) + { + if (QueuedTask.OnWorker) + { + return new(action()); + } + else + { + return QueuedTask.Run(action).AsValueTask(); + } + } + + protected override ValueTask WorkerToMain(Func action) => QueuedTask.Run(action).AsValueTask(); +} diff --git a/Connectors/ArcGIS/Speckle.Connectors.ArcGIS3/Utils/ArcGisDocumentStore.cs b/Connectors/ArcGIS/Speckle.Connectors.ArcGIS3/Utils/ArcGisDocumentStore.cs index 78be6af1d..833b28039 100644 --- a/Connectors/ArcGIS/Speckle.Connectors.ArcGIS3/Utils/ArcGisDocumentStore.cs +++ b/Connectors/ArcGIS/Speckle.Connectors.ArcGIS3/Utils/ArcGisDocumentStore.cs @@ -1,9 +1,10 @@ using System.Xml.Linq; using ArcGIS.Desktop.Core.Events; -using ArcGIS.Desktop.Framework.Threading.Tasks; using ArcGIS.Desktop.Mapping; 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; @@ -11,9 +12,19 @@ namespace Speckle.Connectors.ArcGIS.Utils; public class ArcGISDocumentStore : DocumentModelStore { - public ArcGISDocumentStore(IJsonSerializer jsonSerializer, ITopLevelExceptionHandler topLevelExceptionHandler) + private readonly IThreadContext _threadContext; + private readonly IEventAggregator _eventAggregator; + + public ArcGISDocumentStore( + IJsonSerializer jsonSerializer, + ITopLevelExceptionHandler topLevelExceptionHandler, + IThreadContext threadContext, + IEventAggregator eventAggregator + ) : base(jsonSerializer) { + _threadContext = threadContext; + _eventAggregator = eventAggregator; ActiveMapViewChangedEvent.Subscribe(a => topLevelExceptionHandler.CatchUnhandled(() => OnMapViewChanged(a)), true); ProjectSavingEvent.Subscribe( _ => @@ -37,7 +48,7 @@ public ArcGISDocumentStore(IJsonSerializer jsonSerializer, ITopLevelExceptionHan { IsDocumentInit = true; LoadState(); - OnDocumentChanged(); + eventAggregator.GetEvent().Publish(new object()); } } @@ -71,55 +82,55 @@ private void OnMapViewChanged(ActiveMapViewChangedEventArgs args) IsDocumentInit = true; LoadState(); - OnDocumentChanged(); + _eventAggregator.GetEvent().Publish(new object()); } - protected override void HostAppSaveState(string modelCardState) - { - Map map = MapView.Active.Map; - QueuedTask.Run(() => - { - // Read existing metadata - To prevent messing existing metadata. 🤞 Hope other add-in developers will do same :D - var existingMetadata = map.GetMetadata(); + protected override void HostAppSaveState(string modelCardState) => + _threadContext + .RunOnWorker(() => + { + Map map = MapView.Active.Map; + // Read existing metadata - To prevent messing existing metadata. 🤞 Hope other add-in developers will do same :D + var existingMetadata = map.GetMetadata(); - // Parse existing metadata - XDocument existingXmlDocument = !string.IsNullOrEmpty(existingMetadata) - ? XDocument.Parse(existingMetadata) - : new XDocument(new XElement("metadata")); + // Parse existing metadata + XDocument existingXmlDocument = !string.IsNullOrEmpty(existingMetadata) + ? XDocument.Parse(existingMetadata) + : new XDocument(new XElement("metadata")); - XElement xmlModelCards = new("SpeckleModelCards", modelCardState); + XElement xmlModelCards = new("SpeckleModelCards", modelCardState); - // Check if SpeckleModelCards element already exists at root and update it - var speckleModelCardsElement = existingXmlDocument.Root?.Element("SpeckleModelCards"); - if (speckleModelCardsElement != null) - { - speckleModelCardsElement.ReplaceWith(xmlModelCards); - } - else - { - existingXmlDocument.Root?.Add(xmlModelCards); - } + // Check if SpeckleModelCards element already exists at root and update it + var speckleModelCardsElement = existingXmlDocument.Root?.Element("SpeckleModelCards"); + if (speckleModelCardsElement != null) + { + speckleModelCardsElement.ReplaceWith(xmlModelCards); + } + else + { + existingXmlDocument.Root?.Add(xmlModelCards); + } - map.SetMetadata(existingXmlDocument.ToString()); - }); - } + map.SetMetadata(existingXmlDocument.ToString()); + }) + .Wait(); - protected override void LoadState() - { - Map map = MapView.Active.Map; - QueuedTask.Run(() => - { - var metadata = map.GetMetadata(); - var root = XDocument.Parse(metadata).Root; - var element = root?.Element("SpeckleModelCards"); - if (element is null) + protected override void LoadState() => + _threadContext + .RunOnWorker(() => { - ClearAndSave(); - return; - } + Map map = MapView.Active.Map; + var metadata = map.GetMetadata(); + var root = XDocument.Parse(metadata).Root; + var element = root?.Element("SpeckleModelCards"); + if (element is null) + { + ClearAndSave(); + return; + } - string modelsString = element.Value; - LoadFromString(modelsString); - }); - } + string modelsString = element.Value; + LoadFromString(modelsString); + }) + .Wait(); } diff --git a/Connectors/ArcGIS/Speckle.Connectors.ArcGIS3/packages.lock.json b/Connectors/ArcGIS/Speckle.Connectors.ArcGIS3/packages.lock.json index d98dbf487..05a181da3 100644 --- a/Connectors/ArcGIS/Speckle.Connectors.ArcGIS3/packages.lock.json +++ b/Connectors/ArcGIS/Speckle.Connectors.ArcGIS3/packages.lock.json @@ -242,8 +242,7 @@ "Microsoft.Extensions.Logging.Abstractions": "[2.2.0, )", "Speckle.Connectors.Common": "[1.0.0, )", "Speckle.Sdk": "[3.1.0-dev.205, )", - "Speckle.Sdk.Dependencies": "[3.1.0-dev.205, )", - "System.Threading.Tasks.Dataflow": "[6.0.0, )" + "Speckle.Sdk.Dependencies": "[3.1.0-dev.205, )" } }, "speckle.connectors.dui.webview": { @@ -328,12 +327,6 @@ "requested": "[3.1.0-dev.205, )", "resolved": "3.1.0-dev.205", "contentHash": "MjWRxQhXYZxfpc1JwKc33jjBH3mLLxnbCNx5mcYAdGRJDFb/6ebHzQqWE1abxxCa5k1GJBbH8RpMscFm3w6oVQ==" - }, - "System.Threading.Tasks.Dataflow": { - "type": "CentralTransitive", - "requested": "[6.0.0, )", - "resolved": "6.0.0", - "contentHash": "+tyDCU3/B1lDdOOAJywHQoFwyXIUghIaP2BxG79uvhfTnO+D9qIgjVlL/JV2NTliYbMHpd6eKDmHp2VHpij7MA==" } }, "net6.0-windows7.0/win-x64": { diff --git a/Connectors/Autocad/Speckle.Connectors.Autocad2022/packages.lock.json b/Connectors/Autocad/Speckle.Connectors.Autocad2022/packages.lock.json index 488f4e2f2..7be4622a5 100644 --- a/Connectors/Autocad/Speckle.Connectors.Autocad2022/packages.lock.json +++ b/Connectors/Autocad/Speckle.Connectors.Autocad2022/packages.lock.json @@ -275,8 +275,7 @@ "Microsoft.Extensions.Logging.Abstractions": "[2.2.0, )", "Speckle.Connectors.Common": "[1.0.0, )", "Speckle.Sdk": "[3.1.0-dev.205, )", - "Speckle.Sdk.Dependencies": "[3.1.0-dev.205, )", - "System.Threading.Tasks.Dataflow": "[6.0.0, )" + "Speckle.Sdk.Dependencies": "[3.1.0-dev.205, )" } }, "speckle.connectors.dui.webview": { @@ -367,12 +366,6 @@ "requested": "[3.1.0-dev.205, )", "resolved": "3.1.0-dev.205", "contentHash": "MjWRxQhXYZxfpc1JwKc33jjBH3mLLxnbCNx5mcYAdGRJDFb/6ebHzQqWE1abxxCa5k1GJBbH8RpMscFm3w6oVQ==" - }, - "System.Threading.Tasks.Dataflow": { - "type": "CentralTransitive", - "requested": "[6.0.0, )", - "resolved": "6.0.0", - "contentHash": "+tyDCU3/B1lDdOOAJywHQoFwyXIUghIaP2BxG79uvhfTnO+D9qIgjVlL/JV2NTliYbMHpd6eKDmHp2VHpij7MA==" } } } diff --git a/Connectors/Autocad/Speckle.Connectors.Autocad2023/packages.lock.json b/Connectors/Autocad/Speckle.Connectors.Autocad2023/packages.lock.json index 8036d8f4f..41ae17d17 100644 --- a/Connectors/Autocad/Speckle.Connectors.Autocad2023/packages.lock.json +++ b/Connectors/Autocad/Speckle.Connectors.Autocad2023/packages.lock.json @@ -275,8 +275,7 @@ "Microsoft.Extensions.Logging.Abstractions": "[2.2.0, )", "Speckle.Connectors.Common": "[1.0.0, )", "Speckle.Sdk": "[3.1.0-dev.205, )", - "Speckle.Sdk.Dependencies": "[3.1.0-dev.205, )", - "System.Threading.Tasks.Dataflow": "[6.0.0, )" + "Speckle.Sdk.Dependencies": "[3.1.0-dev.205, )" } }, "speckle.connectors.dui.webview": { @@ -367,12 +366,6 @@ "requested": "[3.1.0-dev.205, )", "resolved": "3.1.0-dev.205", "contentHash": "MjWRxQhXYZxfpc1JwKc33jjBH3mLLxnbCNx5mcYAdGRJDFb/6ebHzQqWE1abxxCa5k1GJBbH8RpMscFm3w6oVQ==" - }, - "System.Threading.Tasks.Dataflow": { - "type": "CentralTransitive", - "requested": "[6.0.0, )", - "resolved": "6.0.0", - "contentHash": "+tyDCU3/B1lDdOOAJywHQoFwyXIUghIaP2BxG79uvhfTnO+D9qIgjVlL/JV2NTliYbMHpd6eKDmHp2VHpij7MA==" } } } diff --git a/Connectors/Autocad/Speckle.Connectors.Autocad2024/packages.lock.json b/Connectors/Autocad/Speckle.Connectors.Autocad2024/packages.lock.json index 6ef1bf6e5..a0f31de93 100644 --- a/Connectors/Autocad/Speckle.Connectors.Autocad2024/packages.lock.json +++ b/Connectors/Autocad/Speckle.Connectors.Autocad2024/packages.lock.json @@ -275,8 +275,7 @@ "Microsoft.Extensions.Logging.Abstractions": "[2.2.0, )", "Speckle.Connectors.Common": "[1.0.0, )", "Speckle.Sdk": "[3.1.0-dev.205, )", - "Speckle.Sdk.Dependencies": "[3.1.0-dev.205, )", - "System.Threading.Tasks.Dataflow": "[6.0.0, )" + "Speckle.Sdk.Dependencies": "[3.1.0-dev.205, )" } }, "speckle.connectors.dui.webview": { @@ -368,12 +367,6 @@ "requested": "[3.1.0-dev.205, )", "resolved": "3.1.0-dev.205", "contentHash": "MjWRxQhXYZxfpc1JwKc33jjBH3mLLxnbCNx5mcYAdGRJDFb/6ebHzQqWE1abxxCa5k1GJBbH8RpMscFm3w6oVQ==" - }, - "System.Threading.Tasks.Dataflow": { - "type": "CentralTransitive", - "requested": "[6.0.0, )", - "resolved": "6.0.0", - "contentHash": "+tyDCU3/B1lDdOOAJywHQoFwyXIUghIaP2BxG79uvhfTnO+D9qIgjVlL/JV2NTliYbMHpd6eKDmHp2VHpij7MA==" } } } diff --git a/Connectors/Autocad/Speckle.Connectors.Autocad2025/packages.lock.json b/Connectors/Autocad/Speckle.Connectors.Autocad2025/packages.lock.json index 8ebeb4e29..cad8d9ce2 100644 --- a/Connectors/Autocad/Speckle.Connectors.Autocad2025/packages.lock.json +++ b/Connectors/Autocad/Speckle.Connectors.Autocad2025/packages.lock.json @@ -231,8 +231,7 @@ "Microsoft.Extensions.Logging.Abstractions": "[2.2.0, )", "Speckle.Connectors.Common": "[1.0.0, )", "Speckle.Sdk": "[3.1.0-dev.205, )", - "Speckle.Sdk.Dependencies": "[3.1.0-dev.205, )", - "System.Threading.Tasks.Dataflow": "[6.0.0, )" + "Speckle.Sdk.Dependencies": "[3.1.0-dev.205, )" } }, "speckle.connectors.dui.webview": { @@ -323,12 +322,6 @@ "requested": "[3.1.0-dev.205, )", "resolved": "3.1.0-dev.205", "contentHash": "MjWRxQhXYZxfpc1JwKc33jjBH3mLLxnbCNx5mcYAdGRJDFb/6ebHzQqWE1abxxCa5k1GJBbH8RpMscFm3w6oVQ==" - }, - "System.Threading.Tasks.Dataflow": { - "type": "CentralTransitive", - "requested": "[6.0.0, )", - "resolved": "6.0.0", - "contentHash": "+tyDCU3/B1lDdOOAJywHQoFwyXIUghIaP2BxG79uvhfTnO+D9qIgjVlL/JV2NTliYbMHpd6eKDmHp2VHpij7MA==" } }, "net8.0-windows7.0/win-x64": { diff --git a/Connectors/Autocad/Speckle.Connectors.AutocadShared/Bindings/AutocadBasicConnectorBinding.cs b/Connectors/Autocad/Speckle.Connectors.AutocadShared/Bindings/AutocadBasicConnectorBinding.cs index 5a976f0fc..8e16322c7 100644 --- a/Connectors/Autocad/Speckle.Connectors.AutocadShared/Bindings/AutocadBasicConnectorBinding.cs +++ b/Connectors/Autocad/Speckle.Connectors.AutocadShared/Bindings/AutocadBasicConnectorBinding.cs @@ -1,8 +1,10 @@ using Autodesk.AutoCAD.DatabaseServices; using Microsoft.Extensions.Logging; using Speckle.Connectors.Autocad.HostApp.Extensions; +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; @@ -19,6 +21,7 @@ public class AutocadBasicConnectorBinding : IBasicConnectorBinding private readonly DocumentModelStore _store; private readonly ISpeckleApplication _speckleApplication; + private readonly IThreadContext _threadContext; private readonly ILogger _logger; public BasicConnectorBindingCommands Commands { get; } @@ -28,7 +31,9 @@ public AutocadBasicConnectorBinding( IBrowserBridge parent, IAccountManager accountManager, ISpeckleApplication speckleApplication, - ILogger logger + ILogger logger, + IEventAggregator eventAggregator, + IThreadContext threadContext ) { _store = store; @@ -36,12 +41,14 @@ ILogger logger _accountManager = accountManager; _speckleApplication = speckleApplication; Commands = new BasicConnectorBindingCommands(parent); - _store.DocumentChanged += (_, _) => - parent.TopLevelExceptionHandler.FireAndForget(async () => + eventAggregator + .GetEvent() + .Subscribe(async _ => { await Commands.NotifyDocumentChanged().ConfigureAwait(false); }); _logger = logger; + _threadContext = threadContext; } public string GetConnectorVersion() => _speckleApplication.SpeckleVersion; @@ -129,12 +136,12 @@ private async Task HighlightObjectsOnView(ObjectId[] objectIds, string? modelCar { var doc = Application.DocumentManager.MdiActiveDocument; - await Parent - .RunOnMainThreadAsync(async () => + await _threadContext + .RunOnMainAsync(async () => { try { - doc.Editor.SetImpliedSelection(Array.Empty()); // Deselects + doc.Editor.SetImpliedSelection([]); // Deselects try { doc.Editor.SetImpliedSelection(objectIds); diff --git a/Connectors/Autocad/Speckle.Connectors.AutocadShared/Bindings/AutocadSelectionBinding.cs b/Connectors/Autocad/Speckle.Connectors.AutocadShared/Bindings/AutocadSelectionBinding.cs index 0be4d5611..1f3dfc809 100644 --- a/Connectors/Autocad/Speckle.Connectors.AutocadShared/Bindings/AutocadSelectionBinding.cs +++ b/Connectors/Autocad/Speckle.Connectors.AutocadShared/Bindings/AutocadSelectionBinding.cs @@ -1,6 +1,7 @@ using Autodesk.AutoCAD.DatabaseServices; using Autodesk.AutoCAD.EditorInput; using Speckle.Connectors.Autocad.HostApp.Extensions; +using Speckle.Connectors.Common.Threading; using Speckle.Connectors.DUI.Bindings; using Speckle.Connectors.DUI.Bridge; @@ -10,16 +11,22 @@ public class AutocadSelectionBinding : ISelectionBinding { private const string SELECTION_EVENT = "setSelection"; private readonly ITopLevelExceptionHandler _topLevelExceptionHandler; + private readonly IThreadContext _threadContext; private readonly HashSet _visitedDocuments = new(); public string Name => "selectionBinding"; public IBrowserBridge Parent { get; } - public AutocadSelectionBinding(IBrowserBridge parent) + public AutocadSelectionBinding( + IBrowserBridge parent, + IThreadContext threadContext, + ITopLevelExceptionHandler topLevelExceptionHandler + ) { - _topLevelExceptionHandler = parent.TopLevelExceptionHandler; + _topLevelExceptionHandler = topLevelExceptionHandler; Parent = parent; + _threadContext = threadContext; // POC: Use here Context for doc. In converters it's OK but we are still lacking to use context into bindings. // It is with the case of if binding created with already a document @@ -42,7 +49,7 @@ private void TryRegisterDocumentForSelection(Document? document) { document.ImpliedSelectionChanged += (_, _) => _topLevelExceptionHandler.FireAndForget( - async () => await Parent.RunOnMainThreadAsync(OnSelectionChanged).ConfigureAwait(false) + async () => await _threadContext.RunOnMainAsync(OnSelectionChanged).ConfigureAwait(false) ); _visitedDocuments.Add(document); @@ -54,7 +61,7 @@ private void TryRegisterDocumentForSelection(Document? document) // Ui requests to GetSelection() should just return this local copy that is kept up to date by the event handler. private SelectionInfo _selectionInfo; - private async Task OnSelectionChanged() + private async ValueTask OnSelectionChanged() { _selectionInfo = GetSelectionInternal(); await Parent.Send(SELECTION_EVENT, _selectionInfo).ConfigureAwait(false); diff --git a/Connectors/Autocad/Speckle.Connectors.AutocadShared/Bindings/AutocadSendBaseBinding.cs b/Connectors/Autocad/Speckle.Connectors.AutocadShared/Bindings/AutocadSendBaseBinding.cs index 11f840f92..eba1d47e0 100644 --- a/Connectors/Autocad/Speckle.Connectors.AutocadShared/Bindings/AutocadSendBaseBinding.cs +++ b/Connectors/Autocad/Speckle.Connectors.AutocadShared/Bindings/AutocadSendBaseBinding.cs @@ -8,8 +8,10 @@ using Speckle.Connectors.Common.Caching; using Speckle.Connectors.Common.Cancellation; using Speckle.Connectors.Common.Operations; +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; @@ -38,6 +40,7 @@ public abstract class AutocadSendBaseBinding : ISendBinding private readonly ILogger _logger; private readonly ITopLevelExceptionHandler _topLevelExceptionHandler; private readonly ISpeckleApplication _speckleApplication; + private readonly IThreadContext _threadContext; /// /// Used internally to aggregate the changed objects' id. Note we're using a concurrent dictionary here as the expiry check method is not thread safe, and this was causing problems. See: @@ -57,7 +60,10 @@ protected AutocadSendBaseBinding( ISendConversionCache sendConversionCache, IOperationProgressManager operationProgressManager, ILogger logger, - ISpeckleApplication speckleApplication + ISpeckleApplication speckleApplication, + ITopLevelExceptionHandler topLevelExceptionHandler, + IThreadContext threadContext, + IEventAggregator eventAggregator ) { _store = store; @@ -69,7 +75,8 @@ ISpeckleApplication speckleApplication _operationProgressManager = operationProgressManager; _logger = logger; _speckleApplication = speckleApplication; - _topLevelExceptionHandler = parent.TopLevelExceptionHandler; + _threadContext = threadContext; + _topLevelExceptionHandler = topLevelExceptionHandler; Parent = parent; Commands = new SendBindingUICommands(parent); @@ -82,10 +89,8 @@ ISpeckleApplication speckleApplication 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(); @@ -144,8 +149,8 @@ private async Task RunExpirationChecks() public List GetSendSettings() => []; public async Task Send(string modelCardId) => - await Parent - .RunOnMainThreadAsync(async () => await SendInternal(modelCardId).ConfigureAwait(false)) + await _threadContext + .RunOnWorkerAsync(async () => await SendInternal(modelCardId).ConfigureAwait(false)) .ConfigureAwait(false); protected abstract void InitializeSettings(IServiceProvider serviceProvider); diff --git a/Connectors/Autocad/Speckle.Connectors.AutocadShared/Bindings/AutocadSendBinding.cs b/Connectors/Autocad/Speckle.Connectors.AutocadShared/Bindings/AutocadSendBinding.cs index bc2a28a96..e222636c6 100644 --- a/Connectors/Autocad/Speckle.Connectors.AutocadShared/Bindings/AutocadSendBinding.cs +++ b/Connectors/Autocad/Speckle.Connectors.AutocadShared/Bindings/AutocadSendBinding.cs @@ -3,8 +3,10 @@ using Speckle.Connectors.Autocad.HostApp; using Speckle.Connectors.Common.Caching; using Speckle.Connectors.Common.Cancellation; +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; @@ -28,7 +30,10 @@ public AutocadSendBinding( IOperationProgressManager operationProgressManager, ILogger logger, IAutocadConversionSettingsFactory autocadConversionSettingsFactory, - ISpeckleApplication speckleApplication + ISpeckleApplication speckleApplication, + ITopLevelExceptionHandler topLevelExceptionHandler, + IThreadContext threadContext, + IEventAggregator eventAggregator ) : base( store, @@ -40,7 +45,10 @@ ISpeckleApplication speckleApplication sendConversionCache, operationProgressManager, logger, - speckleApplication + speckleApplication, + 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 72bc3567f..7158cdb49 100644 --- a/Connectors/Autocad/Speckle.Connectors.AutocadShared/DependencyInjection/SharedRegistration.cs +++ b/Connectors/Autocad/Speckle.Connectors.AutocadShared/DependencyInjection/SharedRegistration.cs @@ -10,6 +10,7 @@ using Speckle.Connectors.Common.Caching; using Speckle.Connectors.Common.Instances; using Speckle.Connectors.Common.Operations; +using Speckle.Connectors.Common.Threading; using Speckle.Connectors.DUI; using Speckle.Connectors.DUI.Bindings; using Speckle.Connectors.DUI.Bridge; @@ -24,7 +25,7 @@ public static class SharedRegistration public static void AddAutocadBase(this IServiceCollection serviceCollection) { serviceCollection.AddConnectorUtils(); - serviceCollection.AddDUI(); + serviceCollection.AddDUI(); serviceCollection.AddDUIView(); // Register other connector specific types @@ -43,10 +44,8 @@ public static void AddAutocadBase(this IServiceCollection serviceCollection) serviceCollection.AddScoped(); serviceCollection.AddScoped(); - serviceCollection.AddScoped(); serviceCollection.AddScoped(); - serviceCollection.AddScoped(); serviceCollection.AddSingleton(); @@ -60,8 +59,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/AutocadColorBaker.cs b/Connectors/Autocad/Speckle.Connectors.AutocadShared/HostApp/AutocadColorBaker.cs index 7395ab6e8..c02e1d839 100644 --- a/Connectors/Autocad/Speckle.Connectors.AutocadShared/HostApp/AutocadColorBaker.cs +++ b/Connectors/Autocad/Speckle.Connectors.AutocadShared/HostApp/AutocadColorBaker.cs @@ -1,6 +1,7 @@ using Autodesk.AutoCAD.Colors; using Microsoft.Extensions.Logging; using Speckle.Connectors.Common.Operations; +using Speckle.InterfaceGenerator; using Speckle.Sdk; using Speckle.Sdk.Models.Proxies; using AutocadColor = Autodesk.AutoCAD.Colors.Color; @@ -10,15 +11,9 @@ namespace Speckle.Connectors.Autocad.HostApp; /// /// Expects to be a scoped dependency for a given operation and helps with layer creation and cleanup. /// -public class AutocadColorBaker +[GenerateAutoInterface] +public class AutocadColorBaker(ILogger logger) : IAutocadColorBaker { - private readonly ILogger _logger; - - public AutocadColorBaker(ILogger logger) - { - _logger = logger; - } - /// /// For receive operations /// @@ -59,7 +54,7 @@ public void ParseColors(IReadOnlyCollection colorProxies, IProgress< } catch (Exception ex) when (!ex.IsFatal()) { - _logger.LogError(ex, "Failed parsing color proxy"); + logger.LogError(ex, "Failed parsing color proxy"); } } } diff --git a/Connectors/Autocad/Speckle.Connectors.AutocadShared/HostApp/AutocadDocumentModelStore.cs b/Connectors/Autocad/Speckle.Connectors.AutocadShared/HostApp/AutocadDocumentModelStore.cs index c8319d638..04c68ab29 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! @@ -48,7 +52,8 @@ private void OnDocChangeInternal(Document? doc) _previousDocName = currentDocName; LoadState(); - OnDocumentChanged(); + LoadState(); + _eventAggregator.GetEvent().Publish(new object()); } protected override void LoadState() diff --git a/Connectors/Autocad/Speckle.Connectors.AutocadShared/HostApp/AutocadInstanceBaker.cs b/Connectors/Autocad/Speckle.Connectors.AutocadShared/HostApp/AutocadInstanceBaker.cs index 8121d97f7..e1373f54f 100644 --- a/Connectors/Autocad/Speckle.Connectors.AutocadShared/HostApp/AutocadInstanceBaker.cs +++ b/Connectors/Autocad/Speckle.Connectors.AutocadShared/HostApp/AutocadInstanceBaker.cs @@ -26,16 +26,16 @@ namespace Speckle.Connectors.Autocad.HostApp; public class AutocadInstanceBaker : IInstanceBaker> { private readonly AutocadLayerBaker _layerBaker; - private readonly AutocadColorBaker _colorBaker; - private readonly AutocadMaterialBaker _materialBaker; + private readonly IAutocadColorBaker _colorBaker; + private readonly IAutocadMaterialBaker _materialBaker; private readonly AutocadContext _autocadContext; private readonly ILogger _logger; private readonly IConverterSettingsStore _converterSettings; public AutocadInstanceBaker( AutocadLayerBaker layerBaker, - AutocadColorBaker colorBaker, - AutocadMaterialBaker materialBaker, + IAutocadColorBaker colorBaker, + IAutocadMaterialBaker materialBaker, AutocadContext autocadContext, ILogger logger, IConverterSettingsStore converterSettings diff --git a/Connectors/Autocad/Speckle.Connectors.AutocadShared/HostApp/AutocadLayerBaker.cs b/Connectors/Autocad/Speckle.Connectors.AutocadShared/HostApp/AutocadLayerBaker.cs index f70d1dca4..ebd4737eb 100644 --- a/Connectors/Autocad/Speckle.Connectors.AutocadShared/HostApp/AutocadLayerBaker.cs +++ b/Connectors/Autocad/Speckle.Connectors.AutocadShared/HostApp/AutocadLayerBaker.cs @@ -12,15 +12,15 @@ public class AutocadLayerBaker : TraversalContextUnpacker { private readonly string _layerFilterName = "Speckle"; private readonly AutocadContext _autocadContext; - private readonly AutocadMaterialBaker _materialBaker; - private readonly AutocadColorBaker _colorBaker; + private readonly IAutocadMaterialBaker _materialBaker; + private readonly IAutocadColorBaker _colorBaker; private Document Doc => Application.DocumentManager.MdiActiveDocument; private readonly HashSet _uniqueLayerNames = new(); public AutocadLayerBaker( AutocadContext autocadContext, - AutocadMaterialBaker materialBaker, - AutocadColorBaker colorBaker + IAutocadMaterialBaker materialBaker, + IAutocadColorBaker colorBaker ) { _autocadContext = autocadContext; diff --git a/Connectors/Autocad/Speckle.Connectors.AutocadShared/HostApp/AutocadMaterialBaker.cs b/Connectors/Autocad/Speckle.Connectors.AutocadShared/HostApp/AutocadMaterialBaker.cs index 13fa39e4e..1e09a7f2b 100644 --- a/Connectors/Autocad/Speckle.Connectors.AutocadShared/HostApp/AutocadMaterialBaker.cs +++ b/Connectors/Autocad/Speckle.Connectors.AutocadShared/HostApp/AutocadMaterialBaker.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.Logging; using Speckle.Connectors.Common.Conversion; using Speckle.Connectors.Common.Operations; +using Speckle.InterfaceGenerator; using Speckle.Objects.Other; using Speckle.Sdk; using Speckle.Sdk.Common; @@ -16,7 +17,8 @@ namespace Speckle.Connectors.Autocad.HostApp; /// /// Expects to be a scoped dependency for a given operation and helps with layer creation and cleanup. /// -public class AutocadMaterialBaker +[GenerateAutoInterface] +public class AutocadMaterialBaker : IAutocadMaterialBaker { private readonly ILogger _logger; private readonly AutocadContext _autocadContext; diff --git a/Connectors/Autocad/Speckle.Connectors.AutocadShared/Operations/Receive/AutocadHostObjectBuilder.cs b/Connectors/Autocad/Speckle.Connectors.AutocadShared/Operations/Receive/AutocadHostObjectBuilder.cs index 43e5d3dea..94e0d2a80 100644 --- a/Connectors/Autocad/Speckle.Connectors.AutocadShared/Operations/Receive/AutocadHostObjectBuilder.cs +++ b/Connectors/Autocad/Speckle.Connectors.AutocadShared/Operations/Receive/AutocadHostObjectBuilder.cs @@ -20,86 +20,44 @@ namespace Speckle.Connectors.Autocad.Operations.Receive; /// /// Expects to be a scoped dependency per receive operation. /// -public class AutocadHostObjectBuilder : IHostObjectBuilder +public class AutocadHostObjectBuilder( + IRootToHostConverter converter, + AutocadLayerBaker layerBaker, + AutocadGroupBaker groupBaker, + AutocadInstanceBaker instanceBaker, + IAutocadMaterialBaker materialBaker, + IAutocadColorBaker colorBaker, + AutocadContext autocadContext, + RootObjectUnpacker rootObjectUnpacker +) : IHostObjectBuilder { - private readonly AutocadLayerBaker _layerBaker; - private readonly IRootToHostConverter _converter; - private readonly ISyncToThread _syncToThread; - private readonly AutocadGroupBaker _groupBaker; - private readonly AutocadMaterialBaker _materialBaker; - private readonly AutocadColorBaker _colorBaker; - private readonly AutocadInstanceBaker _instanceBaker; - private readonly AutocadContext _autocadContext; - private readonly RootObjectUnpacker _rootObjectUnpacker; - - public AutocadHostObjectBuilder( - IRootToHostConverter converter, - AutocadLayerBaker layerBaker, - AutocadGroupBaker groupBaker, - AutocadInstanceBaker instanceBaker, - AutocadMaterialBaker materialBaker, - AutocadColorBaker colorBaker, - ISyncToThread syncToThread, - AutocadContext autocadContext, - RootObjectUnpacker rootObjectUnpacker - ) - { - _converter = converter; - _layerBaker = layerBaker; - _groupBaker = groupBaker; - _instanceBaker = instanceBaker; - _materialBaker = materialBaker; - _colorBaker = colorBaker; - _syncToThread = syncToThread; - _autocadContext = autocadContext; - _rootObjectUnpacker = rootObjectUnpacker; - } - - public async Task Build( + public HostObjectBuilderResult Build( Base rootObject, string projectName, string modelName, IProgress onOperationProgressed, - CancellationToken _ - ) - { - // NOTE: This is the only place we apply ISyncToThread across connectors. We need to sync up with main thread here - // after GetObject and Deserialization. It is anti-pattern now. Happiness level 3/10 but works. - return await _syncToThread - .RunOnThread(async () => - { - await Task.CompletedTask.ConfigureAwait(true); - return BuildSync(rootObject, projectName, modelName, onOperationProgressed); - }) - .ConfigureAwait(false); - } - - private HostObjectBuilderResult BuildSync( - Base rootObject, - string projectName, - string modelName, - IProgress onOperationProgressed + CancellationToken cancellationToken ) { // Prompt the UI conversion started. Progress bar will swoosh. onOperationProgressed.Report(new("Converting", null)); // Layer filter for received commit with project and model name - _layerBaker.CreateLayerFilter(projectName, modelName); + layerBaker.CreateLayerFilter(projectName, modelName); // 0 - Clean then Rock n Roll! - string baseLayerPrefix = _autocadContext.RemoveInvalidChars($"SPK-{projectName}-{modelName}-"); + string baseLayerPrefix = autocadContext.RemoveInvalidChars($"SPK-{projectName}-{modelName}-"); PreReceiveDeepClean(baseLayerPrefix); // 1 - Unpack objects and proxies from root commit object - var unpackedRoot = _rootObjectUnpacker.Unpack(rootObject); + var unpackedRoot = rootObjectUnpacker.Unpack(rootObject); // 2 - Split atomic objects and instance components with their path - var (atomicObjects, instanceComponents) = _rootObjectUnpacker.SplitAtomicObjectsAndInstances( + var (atomicObjects, instanceComponents) = rootObjectUnpacker.SplitAtomicObjectsAndInstances( unpackedRoot.ObjectsToConvert ); - var atomicObjectsWithPath = _layerBaker.GetAtomicObjectsWithPath(atomicObjects); - var instanceComponentsWithPath = _layerBaker.GetInstanceComponentsWithPath(instanceComponents); + var atomicObjectsWithPath = layerBaker.GetAtomicObjectsWithPath(atomicObjects); + var instanceComponentsWithPath = layerBaker.GetInstanceComponentsWithPath(instanceComponents); // POC: these are not captured by traversal, so we need to re-add them here if (unpackedRoot.DefinitionProxies != null && unpackedRoot.DefinitionProxies.Count > 0) @@ -113,7 +71,7 @@ IProgress onOperationProgressed // 3 - Bake materials and colors, as they are used later down the line by layers and objects if (unpackedRoot.RenderMaterialProxies != null) { - _materialBaker.ParseAndBakeRenderMaterials( + materialBaker.ParseAndBakeRenderMaterials( unpackedRoot.RenderMaterialProxies, baseLayerPrefix, onOperationProgressed @@ -122,7 +80,7 @@ IProgress onOperationProgressed if (unpackedRoot.ColorProxies != null) { - _colorBaker.ParseColors(unpackedRoot.ColorProxies, onOperationProgressed); + colorBaker.ParseColors(unpackedRoot.ColorProxies, onOperationProgressed); } // 5 - Convert atomic objects @@ -134,6 +92,7 @@ IProgress onOperationProgressed { string objectId = atomicObject.applicationId ?? atomicObject.id.NotNull(); onOperationProgressed.Report(new("Converting objects", (double)++count / atomicObjects.Count)); + cancellationToken.ThrowIfCancellationRequested(); try { IReadOnlyCollection convertedObjects = ConvertObject(atomicObject, layerPath, baseLayerPrefix); @@ -158,7 +117,7 @@ IProgress onOperationProgressed } // 6 - Convert instances - var (createdInstanceIds, consumedObjectIds, instanceConversionResults) = _instanceBaker.BakeInstances( + var (createdInstanceIds, consumedObjectIds, instanceConversionResults) = instanceBaker.BakeInstances( instanceComponentsWithPath, applicationIdMap, baseLayerPrefix, @@ -173,7 +132,7 @@ IProgress onOperationProgressed // 7 - Create groups if (unpackedRoot.GroupProxies != null) { - IReadOnlyCollection groupResults = _groupBaker.CreateGroups( + IReadOnlyCollection groupResults = groupBaker.CreateGroups( unpackedRoot.GroupProxies, applicationIdMap ); @@ -185,20 +144,20 @@ IProgress onOperationProgressed private void PreReceiveDeepClean(string baseLayerPrefix) { - _layerBaker.DeleteAllLayersByPrefix(baseLayerPrefix); - _instanceBaker.PurgeInstances(baseLayerPrefix); - _materialBaker.PurgeMaterials(baseLayerPrefix); + layerBaker.DeleteAllLayersByPrefix(baseLayerPrefix); + instanceBaker.PurgeInstances(baseLayerPrefix); + materialBaker.PurgeMaterials(baseLayerPrefix); } private IReadOnlyCollection ConvertObject(Base obj, Collection[] layerPath, string baseLayerNamePrefix) { - string layerName = _layerBaker.CreateLayerForReceive(layerPath, baseLayerNamePrefix); + string layerName = layerBaker.CreateLayerForReceive(layerPath, baseLayerNamePrefix); var convertedEntities = new HashSet(); using var tr = Application.DocumentManager.CurrentDocument.Database.TransactionManager.StartTransaction(); // 1: convert - var converted = _converter.Convert(obj); + var converted = converter.Convert(obj); // 2: handle result if (converted is Entity entity) @@ -219,12 +178,12 @@ private IReadOnlyCollection ConvertObject(Base obj, Collection[] layerPa private Entity BakeObject(Entity entity, Base originalObject, string layerName, Base? parentObject = null) { var objId = originalObject.applicationId ?? originalObject.id.NotNull(); - if (_colorBaker.ObjectColorsIdMap.TryGetValue(objId, out AutocadColor? color)) + if (colorBaker.ObjectColorsIdMap.TryGetValue(objId, out AutocadColor? color)) { entity.Color = color; } - if (_materialBaker.TryGetMaterialId(originalObject, parentObject, out ObjectId matId)) + if (materialBaker.TryGetMaterialId(originalObject, parentObject, out ObjectId matId)) { entity.MaterialId = matId; } @@ -259,7 +218,7 @@ string baseLayerName var groupDictionary = (DBDictionary) tr.GetObject(Application.DocumentManager.CurrentDocument.Database.GroupDictionaryId, OpenMode.ForWrite); - var groupName = _autocadContext.RemoveInvalidChars( + var groupName = autocadContext.RemoveInvalidChars( $@"{parentObject.speckle_type.Split('.').Last()} - {parentObject.applicationId ?? parentObject.id} ({baseLayerName})" ); diff --git a/Connectors/Autocad/Speckle.Connectors.AutocadShared/Operations/Send/AutocadRootObjectBaseBuilder.cs b/Connectors/Autocad/Speckle.Connectors.AutocadShared/Operations/Send/AutocadRootObjectBaseBuilder.cs index 9c8c77e15..62290cec2 100644 --- a/Connectors/Autocad/Speckle.Connectors.AutocadShared/Operations/Send/AutocadRootObjectBaseBuilder.cs +++ b/Connectors/Autocad/Speckle.Connectors.AutocadShared/Operations/Send/AutocadRootObjectBaseBuilder.cs @@ -49,13 +49,6 @@ ISdkActivityFactory activityFactory _activityFactory = activityFactory; } - public Task Build( - IReadOnlyList objects, - SendInfo sendInfo, - IProgress onOperationProgressed, - CancellationToken ct = default - ) => Task.FromResult(BuildSync(objects, sendInfo, onOperationProgressed, ct)); - [SuppressMessage( "Maintainability", "CA1506:Avoid excessive class coupling", @@ -65,11 +58,10 @@ It is already simplified but has many different references since it is a builder proxy classes yet. So I'm supressing this one now!!! """ )] - private RootObjectBuilderResult BuildSync( + public RootObjectBuilderResult Build( IReadOnlyList objects, SendInfo sendInfo, - IProgress onOperationProgressed, - CancellationToken ct = default + IProgress onOperationProgressed ) { // 0 - Init the root @@ -101,7 +93,6 @@ private RootObjectBuilderResult BuildSync( int count = 0; foreach (var (entity, applicationId) in atomicObjects) { - ct.ThrowIfCancellationRequested(); using (var convertActivity = _activityFactory.Start("Converting object")) { // Create and add a collection for this entity if not done so already. diff --git a/Connectors/Autocad/Speckle.Connectors.AutocadShared/Plugin/AutocadCommand.cs b/Connectors/Autocad/Speckle.Connectors.AutocadShared/Plugin/AutocadCommand.cs index f3c177540..cbc69d098 100644 --- a/Connectors/Autocad/Speckle.Connectors.AutocadShared/Plugin/AutocadCommand.cs +++ b/Connectors/Autocad/Speckle.Connectors.AutocadShared/Plugin/AutocadCommand.cs @@ -3,7 +3,6 @@ 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; @@ -48,7 +47,6 @@ public void Command() services.AddCivil3dConverters(); #endif Container = services.BuildServiceProvider(); - Container.UseDUI(); var panelWebView = Container.GetRequiredService(); diff --git a/Connectors/Autocad/Speckle.Connectors.Civil3d2022/packages.lock.json b/Connectors/Autocad/Speckle.Connectors.Civil3d2022/packages.lock.json index 069bbdc75..722d9e2cf 100644 --- a/Connectors/Autocad/Speckle.Connectors.Civil3d2022/packages.lock.json +++ b/Connectors/Autocad/Speckle.Connectors.Civil3d2022/packages.lock.json @@ -284,8 +284,7 @@ "Microsoft.Extensions.Logging.Abstractions": "[2.2.0, )", "Speckle.Connectors.Common": "[1.0.0, )", "Speckle.Sdk": "[3.1.0-dev.205, )", - "Speckle.Sdk.Dependencies": "[3.1.0-dev.205, )", - "System.Threading.Tasks.Dataflow": "[6.0.0, )" + "Speckle.Sdk.Dependencies": "[3.1.0-dev.205, )" } }, "speckle.connectors.dui.webview": { @@ -377,12 +376,6 @@ "requested": "[3.1.0-dev.205, )", "resolved": "3.1.0-dev.205", "contentHash": "MjWRxQhXYZxfpc1JwKc33jjBH3mLLxnbCNx5mcYAdGRJDFb/6ebHzQqWE1abxxCa5k1GJBbH8RpMscFm3w6oVQ==" - }, - "System.Threading.Tasks.Dataflow": { - "type": "CentralTransitive", - "requested": "[6.0.0, )", - "resolved": "6.0.0", - "contentHash": "+tyDCU3/B1lDdOOAJywHQoFwyXIUghIaP2BxG79uvhfTnO+D9qIgjVlL/JV2NTliYbMHpd6eKDmHp2VHpij7MA==" } } } diff --git a/Connectors/Autocad/Speckle.Connectors.Civil3d2023/packages.lock.json b/Connectors/Autocad/Speckle.Connectors.Civil3d2023/packages.lock.json index bc9ca77e1..55ee5c968 100644 --- a/Connectors/Autocad/Speckle.Connectors.Civil3d2023/packages.lock.json +++ b/Connectors/Autocad/Speckle.Connectors.Civil3d2023/packages.lock.json @@ -284,8 +284,7 @@ "Microsoft.Extensions.Logging.Abstractions": "[2.2.0, )", "Speckle.Connectors.Common": "[1.0.0, )", "Speckle.Sdk": "[3.1.0-dev.205, )", - "Speckle.Sdk.Dependencies": "[3.1.0-dev.205, )", - "System.Threading.Tasks.Dataflow": "[6.0.0, )" + "Speckle.Sdk.Dependencies": "[3.1.0-dev.205, )" } }, "speckle.connectors.dui.webview": { @@ -377,12 +376,6 @@ "requested": "[3.1.0-dev.205, )", "resolved": "3.1.0-dev.205", "contentHash": "MjWRxQhXYZxfpc1JwKc33jjBH3mLLxnbCNx5mcYAdGRJDFb/6ebHzQqWE1abxxCa5k1GJBbH8RpMscFm3w6oVQ==" - }, - "System.Threading.Tasks.Dataflow": { - "type": "CentralTransitive", - "requested": "[6.0.0, )", - "resolved": "6.0.0", - "contentHash": "+tyDCU3/B1lDdOOAJywHQoFwyXIUghIaP2BxG79uvhfTnO+D9qIgjVlL/JV2NTliYbMHpd6eKDmHp2VHpij7MA==" } } } diff --git a/Connectors/Autocad/Speckle.Connectors.Civil3d2024/packages.lock.json b/Connectors/Autocad/Speckle.Connectors.Civil3d2024/packages.lock.json index 54a2690e8..bb39e4f34 100644 --- a/Connectors/Autocad/Speckle.Connectors.Civil3d2024/packages.lock.json +++ b/Connectors/Autocad/Speckle.Connectors.Civil3d2024/packages.lock.json @@ -284,8 +284,7 @@ "Microsoft.Extensions.Logging.Abstractions": "[2.2.0, )", "Speckle.Connectors.Common": "[1.0.0, )", "Speckle.Sdk": "[3.1.0-dev.205, )", - "Speckle.Sdk.Dependencies": "[3.1.0-dev.205, )", - "System.Threading.Tasks.Dataflow": "[6.0.0, )" + "Speckle.Sdk.Dependencies": "[3.1.0-dev.205, )" } }, "speckle.connectors.dui.webview": { @@ -377,12 +376,6 @@ "requested": "[3.1.0-dev.205, )", "resolved": "3.1.0-dev.205", "contentHash": "MjWRxQhXYZxfpc1JwKc33jjBH3mLLxnbCNx5mcYAdGRJDFb/6ebHzQqWE1abxxCa5k1GJBbH8RpMscFm3w6oVQ==" - }, - "System.Threading.Tasks.Dataflow": { - "type": "CentralTransitive", - "requested": "[6.0.0, )", - "resolved": "6.0.0", - "contentHash": "+tyDCU3/B1lDdOOAJywHQoFwyXIUghIaP2BxG79uvhfTnO+D9qIgjVlL/JV2NTliYbMHpd6eKDmHp2VHpij7MA==" } } } diff --git a/Connectors/Autocad/Speckle.Connectors.Civil3d2025/packages.lock.json b/Connectors/Autocad/Speckle.Connectors.Civil3d2025/packages.lock.json index 3bf84afd1..d82ac8f81 100644 --- a/Connectors/Autocad/Speckle.Connectors.Civil3d2025/packages.lock.json +++ b/Connectors/Autocad/Speckle.Connectors.Civil3d2025/packages.lock.json @@ -240,8 +240,7 @@ "Microsoft.Extensions.Logging.Abstractions": "[2.2.0, )", "Speckle.Connectors.Common": "[1.0.0, )", "Speckle.Sdk": "[3.1.0-dev.205, )", - "Speckle.Sdk.Dependencies": "[3.1.0-dev.205, )", - "System.Threading.Tasks.Dataflow": "[6.0.0, )" + "Speckle.Sdk.Dependencies": "[3.1.0-dev.205, )" } }, "speckle.connectors.dui.webview": { @@ -333,12 +332,6 @@ "requested": "[3.1.0-dev.205, )", "resolved": "3.1.0-dev.205", "contentHash": "MjWRxQhXYZxfpc1JwKc33jjBH3mLLxnbCNx5mcYAdGRJDFb/6ebHzQqWE1abxxCa5k1GJBbH8RpMscFm3w6oVQ==" - }, - "System.Threading.Tasks.Dataflow": { - "type": "CentralTransitive", - "requested": "[6.0.0, )", - "resolved": "6.0.0", - "contentHash": "+tyDCU3/B1lDdOOAJywHQoFwyXIUghIaP2BxG79uvhfTnO+D9qIgjVlL/JV2NTliYbMHpd6eKDmHp2VHpij7MA==" } }, "net8.0-windows7.0/win-x64": { diff --git a/Connectors/Autocad/Speckle.Connectors.Civil3dShared/Bindings/Civil3dSendBinding.cs b/Connectors/Autocad/Speckle.Connectors.Civil3dShared/Bindings/Civil3dSendBinding.cs index a1aa571b0..3ff8e014e 100644 --- a/Connectors/Autocad/Speckle.Connectors.Civil3dShared/Bindings/Civil3dSendBinding.cs +++ b/Connectors/Autocad/Speckle.Connectors.Civil3dShared/Bindings/Civil3dSendBinding.cs @@ -4,8 +4,10 @@ using Speckle.Connectors.Autocad.HostApp; using Speckle.Connectors.Common.Caching; using Speckle.Connectors.Common.Cancellation; +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; @@ -32,7 +34,10 @@ public Civil3dSendBinding( ILogger logger, ICivil3dConversionSettingsFactory civil3dConversionSettingsFactory, IAutocadConversionSettingsFactory autocadConversionSettingsFactory, - ISpeckleApplication speckleApplication + ISpeckleApplication speckleApplication, + ITopLevelExceptionHandler topLevelExceptionHandler, + IThreadContext threadContext, + IEventAggregator eventAggregator ) : base( store, @@ -44,7 +49,10 @@ ISpeckleApplication speckleApplication sendConversionCache, operationProgressManager, logger, - speckleApplication + speckleApplication, + topLevelExceptionHandler, + threadContext, + eventAggregator ) { _civil3dConversionSettingsFactory = civil3dConversionSettingsFactory; diff --git a/Connectors/CSi/Speckle.Connectors.CSiShared/Operations/Send/CSiRootObjectBuilder.cs b/Connectors/CSi/Speckle.Connectors.CSiShared/Operations/Send/CSiRootObjectBuilder.cs index a5cb5c58c..88c64afd2 100644 --- a/Connectors/CSi/Speckle.Connectors.CSiShared/Operations/Send/CSiRootObjectBuilder.cs +++ b/Connectors/CSi/Speckle.Connectors.CSiShared/Operations/Send/CSiRootObjectBuilder.cs @@ -42,11 +42,10 @@ ICSiApplicationService csiApplicationService _csiApplicationService = csiApplicationService; } - public async Task Build( + public RootObjectBuilderResult Build( IReadOnlyList csiObjects, SendInfo sendInfo, - IProgress onOperationProgressed, - CancellationToken cancellationToken = default + IProgress onOperationProgressed ) { using var activity = _activityFactory.Start("Build"); @@ -63,7 +62,6 @@ public async Task Build( foreach (ICSiWrapper csiObject in csiObjects) { using var _2 = _activityFactory.Start("Convert"); - cancellationToken.ThrowIfCancellationRequested(); var result = ConvertCSiObject(csiObject, rootObjectCollection, sendInfo.ProjectId); results.Add(result); @@ -78,7 +76,6 @@ public async Task Build( throw new SpeckleException("Failed to convert all objects."); } - await Task.Yield(); return new RootObjectBuilderResult(rootObjectCollection, results); } diff --git a/Connectors/CSi/Speckle.Connectors.CSiShared/ServiceRegistration.cs b/Connectors/CSi/Speckle.Connectors.CSiShared/ServiceRegistration.cs index 994d0c75d..60448414a 100644 --- a/Connectors/CSi/Speckle.Connectors.CSiShared/ServiceRegistration.cs +++ b/Connectors/CSi/Speckle.Connectors.CSiShared/ServiceRegistration.cs @@ -2,6 +2,7 @@ using Speckle.Connectors.Common; using Speckle.Connectors.Common.Builders; using Speckle.Connectors.Common.Operations; +using Speckle.Connectors.Common.Threading; using Speckle.Connectors.CSiShared.Bindings; using Speckle.Connectors.CSiShared.Builders; using Speckle.Connectors.CSiShared.Filters; @@ -9,7 +10,6 @@ using Speckle.Connectors.DUI; using Speckle.Connectors.DUI.Bindings; using Speckle.Connectors.DUI.Bridge; -using Speckle.Connectors.DUI.Models; using Speckle.Connectors.DUI.Models.Card.SendFilter; using Speckle.Connectors.DUI.WebView; using Speckle.Converters.CSiShared; @@ -24,11 +24,9 @@ public static IServiceCollection AddCSi(this IServiceCollection services) services.AddSingleton(); services.AddConnectorUtils(); - services.AddDUI(); + services.AddDUI(); services.AddDUIView(); - services.AddSingleton(); - services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); @@ -45,8 +43,6 @@ public static IServiceCollection AddCSi(this IServiceCollection services) services.AddScoped, CSiRootObjectBuilder>(); services.AddScoped>(); - services.RegisterTopLevelExceptionHandler(); - return services; } } diff --git a/Connectors/CSi/Speckle.Connectors.ETABS21/packages.lock.json b/Connectors/CSi/Speckle.Connectors.ETABS21/packages.lock.json index 6b0cdbee0..04ddf8e78 100644 --- a/Connectors/CSi/Speckle.Connectors.ETABS21/packages.lock.json +++ b/Connectors/CSi/Speckle.Connectors.ETABS21/packages.lock.json @@ -275,8 +275,7 @@ "Microsoft.Extensions.Logging.Abstractions": "[2.2.0, )", "Speckle.Connectors.Common": "[1.0.0, )", "Speckle.Sdk": "[3.1.0-dev.205, )", - "Speckle.Sdk.Dependencies": "[3.1.0-dev.205, )", - "System.Threading.Tasks.Dataflow": "[6.0.0, )" + "Speckle.Sdk.Dependencies": "[3.1.0-dev.205, )" } }, "speckle.connectors.dui.webview": { @@ -366,12 +365,6 @@ "requested": "[3.1.0-dev.205, )", "resolved": "3.1.0-dev.205", "contentHash": "MjWRxQhXYZxfpc1JwKc33jjBH3mLLxnbCNx5mcYAdGRJDFb/6ebHzQqWE1abxxCa5k1GJBbH8RpMscFm3w6oVQ==" - }, - "System.Threading.Tasks.Dataflow": { - "type": "CentralTransitive", - "requested": "[6.0.0, )", - "resolved": "6.0.0", - "contentHash": "+tyDCU3/B1lDdOOAJywHQoFwyXIUghIaP2BxG79uvhfTnO+D9qIgjVlL/JV2NTliYbMHpd6eKDmHp2VHpij7MA==" } } } diff --git a/Connectors/CSi/Speckle.Connectors.ETABS22/packages.lock.json b/Connectors/CSi/Speckle.Connectors.ETABS22/packages.lock.json index 9cdaf3fd9..cde963ea9 100644 --- a/Connectors/CSi/Speckle.Connectors.ETABS22/packages.lock.json +++ b/Connectors/CSi/Speckle.Connectors.ETABS22/packages.lock.json @@ -231,8 +231,7 @@ "Microsoft.Extensions.Logging.Abstractions": "[2.2.0, )", "Speckle.Connectors.Common": "[1.0.0, )", "Speckle.Sdk": "[3.1.0-dev.205, )", - "Speckle.Sdk.Dependencies": "[3.1.0-dev.205, )", - "System.Threading.Tasks.Dataflow": "[6.0.0, )" + "Speckle.Sdk.Dependencies": "[3.1.0-dev.205, )" } }, "speckle.connectors.dui.webview": { @@ -321,12 +320,6 @@ "requested": "[3.1.0-dev.205, )", "resolved": "3.1.0-dev.205", "contentHash": "MjWRxQhXYZxfpc1JwKc33jjBH3mLLxnbCNx5mcYAdGRJDFb/6ebHzQqWE1abxxCa5k1GJBbH8RpMscFm3w6oVQ==" - }, - "System.Threading.Tasks.Dataflow": { - "type": "CentralTransitive", - "requested": "[6.0.0, )", - "resolved": "6.0.0", - "contentHash": "+tyDCU3/B1lDdOOAJywHQoFwyXIUghIaP2BxG79uvhfTnO+D9qIgjVlL/JV2NTliYbMHpd6eKDmHp2VHpij7MA==" } } } diff --git a/Connectors/Revit/Speckle.Connectors.Revit2022/packages.lock.json b/Connectors/Revit/Speckle.Connectors.Revit2022/packages.lock.json index 063cefcbf..d72b6e2ee 100644 --- a/Connectors/Revit/Speckle.Connectors.Revit2022/packages.lock.json +++ b/Connectors/Revit/Speckle.Connectors.Revit2022/packages.lock.json @@ -303,8 +303,7 @@ "Microsoft.Extensions.Logging.Abstractions": "[2.2.0, )", "Speckle.Connectors.Common": "[1.0.0, )", "Speckle.Sdk": "[3.1.0-dev.205, )", - "Speckle.Sdk.Dependencies": "[3.1.0-dev.205, )", - "System.Threading.Tasks.Dataflow": "[6.0.0, )" + "Speckle.Sdk.Dependencies": "[3.1.0-dev.205, )" } }, "speckle.connectors.logging": { @@ -388,12 +387,6 @@ "requested": "[3.1.0-dev.205, )", "resolved": "3.1.0-dev.205", "contentHash": "MjWRxQhXYZxfpc1JwKc33jjBH3mLLxnbCNx5mcYAdGRJDFb/6ebHzQqWE1abxxCa5k1GJBbH8RpMscFm3w6oVQ==" - }, - "System.Threading.Tasks.Dataflow": { - "type": "CentralTransitive", - "requested": "[6.0.0, )", - "resolved": "6.0.0", - "contentHash": "+tyDCU3/B1lDdOOAJywHQoFwyXIUghIaP2BxG79uvhfTnO+D9qIgjVlL/JV2NTliYbMHpd6eKDmHp2VHpij7MA==" } } } diff --git a/Connectors/Revit/Speckle.Connectors.Revit2023/packages.lock.json b/Connectors/Revit/Speckle.Connectors.Revit2023/packages.lock.json index 324783669..6720f0aeb 100644 --- a/Connectors/Revit/Speckle.Connectors.Revit2023/packages.lock.json +++ b/Connectors/Revit/Speckle.Connectors.Revit2023/packages.lock.json @@ -303,8 +303,7 @@ "Microsoft.Extensions.Logging.Abstractions": "[2.2.0, )", "Speckle.Connectors.Common": "[1.0.0, )", "Speckle.Sdk": "[3.1.0-dev.205, )", - "Speckle.Sdk.Dependencies": "[3.1.0-dev.205, )", - "System.Threading.Tasks.Dataflow": "[6.0.0, )" + "Speckle.Sdk.Dependencies": "[3.1.0-dev.205, )" } }, "speckle.connectors.logging": { @@ -388,12 +387,6 @@ "requested": "[3.1.0-dev.205, )", "resolved": "3.1.0-dev.205", "contentHash": "MjWRxQhXYZxfpc1JwKc33jjBH3mLLxnbCNx5mcYAdGRJDFb/6ebHzQqWE1abxxCa5k1GJBbH8RpMscFm3w6oVQ==" - }, - "System.Threading.Tasks.Dataflow": { - "type": "CentralTransitive", - "requested": "[6.0.0, )", - "resolved": "6.0.0", - "contentHash": "+tyDCU3/B1lDdOOAJywHQoFwyXIUghIaP2BxG79uvhfTnO+D9qIgjVlL/JV2NTliYbMHpd6eKDmHp2VHpij7MA==" } } } diff --git a/Connectors/Revit/Speckle.Connectors.Revit2024/packages.lock.json b/Connectors/Revit/Speckle.Connectors.Revit2024/packages.lock.json index e9725e5e0..2abd77e1c 100644 --- a/Connectors/Revit/Speckle.Connectors.Revit2024/packages.lock.json +++ b/Connectors/Revit/Speckle.Connectors.Revit2024/packages.lock.json @@ -303,8 +303,7 @@ "Microsoft.Extensions.Logging.Abstractions": "[2.2.0, )", "Speckle.Connectors.Common": "[1.0.0, )", "Speckle.Sdk": "[3.1.0-dev.205, )", - "Speckle.Sdk.Dependencies": "[3.1.0-dev.205, )", - "System.Threading.Tasks.Dataflow": "[6.0.0, )" + "Speckle.Sdk.Dependencies": "[3.1.0-dev.205, )" } }, "speckle.connectors.logging": { @@ -388,12 +387,6 @@ "requested": "[3.1.0-dev.205, )", "resolved": "3.1.0-dev.205", "contentHash": "MjWRxQhXYZxfpc1JwKc33jjBH3mLLxnbCNx5mcYAdGRJDFb/6ebHzQqWE1abxxCa5k1GJBbH8RpMscFm3w6oVQ==" - }, - "System.Threading.Tasks.Dataflow": { - "type": "CentralTransitive", - "requested": "[6.0.0, )", - "resolved": "6.0.0", - "contentHash": "+tyDCU3/B1lDdOOAJywHQoFwyXIUghIaP2BxG79uvhfTnO+D9qIgjVlL/JV2NTliYbMHpd6eKDmHp2VHpij7MA==" } } } diff --git a/Connectors/Revit/Speckle.Connectors.Revit2025/packages.lock.json b/Connectors/Revit/Speckle.Connectors.Revit2025/packages.lock.json index 852b645c2..306b37d6b 100644 --- a/Connectors/Revit/Speckle.Connectors.Revit2025/packages.lock.json +++ b/Connectors/Revit/Speckle.Connectors.Revit2025/packages.lock.json @@ -253,8 +253,7 @@ "Microsoft.Extensions.Logging.Abstractions": "[2.2.0, )", "Speckle.Connectors.Common": "[1.0.0, )", "Speckle.Sdk": "[3.1.0-dev.205, )", - "Speckle.Sdk.Dependencies": "[3.1.0-dev.205, )", - "System.Threading.Tasks.Dataflow": "[6.0.0, )" + "Speckle.Sdk.Dependencies": "[3.1.0-dev.205, )" } }, "speckle.connectors.dui.webview": { @@ -350,12 +349,6 @@ "requested": "[3.1.0-dev.205, )", "resolved": "3.1.0-dev.205", "contentHash": "MjWRxQhXYZxfpc1JwKc33jjBH3mLLxnbCNx5mcYAdGRJDFb/6ebHzQqWE1abxxCa5k1GJBbH8RpMscFm3w6oVQ==" - }, - "System.Threading.Tasks.Dataflow": { - "type": "CentralTransitive", - "requested": "[6.0.0, )", - "resolved": "6.0.0", - "contentHash": "+tyDCU3/B1lDdOOAJywHQoFwyXIUghIaP2BxG79uvhfTnO+D9qIgjVlL/JV2NTliYbMHpd6eKDmHp2VHpij7MA==" } }, "net8.0-windows7.0/win-x64": { diff --git a/Connectors/Revit/Speckle.Connectors.RevitShared.Cef/CefSharpPanel.xaml.cs b/Connectors/Revit/Speckle.Connectors.RevitShared.Cef/CefSharpPanel.xaml.cs index ae78681ed..98e98c88f 100644 --- a/Connectors/Revit/Speckle.Connectors.RevitShared.Cef/CefSharpPanel.xaml.cs +++ b/Connectors/Revit/Speckle.Connectors.RevitShared.Cef/CefSharpPanel.xaml.cs @@ -13,15 +13,9 @@ public CefSharpPanel() InitializeComponent(); } - public Task ExecuteScriptAsyncMethod(string script, CancellationToken cancellationToken) + public void ExecuteScript(string script) { - Browser.Dispatcher.Invoke( - () => Browser.ExecuteScriptAsync(script), - DispatcherPriority.Background, - cancellationToken - ); - - return Task.CompletedTask; + Browser.Dispatcher.Invoke(() => Browser.ExecuteScriptAsync(script), DispatcherPriority.Background); } public bool IsBrowserInitialized => Browser.IsBrowserInitialized; diff --git a/Connectors/Revit/Speckle.Connectors.RevitShared/Bindings/BasicConnectorBindingRevit.cs b/Connectors/Revit/Speckle.Connectors.RevitShared/Bindings/BasicConnectorBindingRevit.cs index 49f973536..df1f85198 100644 --- a/Connectors/Revit/Speckle.Connectors.RevitShared/Bindings/BasicConnectorBindingRevit.cs +++ b/Connectors/Revit/Speckle.Connectors.RevitShared/Bindings/BasicConnectorBindingRevit.cs @@ -1,9 +1,8 @@ using Autodesk.Revit.DB; -using Revit.Async; using Speckle.Connectors.DUI.Bridge; +using Speckle.Connectors.DUI.Eventing; using Speckle.Connectors.DUI.Models; using Speckle.Connectors.DUI.Models.Card; -using Speckle.Connectors.Revit.HostApp; using Speckle.Connectors.RevitShared; using Speckle.Connectors.RevitShared.Operations.Send.Filters; using Speckle.Converters.RevitShared.Helpers; @@ -20,30 +19,29 @@ internal sealed class BasicConnectorBindingRevit : IBasicConnectorBinding public BasicConnectorBindingCommands Commands { get; } - private readonly APIContext _apiContext; private readonly DocumentModelStore _store; private readonly RevitContext _revitContext; private readonly ISpeckleApplication _speckleApplication; public BasicConnectorBindingRevit( - APIContext apiContext, DocumentModelStore store, IBrowserBridge parent, RevitContext revitContext, - ISpeckleApplication speckleApplication + ISpeckleApplication speckleApplication, + IEventAggregator eventAggregator ) { Name = "baseBinding"; Parent = parent; - _apiContext = apiContext; _store = store; _revitContext = revitContext; _speckleApplication = speckleApplication; Commands = new BasicConnectorBindingCommands(parent); // POC: event binding? - _store.DocumentChanged += (_, _) => - parent.TopLevelExceptionHandler.FireAndForget(async () => + eventAggregator + .GetEvent() + .Subscribe(async _ => { await Commands.NotifyDocumentChanged().ConfigureAwait(false); }); @@ -97,21 +95,16 @@ public async Task HighlightModel(string modelCardId) { if (senderModelCard.SendFilter is IRevitSendFilter revitFilter) { - revitFilter.SetContext(_revitContext, _apiContext); + revitFilter.SetContext(_revitContext); } if (senderModelCard.SendFilter is RevitViewsFilter revitViewsFilter) { - await _apiContext - .Run(() => - { - var view = revitViewsFilter.GetView(); - if (view is not null) - { - _revitContext.UIApplication.ActiveUIDocument.ActiveView = view; - } - }) - .ConfigureAwait(false); + var view = revitViewsFilter.GetView(); + if (view is not null) + { + _revitContext.UIApplication.ActiveUIDocument.ActiveView = view; + } return; } @@ -142,45 +135,38 @@ await Commands return; } - await HighlightObjectsOnView(elementIds).ConfigureAwait(false); + HighlightObjectsOnView(elementIds); } /// /// Highlights the objects from the given ids. /// /// UniqueId's of the DB.Elements. - public async Task HighlightObjects(IReadOnlyList objectIds) + public Task HighlightObjects(IReadOnlyList objectIds) { var activeUIDoc = _revitContext.UIApplication?.ActiveUIDocument ?? throw new SpeckleException("Unable to retrieve active UI document"); - await HighlightObjectsOnView( - objectIds - .Select(uid => ElementIdHelper.GetElementIdFromUniqueId(activeUIDoc.Document, uid)) - .Where(el => el is not null) - .Cast() - .ToList() - ) - .ConfigureAwait(false); - ; + HighlightObjectsOnView( + objectIds + .Select(uid => ElementIdHelper.GetElementIdFromUniqueId(activeUIDoc.Document, uid)) + .Where(el => el is not null) + .Cast() + .ToList() + ); + return Task.CompletedTask; } - private async Task HighlightObjectsOnView(List objectIds) + private void HighlightObjectsOnView(List objectIds) { // POC: don't know if we can rely on storing the ActiveUIDocument, hence getting it each time var activeUIDoc = _revitContext.UIApplication?.ActiveUIDocument ?? throw new SpeckleException("Unable to retrieve active UI document"); - // UiDocument operations should be wrapped into RevitTask, otherwise doesn't work on other tasks. - await RevitTask - .RunAsync(() => - { - activeUIDoc.Selection.SetElementIds(objectIds); - activeUIDoc.ShowElements(objectIds); - }) - .ConfigureAwait(false); + activeUIDoc.Selection.SetElementIds(objectIds); + activeUIDoc.ShowElements(objectIds); ; } } diff --git a/Connectors/Revit/Speckle.Connectors.RevitShared/Bindings/RevitSendBinding.cs b/Connectors/Revit/Speckle.Connectors.RevitShared/Bindings/RevitSendBinding.cs index c491b7fac..c3fff1c6d 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; @@ -16,7 +17,6 @@ using Speckle.Connectors.DUI.Settings; using Speckle.Connectors.Revit.HostApp; using Speckle.Connectors.Revit.Operations.Send.Settings; -using Speckle.Connectors.Revit.Plugin; using Speckle.Connectors.RevitShared.Operations.Send.Filters; using Speckle.Converters.Common; using Speckle.Converters.RevitShared.Helpers; @@ -28,8 +28,7 @@ namespace Speckle.Connectors.Revit.Bindings; internal sealed class RevitSendBinding : RevitBaseBinding, ISendBinding { - private readonly IRevitIdleManager _idleManager; - private readonly APIContext _apiContext; + private readonly IAppIdleManager _idleManager; private readonly CancellationManager _cancellationManager; private readonly IServiceProvider _serviceProvider; private readonly ISendConversionCache _sendConversionCache; @@ -49,9 +48,8 @@ internal sealed class RevitSendBinding : RevitBaseBinding, ISendBinding private ConcurrentDictionary ChangedObjectIds { get; set; } = new(); public RevitSendBinding( - IRevitIdleManager idleManager, + IAppIdleManager idleManager, RevitContext revitContext, - APIContext apiContext, DocumentModelStore store, CancellationManager cancellationManager, IBrowserBridge bridge, @@ -62,12 +60,13 @@ public RevitSendBinding( ILogger logger, ElementUnpacker elementUnpacker, IRevitConversionSettingsFactory revitConversionSettingsFactory, - ISpeckleApplication speckleApplication + ISpeckleApplication speckleApplication, + IEventAggregator eventAggregator, + ITopLevelExceptionHandler topLevelExceptionHandler ) : base("sendBinding", store, bridge, revitContext) { _idleManager = idleManager; - _apiContext = apiContext; _cancellationManager = cancellationManager; _serviceProvider = serviceProvider; _sendConversionCache = sendConversionCache; @@ -77,25 +76,26 @@ ISpeckleApplication speckleApplication _elementUnpacker = elementUnpacker; _revitConversionSettingsFactory = revitConversionSettingsFactory; _speckleApplication = speckleApplication; - var topLevelExceptionHandler = Parent.TopLevelExceptionHandler; Commands = new SendBindingUICommands(bridge); // TODO expiry events // TODO filters need refresh events - _idleManager.RunAsync(() => - { - revitContext.UIApplication.NotNull().Application.DocumentChanged += (_, e) => - topLevelExceptionHandler.CatchUnhandled(() => DocChangeHandler(e)); - }); - Store.DocumentChanged += (_, _) => - topLevelExceptionHandler.FireAndForget(async () => await OnDocumentChanged().ConfigureAwait(false)); + + revitContext.UIApplication.NotNull().Application.DocumentChanged += (_, e) => + topLevelExceptionHandler.CatchUnhandled(() => DocChangeHandler(e)); + eventAggregator + .GetEvent() + .Subscribe(async _ => + { + await OnDocumentChanged().ConfigureAwait(false); + }); } public List GetSendFilters() => [ new RevitSelectionFilter() { IsDefault = true }, - new RevitViewsFilter(RevitContext, _apiContext), - new RevitCategoriesFilter(RevitContext, _apiContext) + new RevitViewsFilter(RevitContext), + new RevitCategoriesFilter(RevitContext) ]; public List GetSendSettings() => @@ -182,12 +182,10 @@ private async Task> RefreshElementsOnSender(SenderModelCard modelC if (modelCard.SendFilter is IRevitSendFilter viewFilter) { - viewFilter.SetContext(RevitContext, _apiContext); + viewFilter.SetContext(RevitContext); } - var selectedObjects = await _apiContext - .Run(_ => modelCard.SendFilter.NotNull().RefreshObjectIds()) - .ConfigureAwait(false); + var selectedObjects = modelCard.SendFilter.NotNull().RefreshObjectIds(); List elements = selectedObjects .Select(uid => activeUIDoc.Document.GetElement(uid)) @@ -394,7 +392,7 @@ private async Task RunExpirationChecks() { if (modelCard.SendFilter is IRevitSendFilter viewFilter) { - viewFilter.SetContext(RevitContext, _apiContext); + viewFilter.SetContext(RevitContext); } var selectedObjects = modelCard.SendFilter.NotNull().IdMap.NotNull().Values; diff --git a/Connectors/Revit/Speckle.Connectors.RevitShared/Bindings/SelectionBinding.cs b/Connectors/Revit/Speckle.Connectors.RevitShared/Bindings/SelectionBinding.cs index 174131ee2..a5903928f 100644 --- a/Connectors/Revit/Speckle.Connectors.RevitShared/Bindings/SelectionBinding.cs +++ b/Connectors/Revit/Speckle.Connectors.RevitShared/Bindings/SelectionBinding.cs @@ -1,7 +1,6 @@ using Speckle.Connectors.DUI.Bindings; using Speckle.Connectors.DUI.Bridge; using Speckle.Connectors.DUI.Models; -using Speckle.Connectors.Revit.Plugin; using Speckle.Converters.RevitShared.Helpers; using Speckle.Sdk.Common; @@ -17,7 +16,8 @@ internal sealed class SelectionBinding : RevitBaseBinding, ISelectionBinding, ID public SelectionBinding( RevitContext revitContext, DocumentModelStore store, - IRevitIdleManager revitIdleManager, + IAppIdleManager revitIdleManager, + ITopLevelExceptionHandler topLevelExceptionHandler, IBrowserBridge parent ) : base("selectionBinding", store, parent, revitContext) @@ -25,14 +25,12 @@ 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 - revitIdleManager.RunAsync(() => - { - RevitContext.UIApplication.NotNull().SelectionChanged += (_, _) => - revitIdleManager.SubscribeToIdle(nameof(SelectionBinding), OnSelectionChanged); - }); + + RevitContext.UIApplication.NotNull().SelectionChanged += (_, _) => + revitIdleManager.SubscribeToIdle(nameof(SelectionBinding), OnSelectionChanged); #endif } diff --git a/Connectors/Revit/Speckle.Connectors.RevitShared/DependencyInjection/RevitConnectorModule.cs b/Connectors/Revit/Speckle.Connectors.RevitShared/DependencyInjection/RevitConnectorModule.cs index 8302eb18f..59dc9da5f 100644 --- a/Connectors/Revit/Speckle.Connectors.RevitShared/DependencyInjection/RevitConnectorModule.cs +++ b/Connectors/Revit/Speckle.Connectors.RevitShared/DependencyInjection/RevitConnectorModule.cs @@ -25,7 +25,7 @@ public static class ServiceRegistration public static void AddRevit(this IServiceCollection serviceCollection) { serviceCollection.AddConnectorUtils(); - serviceCollection.AddDUI(); + serviceCollection.AddDUI(); RegisterUiDependencies(serviceCollection); // Storage Schema @@ -41,9 +41,7 @@ public static void AddRevit(this IServiceCollection serviceCollection) serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); - - serviceCollection.RegisterTopLevelExceptionHandler(); + serviceCollection.AddSingleton(); serviceCollection.AddSingleton(sp => sp.GetRequiredService()); serviceCollection.AddSingleton(); @@ -69,9 +67,6 @@ public static void AddRevit(this IServiceCollection serviceCollection) // operation progress manager serviceCollection.AddSingleton(); - - // API context helps us to run functions on Revit UI Thread (main) - serviceCollection.AddSingleton(); } public static void RegisterUiDependencies(IServiceCollection serviceCollection) diff --git a/Connectors/Revit/Speckle.Connectors.RevitShared/HostApp/APIContext.cs b/Connectors/Revit/Speckle.Connectors.RevitShared/HostApp/APIContext.cs deleted file mode 100644 index 5260d0933..000000000 --- a/Connectors/Revit/Speckle.Connectors.RevitShared/HostApp/APIContext.cs +++ /dev/null @@ -1,130 +0,0 @@ -using Autodesk.Revit.UI; - -namespace Speckle.Connectors.Revit.HostApp; - -/// -/// This class gives access to the Revit API context from anywhere in your codebase. This is essentially a -/// lite version of the Revit.Async package from Kennan Chan. Most of the functionality was taken from that code. -/// The main difference is that this class does not subscribe to the applicationIdling event from revit -/// which the docs say will impact the performance of Revit -/// -public sealed class APIContext : IDisposable -{ - private readonly SemaphoreSlim _semaphore = new(1, 1); - private readonly UIControlledApplication _uiApplication; - private readonly ExternalEventHandler _factoryExternalEventHandler; -#pragma warning disable CA2213 - private readonly ExternalEvent _factoryExternalEvent; -#pragma warning restore CA2213 - - public APIContext(UIControlledApplication application) - { - _uiApplication = application; - _factoryExternalEventHandler = new(ExternalEvent.Create); - _factoryExternalEvent = ExternalEvent.Create(_factoryExternalEventHandler); - } - - public async Task Run(Func func) - { - await _semaphore.WaitAsync().ConfigureAwait(false); - try - { - var handler = new ExternalEventHandler(func); - using var externalEvent = await Run(_factoryExternalEventHandler, handler, _factoryExternalEvent) - .ConfigureAwait(false); - - return await Run(handler, _uiApplication, externalEvent).ConfigureAwait(false); - } - finally - { - _semaphore.Release(); - } - } - - public async Task Run(Action action) => - await Run(app => - { - action(app); - return null!; - }) - .ConfigureAwait(false); - - public async Task Run(Action action) => - await Run(_ => - { - action(); - return null!; - }) - .ConfigureAwait(false); - - private async Task Run( - ExternalEventHandler handler, - TParameter parameter, - ExternalEvent externalEvent - ) - { - var task = handler.GetTask(parameter); - externalEvent.Raise(); - - return await task.ConfigureAwait(false); - } - - public void Dispose() - { - _factoryExternalEvent.Dispose(); - _semaphore.Dispose(); - } -} - -public enum HandlerStatus -{ - NotStarted, - Started, - IsCompleted, - IsFaulted, -} - -internal sealed class ExternalEventHandler : IExternalEventHandler -{ - private TaskCompletionSource Result { get; set; } - - public Task GetTask(TParameter parameter) - { - Parameter = parameter; - Result = new TaskCompletionSource(); - return Result.Task; - } - - private readonly Func _func; - - public ExternalEventHandler(Func func) - { - this._func = func; - } - - public HandlerStatus Status { get; private set; } = HandlerStatus.NotStarted; - private TParameter Parameter { get; set; } - - [System.Diagnostics.CodeAnalysis.SuppressMessage( - "Design", - "CA1031:Do not catch general exception types", - Justification = "This is a very generic utility method for running things in a Revit context. If the result of the Run method is awaited, then the exception caught here will be raised there." - )] - public void Execute(UIApplication app) - { - Status = HandlerStatus.Started; - try - { - var r = _func(Parameter); - Result.SetResult(r); - Status = HandlerStatus.IsCompleted; - } - catch (Exception ex) - { - Status = HandlerStatus.IsFaulted; - Result.SetException(ex); - } - } - - public string GetName() => "SpeckleRevitContextEventHandler"; -} diff --git a/Connectors/Revit/Speckle.Connectors.RevitShared/HostApp/RevitDocumentStore.cs b/Connectors/Revit/Speckle.Connectors.RevitShared/HostApp/RevitDocumentStore.cs index d3254822e..8ddf7aa58 100644 --- a/Connectors/Revit/Speckle.Connectors.RevitShared/HostApp/RevitDocumentStore.cs +++ b/Connectors/Revit/Speckle.Connectors.RevitShared/HostApp/RevitDocumentStore.cs @@ -2,11 +2,10 @@ using Autodesk.Revit.DB.ExtensibleStorage; using Autodesk.Revit.UI; using Autodesk.Revit.UI.Events; -using Revit.Async; using Speckle.Connectors.DUI.Bridge; +using Speckle.Connectors.DUI.Eventing; using Speckle.Connectors.DUI.Models; using Speckle.Connectors.DUI.Utils; -using Speckle.Connectors.Revit.Plugin; using Speckle.Converters.RevitShared.Helpers; using Speckle.Sdk.Common; @@ -19,16 +18,18 @@ internal sealed class RevitDocumentStore : DocumentModelStore private static readonly Guid s_revitDocumentStoreId = new("D35B3695-EDC9-4E15-B62A-D3FC2CB83FA3"); private readonly RevitContext _revitContext; - private readonly IRevitIdleManager _idleManager; + private readonly IAppIdleManager _idleManager; private readonly DocumentModelStorageSchema _documentModelStorageSchema; private readonly IdStorageSchema _idStorageSchema; + private readonly IEventAggregator _eventAggregator; public RevitDocumentStore( - IRevitIdleManager idleManager, + IAppIdleManager idleManager, RevitContext revitContext, IJsonSerializer jsonSerializer, DocumentModelStorageSchema documentModelStorageSchema, IdStorageSchema idStorageSchema, + IEventAggregator eventAggregator, ITopLevelExceptionHandler topLevelExceptionHandler ) : base(jsonSerializer) @@ -37,24 +38,23 @@ ITopLevelExceptionHandler topLevelExceptionHandler _revitContext = revitContext; _documentModelStorageSchema = documentModelStorageSchema; _idStorageSchema = idStorageSchema; + _eventAggregator = eventAggregator; - _idleManager.RunAsync(() => - { - UIApplication uiApplication = _revitContext.UIApplication.NotNull(); + UIApplication uiApplication = _revitContext.UIApplication.NotNull(); - uiApplication.ViewActivated += (s, e) => topLevelExceptionHandler.CatchUnhandled(() => OnViewActivated(s, e)); + uiApplication.ViewActivated += (s, e) => topLevelExceptionHandler.CatchUnhandled(() => OnViewActivated(s, e)); - uiApplication.Application.DocumentOpening += (_, _) => - topLevelExceptionHandler.CatchUnhandled(() => IsDocumentInit = false); + uiApplication.Application.DocumentOpening += (_, _) => + topLevelExceptionHandler.CatchUnhandled(() => IsDocumentInit = false); - uiApplication.Application.DocumentOpened += (_, _) => - topLevelExceptionHandler.CatchUnhandled(() => IsDocumentInit = false); - }); + uiApplication.Application.DocumentOpened += (_, _) => + topLevelExceptionHandler.CatchUnhandled(() => IsDocumentInit = false); // 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(); + + eventAggregator.GetEvent().Publish(new object()); } /// @@ -79,7 +79,7 @@ private void OnViewActivated(object? _, ViewActivatedEventArgs e) () => { LoadState(); - OnDocumentChanged(); + _eventAggregator.GetEvent().Publish(new object()); } ); } @@ -92,23 +92,21 @@ protected override void HostAppSaveState(string modelCardState) { return; } - RevitTask.RunAsync(() => - { - var doc = (_revitContext.UIApplication?.ActiveUIDocument?.Document).NotNull(); - using Transaction t = new(doc, "Speckle Write State"); - t.Start(); - using DataStorage ds = GetSettingsDataStorage(doc) ?? DataStorage.Create(doc); - using Entity stateEntity = new(_documentModelStorageSchema.GetSchema()); - stateEntity.Set("contents", modelCardState); + using Transaction t = new(doc, "Speckle Write State"); + t.Start(); + using DataStorage ds = GetSettingsDataStorage(doc) ?? DataStorage.Create(doc); + + using Entity stateEntity = new(_documentModelStorageSchema.GetSchema()); + string serializedModels = Serialize(); + stateEntity.Set("contents", serializedModels); - using Entity idEntity = new(_idStorageSchema.GetSchema()); - idEntity.Set("Id", s_revitDocumentStoreId); + using Entity idEntity = new(_idStorageSchema.GetSchema()); + idEntity.Set("Id", s_revitDocumentStoreId); - ds.SetEntity(idEntity); - ds.SetEntity(stateEntity); - t.Commit(); - }); + ds.SetEntity(idEntity); + ds.SetEntity(stateEntity); + t.Commit(); } protected override void LoadState() diff --git a/Connectors/Revit/Speckle.Connectors.RevitShared/Operations/Receive/RevitHostObjectBuilder.cs b/Connectors/Revit/Speckle.Connectors.RevitShared/Operations/Receive/RevitHostObjectBuilder.cs index 24258ecc7..f44c230d0 100644 --- a/Connectors/Revit/Speckle.Connectors.RevitShared/Operations/Receive/RevitHostObjectBuilder.cs +++ b/Connectors/Revit/Speckle.Connectors.RevitShared/Operations/Receive/RevitHostObjectBuilder.cs @@ -1,6 +1,5 @@ using Autodesk.Revit.DB; using Microsoft.Extensions.Logging; -using Revit.Async; using Speckle.Connectors.Common.Builders; using Speckle.Connectors.Common.Conversion; using Speckle.Connectors.Common.Instances; @@ -24,64 +23,24 @@ namespace Speckle.Connectors.Revit.Operations.Receive; -internal sealed class RevitHostObjectBuilder : IHostObjectBuilder, IDisposable -{ - private readonly IRootToHostConverter _converter; - private readonly IConverterSettingsStore _converterSettings; - private readonly RevitToHostCacheSingleton _revitToHostCacheSingleton; - private readonly ITransactionManager _transactionManager; - private readonly ILocalToGlobalUnpacker _localToGlobalUnpacker; - private readonly RevitGroupBaker _groupBaker; - private readonly RevitMaterialBaker _materialBaker; - private readonly ILogger _logger; - private readonly ITypedConverter< +public sealed class RevitHostObjectBuilder( + IRootToHostConverter converter, + IConverterSettingsStore converterSettings, + ITransactionManager transactionManager, + ISdkActivityFactory activityFactory, + ILocalToGlobalUnpacker localToGlobalUnpacker, + RevitGroupBaker groupManager, + RevitMaterialBaker materialBaker, + RootObjectUnpacker rootObjectUnpacker, + ILogger logger, + RevitToHostCacheSingleton revitToHostCacheSingleton, + ITypedConverter< (Base atomicObject, IReadOnlyCollection matrix), DirectShape - > _localToGlobalDirectShapeConverter; - - private readonly RootObjectUnpacker _rootObjectUnpacker; - private readonly ISdkActivityFactory _activityFactory; - - public RevitHostObjectBuilder( - IRootToHostConverter converter, - IConverterSettingsStore converterSettings, - ITransactionManager transactionManager, - ISdkActivityFactory activityFactory, - ILocalToGlobalUnpacker localToGlobalUnpacker, - RevitGroupBaker groupManager, - RevitMaterialBaker materialBaker, - RootObjectUnpacker rootObjectUnpacker, - ILogger logger, - RevitToHostCacheSingleton revitToHostCacheSingleton, - ITypedConverter< - (Base atomicObject, IReadOnlyCollection matrix), - DirectShape - > localToGlobalDirectShapeConverter - ) - { - _converter = converter; - _converterSettings = converterSettings; - _transactionManager = transactionManager; - _localToGlobalUnpacker = localToGlobalUnpacker; - _groupBaker = groupManager; - _materialBaker = materialBaker; - _rootObjectUnpacker = rootObjectUnpacker; - _logger = logger; - _revitToHostCacheSingleton = revitToHostCacheSingleton; - _localToGlobalDirectShapeConverter = localToGlobalDirectShapeConverter; - _activityFactory = activityFactory; - } - - public Task Build( - Base rootObject, - string projectName, - string modelName, - IProgress onOperationProgressed, - CancellationToken cancellationToken - ) => - RevitTask.RunAsync(() => BuildSync(rootObject, projectName, modelName, onOperationProgressed, cancellationToken)); - - private HostObjectBuilderResult BuildSync( + > localToGlobalDirectShapeConverter +) : IHostObjectBuilder, IDisposable +{ + public HostObjectBuilderResult Build( Base rootObject, string projectName, string modelName, @@ -92,27 +51,27 @@ CancellationToken cancellationToken var baseGroupName = $"Project {projectName}: Model {modelName}"; // TODO: unify this across connectors! onOperationProgressed.Report(new("Converting", null)); - using var activity = _activityFactory.Start("Build"); + using var activity = activityFactory.Start("Build"); // 0 - Clean then Rock n Roll! 🎸 { - _activityFactory.Start("Pre receive clean"); - _transactionManager.StartTransaction(true, "Pre receive clean"); + activityFactory.Start("Pre receive clean"); + transactionManager.StartTransaction(true, "Pre receive clean"); try { PreReceiveDeepClean(baseGroupName); } catch (Exception ex) when (!ex.IsFatal()) { - _logger.LogError(ex, "Failed to clean up before receive in Revit"); + logger.LogError(ex, "Failed to clean up before receive in Revit"); } - _transactionManager.CommitTransaction(); + transactionManager.CommitTransaction(); } // 1 - Unpack objects and proxies from root commit object - var unpackedRoot = _rootObjectUnpacker.Unpack(rootObject); - var localToGlobalMaps = _localToGlobalUnpacker.Unpack( + var unpackedRoot = rootObjectUnpacker.Unpack(rootObject); + var localToGlobalMaps = localToGlobalUnpacker.Unpack( unpackedRoot.DefinitionProxies, unpackedRoot.ObjectsToConvert.ToList() ); @@ -120,14 +79,14 @@ CancellationToken cancellationToken // 2 - Bake materials if (unpackedRoot.RenderMaterialProxies != null) { - _transactionManager.StartTransaction(true, "Baking materials"); - _materialBaker.MapLayersRenderMaterials(unpackedRoot); - var map = _materialBaker.BakeMaterials(unpackedRoot.RenderMaterialProxies, baseGroupName); + transactionManager.StartTransaction(true, "Baking materials"); + materialBaker.MapLayersRenderMaterials(unpackedRoot); + var map = materialBaker.BakeMaterials(unpackedRoot.RenderMaterialProxies, baseGroupName); foreach (var kvp in map) { - _revitToHostCacheSingleton.MaterialsByObjectId.Add(kvp.Key, kvp.Value); + revitToHostCacheSingleton.MaterialsByObjectId.Add(kvp.Key, kvp.Value); } - _transactionManager.CommitTransaction(); + transactionManager.CommitTransaction(); } // 3 - Bake objects @@ -136,26 +95,26 @@ CancellationToken cancellationToken List<(DirectShape res, string applicationId)> postBakePaintTargets ) conversionResults; { - using var _ = _activityFactory.Start("Baking objects"); - _transactionManager.StartTransaction(true, "Baking objects"); + using var _ = activityFactory.Start("Baking objects"); + transactionManager.StartTransaction(true, "Baking objects"); conversionResults = BakeObjects(localToGlobalMaps, onOperationProgressed, cancellationToken); - _transactionManager.CommitTransaction(); + transactionManager.CommitTransaction(); } // 4 - Paint solids { - using var _ = _activityFactory.Start("Painting solids"); - _transactionManager.StartTransaction(true, "Painting solids"); + using var _ = activityFactory.Start("Painting solids"); + transactionManager.StartTransaction(true, "Painting solids"); PostBakePaint(conversionResults.postBakePaintTargets); - _transactionManager.CommitTransaction(); + transactionManager.CommitTransaction(); } // 5 - Create group { - using var _ = _activityFactory.Start("Grouping"); - _transactionManager.StartTransaction(true, "Grouping"); - _groupBaker.BakeGroupForTopLevel(baseGroupName); - _transactionManager.CommitTransaction(); + using var _ = activityFactory.Start("Grouping"); + transactionManager.StartTransaction(true, "Grouping"); + groupManager.BakeGroupForTopLevel(baseGroupName); + transactionManager.CommitTransaction(); } return conversionResults.builderResult; @@ -170,7 +129,7 @@ CancellationToken cancellationToken CancellationToken cancellationToken ) { - using var _ = _activityFactory.Start("BakeObjects"); + using var _ = activityFactory.Start("BakeObjects"); var conversionResults = new List(); var bakedObjectIds = new List(); int count = 0; @@ -182,7 +141,7 @@ CancellationToken cancellationToken cancellationToken.ThrowIfCancellationRequested(); try { - using var activity = _activityFactory.Start("BakeObject"); + using var activity = activityFactory.Start("BakeObject"); // POC hack of the ages: try to pre transform curves, points and meshes before baking // we need to bypass the local to global converter as there we don't have access to what we want. that service will/should stop existing. @@ -206,17 +165,17 @@ localToGlobalMap.AtomicObject is ITransformable transformable // and ICurve } // actual conversion happens here! - var result = _converter.Convert(localToGlobalMap.AtomicObject); + var result = converter.Convert(localToGlobalMap.AtomicObject); onOperationProgressed.Report(new("Converting", (double)++count / localToGlobalMaps.Count)); if (result is DirectShapeDefinitionWrapper) { // direct shape creation happens here - DirectShape directShapes = _localToGlobalDirectShapeConverter.Convert( + DirectShape directShapes = localToGlobalDirectShapeConverter.Convert( (localToGlobalMap.AtomicObject, localToGlobalMap.Matrix) ); bakedObjectIds.Add(directShapes.UniqueId); - _groupBaker.AddToTopLevelGroup(directShapes); + groupManager.AddToTopLevelGroup(directShapes); if (localToGlobalMap.AtomicObject is IRawEncodedObject and Base myBase) { @@ -235,7 +194,7 @@ localToGlobalMap.AtomicObject is ITransformable transformable // and ICurve catch (Exception ex) when (!ex.IsFatal()) { conversionResults.Add(new(Status.ERROR, localToGlobalMap.AtomicObject, null, null, ex)); - _logger.LogError(ex, $"Failed to convert object of type {localToGlobalMap.AtomicObject.speckle_type}"); + logger.LogError(ex, $"Failed to convert object of type {localToGlobalMap.AtomicObject.speckle_type}"); } } return (new(bakedObjectIds, conversionResults), postBakePaintTargets); @@ -251,7 +210,7 @@ private void PostBakePaint(List<(DirectShape res, string applicationId)> paintTa { var elGeometry = res.get_Geometry(new Options() { DetailLevel = ViewDetailLevel.Undefined }); var materialId = ElementId.InvalidElementId; - if (_revitToHostCacheSingleton.MaterialsByObjectId.TryGetValue(applicationId, out var mappedElementId)) + if (revitToHostCacheSingleton.MaterialsByObjectId.TryGetValue(applicationId, out var mappedElementId)) { materialId = mappedElementId; } @@ -268,7 +227,7 @@ private void PostBakePaint(List<(DirectShape res, string applicationId)> paintTa { foreach (Face face in s.Faces) { - _converterSettings.Current.Document.Paint(res.Id, face, materialId); + converterSettings.Current.Document.Paint(res.Id, face, materialId); } } } @@ -277,12 +236,12 @@ private void PostBakePaint(List<(DirectShape res, string applicationId)> paintTa private void PreReceiveDeepClean(string baseGroupName) { - DirectShapeLibrary.GetDirectShapeLibrary(_converterSettings.Current.Document).Reset(); // Note: this needs to be cleared, as it is being used in the converter + DirectShapeLibrary.GetDirectShapeLibrary(converterSettings.Current.Document).Reset(); // Note: this needs to be cleared, as it is being used in the converter - _revitToHostCacheSingleton.MaterialsByObjectId.Clear(); // Massive hack! - _groupBaker.PurgeGroups(baseGroupName); - _materialBaker.PurgeMaterials(baseGroupName); + revitToHostCacheSingleton.MaterialsByObjectId.Clear(); // Massive hack! + groupManager.PurgeGroups(baseGroupName); + materialBaker.PurgeMaterials(baseGroupName); } - public void Dispose() => _transactionManager?.Dispose(); + public void Dispose() => transactionManager?.Dispose(); } diff --git a/Connectors/Revit/Speckle.Connectors.RevitShared/Operations/Send/Filters/IRevitSendFilter.cs b/Connectors/Revit/Speckle.Connectors.RevitShared/Operations/Send/Filters/IRevitSendFilter.cs index 8895b357b..fe576b8b3 100644 --- a/Connectors/Revit/Speckle.Connectors.RevitShared/Operations/Send/Filters/IRevitSendFilter.cs +++ b/Connectors/Revit/Speckle.Connectors.RevitShared/Operations/Send/Filters/IRevitSendFilter.cs @@ -1,9 +1,8 @@ -using Speckle.Connectors.Revit.HostApp; -using Speckle.Converters.RevitShared.Helpers; +using Speckle.Converters.RevitShared.Helpers; namespace Speckle.Connectors.RevitShared.Operations.Send.Filters; public interface IRevitSendFilter { - public void SetContext(RevitContext revitContext, APIContext apiContext); + public void SetContext(RevitContext revitContext); } diff --git a/Connectors/Revit/Speckle.Connectors.RevitShared/Operations/Send/Filters/RevitCategoriesFilter.cs b/Connectors/Revit/Speckle.Connectors.RevitShared/Operations/Send/Filters/RevitCategoriesFilter.cs index b1cd74db7..25e3d017c 100644 --- a/Connectors/Revit/Speckle.Connectors.RevitShared/Operations/Send/Filters/RevitCategoriesFilter.cs +++ b/Connectors/Revit/Speckle.Connectors.RevitShared/Operations/Send/Filters/RevitCategoriesFilter.cs @@ -2,7 +2,6 @@ using Speckle.Connectors.DUI.Exceptions; using Speckle.Connectors.DUI.Models.Card.SendFilter; using Speckle.Connectors.DUI.Utils; -using Speckle.Connectors.Revit.HostApp; using Speckle.Converters.RevitShared.Helpers; namespace Speckle.Connectors.RevitShared.Operations.Send.Filters; @@ -12,7 +11,6 @@ public record CategoryData(string Name, string Id); public class RevitCategoriesFilter : DiscriminatedObject, ISendFilter, IRevitSendFilter { private RevitContext _revitContext; - private APIContext _apiContext; private Document? _doc; public string Id { get; set; } = "revitCategories"; public string Name { get; set; } = "Categories"; @@ -25,10 +23,9 @@ public class RevitCategoriesFilter : DiscriminatedObject, ISendFilter, IRevitSen public RevitCategoriesFilter() { } - public RevitCategoriesFilter(RevitContext revitContext, APIContext apiContext) + public RevitCategoriesFilter(RevitContext revitContext) { _revitContext = revitContext; - _apiContext = apiContext; _doc = _revitContext.UIApplication?.ActiveUIDocument.Document; GetCategories(); @@ -82,10 +79,9 @@ private void GetCategories() /// NOTE: this is needed since we need doc on `GetObjectIds()` function after it deserialized. /// DI doesn't help here to pass RevitContext from constructor. /// - public void SetContext(RevitContext revitContext, APIContext apiContext) + public void SetContext(RevitContext revitContext) { _revitContext = revitContext; - _apiContext = apiContext; _doc = _revitContext.UIApplication?.ActiveUIDocument.Document; } } diff --git a/Connectors/Revit/Speckle.Connectors.RevitShared/Operations/Send/Filters/RevitViewsFilter.cs b/Connectors/Revit/Speckle.Connectors.RevitShared/Operations/Send/Filters/RevitViewsFilter.cs index d98f46b80..d5ae8f0e9 100644 --- a/Connectors/Revit/Speckle.Connectors.RevitShared/Operations/Send/Filters/RevitViewsFilter.cs +++ b/Connectors/Revit/Speckle.Connectors.RevitShared/Operations/Send/Filters/RevitViewsFilter.cs @@ -2,7 +2,6 @@ using Speckle.Connectors.DUI.Exceptions; using Speckle.Connectors.DUI.Models.Card.SendFilter; using Speckle.Connectors.DUI.Utils; -using Speckle.Connectors.Revit.HostApp; using Speckle.Converters.RevitShared.Helpers; namespace Speckle.Connectors.RevitShared.Operations.Send.Filters; @@ -10,7 +9,6 @@ namespace Speckle.Connectors.RevitShared.Operations.Send.Filters; public class RevitViewsFilter : DiscriminatedObject, ISendFilter, IRevitSendFilter { private RevitContext _revitContext; - private APIContext _apiContext; private Document? _doc; public string Id { get; set; } = "revitViews"; public string Name { get; set; } = "Views"; @@ -23,10 +21,9 @@ public class RevitViewsFilter : DiscriminatedObject, ISendFilter, IRevitSendFilt public RevitViewsFilter() { } - public RevitViewsFilter(RevitContext revitContext, APIContext apiContext) + public RevitViewsFilter(RevitContext revitContext) { _revitContext = revitContext; - _apiContext = apiContext; _doc = _revitContext.UIApplication?.ActiveUIDocument.Document; GetViews(); @@ -100,10 +97,9 @@ private void GetViews() /// NOTE: this is needed since we need doc on `GetObjectIds()` function after it deserialized. /// DI doesn't help here to pass RevitContext from constructor. /// - public void SetContext(RevitContext revitContext, APIContext apiContext) + public void SetContext(RevitContext revitContext) { _revitContext = revitContext; - _apiContext = apiContext; _doc = _revitContext.UIApplication?.ActiveUIDocument.Document; } } diff --git a/Connectors/Revit/Speckle.Connectors.RevitShared/Operations/Send/RevitRootObjectBuilder.cs b/Connectors/Revit/Speckle.Connectors.RevitShared/Operations/Send/RevitRootObjectBuilder.cs index 2b374c37d..287214e2a 100644 --- a/Connectors/Revit/Speckle.Connectors.RevitShared/Operations/Send/RevitRootObjectBuilder.cs +++ b/Connectors/Revit/Speckle.Connectors.RevitShared/Operations/Send/RevitRootObjectBuilder.cs @@ -1,6 +1,5 @@ using Autodesk.Revit.DB; using Microsoft.Extensions.Logging; -using Revit.Async; using Speckle.Connectors.Common.Builders; using Speckle.Connectors.Common.Caching; using Speckle.Connectors.Common.Conversion; @@ -11,61 +10,31 @@ using Speckle.Converters.Common; using Speckle.Converters.RevitShared.Helpers; using Speckle.Converters.RevitShared.Settings; -using Speckle.Converters.RevitShared.ToSpeckle; using Speckle.Sdk; using Speckle.Sdk.Models; using Speckle.Sdk.Models.Collections; namespace Speckle.Connectors.Revit.Operations.Send; -public class RevitRootObjectBuilder : IRootObjectBuilder +public class RevitRootObjectBuilder( + IRootToSpeckleConverter converter, + IConverterSettingsStore converterSettings, + ISendConversionCache sendConversionCache, + ElementUnpacker elementUnpacker, + SendCollectionManager sendCollectionManager, + ILogger logger, + RevitToSpeckleCacheSingleton revitToSpeckleCacheSingleton +) : IRootObjectBuilder { // POC: SendSelection and RevitConversionContextStack should be interfaces, former needs interfaces - private readonly IRootToSpeckleConverter _converter; - private readonly IConverterSettingsStore _converterSettings; - private readonly ISendConversionCache _sendConversionCache; - private readonly ElementUnpacker _elementUnpacker; - private readonly SendCollectionManager _sendCollectionManager; - private readonly RevitToSpeckleCacheSingleton _revitToSpeckleCacheSingleton; - private readonly ILogger _logger; - private readonly ParameterDefinitionHandler _parameterDefinitionHandler; - - public RevitRootObjectBuilder( - IRootToSpeckleConverter converter, - IConverterSettingsStore converterSettings, - ISendConversionCache sendConversionCache, - ElementUnpacker elementUnpacker, - SendCollectionManager sendCollectionManager, - ILogger logger, - ParameterDefinitionHandler parameterDefinitionHandler, - RevitToSpeckleCacheSingleton revitToSpeckleCacheSingleton - ) - { - _converter = converter; - _converterSettings = converterSettings; - _sendConversionCache = sendConversionCache; - _elementUnpacker = elementUnpacker; - _sendCollectionManager = sendCollectionManager; - _revitToSpeckleCacheSingleton = revitToSpeckleCacheSingleton; - _logger = logger; - _parameterDefinitionHandler = parameterDefinitionHandler; - } - - public async Task Build( - IReadOnlyList objects, - SendInfo sendInfo, - IProgress onOperationProgressed, - CancellationToken ct = default - ) => await RevitTask.RunAsync(() => BuildSync(objects, sendInfo, onOperationProgressed, ct)).ConfigureAwait(false); - private RootObjectBuilderResult BuildSync( + public RootObjectBuilderResult Build( IReadOnlyList objects, SendInfo sendInfo, - IProgress onOperationProgressed, - CancellationToken ct = default + IProgress onOperationProgressed ) { - var doc = _converterSettings.Current.Document; + var doc = converterSettings.Current.Document; if (doc.IsFamilyDocument) { @@ -74,15 +43,15 @@ private RootObjectBuilderResult BuildSync( // 0 - Init the root Collection rootObject = - new() { name = _converterSettings.Current.Document.PathName.Split('\\').Last().Split('.').First() }; - rootObject["units"] = _converterSettings.Current.SpeckleUnits; + new() { name = converterSettings.Current.Document.PathName.Split('\\').Last().Split('.').First() }; + rootObject["units"] = converterSettings.Current.SpeckleUnits; var revitElements = new List(); // Convert ids to actual revit elements foreach (var id in objects) { - var el = _converterSettings.Current.Document.GetElement(id); + var el = converterSettings.Current.Document.GetElement(id); if (el != null) { revitElements.Add(el); @@ -97,38 +66,37 @@ private RootObjectBuilderResult BuildSync( List results = new(revitElements.Count); // Unpack groups (& other complex data structures) - var atomicObjects = _elementUnpacker.UnpackSelectionForConversion(revitElements).ToList(); + var atomicObjects = elementUnpacker.UnpackSelectionForConversion(revitElements).ToList(); var countProgress = 0; var cacheHitCount = 0; foreach (Element revitElement in atomicObjects) { - ct.ThrowIfCancellationRequested(); string applicationId = revitElement.UniqueId; string sourceType = revitElement.GetType().Name; try { Base converted; - if (_sendConversionCache.TryGetValue(sendInfo.ProjectId, applicationId, out ObjectReference? value)) + if (sendConversionCache.TryGetValue(sendInfo.ProjectId, applicationId, out ObjectReference? value)) { converted = value; cacheHitCount++; } else { - converted = _converter.Convert(revitElement); + converted = converter.Convert(revitElement); converted.applicationId = applicationId; } - var collection = _sendCollectionManager.GetAndCreateObjectHostCollection(revitElement, rootObject); + var collection = sendCollectionManager.GetAndCreateObjectHostCollection(revitElement, rootObject); collection.elements.Add(converted); results.Add(new(Status.SUCCESS, applicationId, sourceType, converted)); } catch (Exception ex) when (!ex.IsFatal()) { - _logger.LogSendConversionError(ex, sourceType); + logger.LogSendConversionError(ex, sourceType); results.Add(new(Status.ERROR, applicationId, sourceType, null, ex)); } @@ -140,8 +108,8 @@ private RootObjectBuilderResult BuildSync( throw new SpeckleException("Failed to convert all objects."); } - var idsAndSubElementIds = _elementUnpacker.GetElementsAndSubelementIdsFromAtomicObjects(atomicObjects); - var materialProxies = _revitToSpeckleCacheSingleton.GetRenderMaterialProxyListForObjects(idsAndSubElementIds); + var idsAndSubElementIds = elementUnpacker.GetElementsAndSubelementIdsFromAtomicObjects(atomicObjects); + var materialProxies = revitToSpeckleCacheSingleton.GetRenderMaterialProxyListForObjects(idsAndSubElementIds); rootObject[ProxyKeys.RENDER_MATERIAL] = materialProxies; // NOTE: these are currently not used anywhere, we'll skip them until someone calls for it back diff --git a/Connectors/Revit/Speckle.Connectors.RevitShared/Operations/Send/Settings/ToSpeckleSettingsManager.cs b/Connectors/Revit/Speckle.Connectors.RevitShared/Operations/Send/Settings/ToSpeckleSettingsManager.cs index 96ce3fde7..f1e55df9b 100644 --- a/Connectors/Revit/Speckle.Connectors.RevitShared/Operations/Send/Settings/ToSpeckleSettingsManager.cs +++ b/Connectors/Revit/Speckle.Connectors.RevitShared/Operations/Send/Settings/ToSpeckleSettingsManager.cs @@ -14,7 +14,6 @@ namespace Speckle.Connectors.Revit.Operations.Send.Settings; public class ToSpeckleSettingsManager : IToSpeckleSettingsManager { private readonly RevitContext _revitContext; - private readonly APIContext _apiContext; private readonly ISendConversionCache _sendConversionCache; private readonly ElementUnpacker _elementUnpacker; @@ -25,13 +24,11 @@ public class ToSpeckleSettingsManager : IToSpeckleSettingsManager public ToSpeckleSettingsManager( RevitContext revitContext, - APIContext apiContext, ISendConversionCache sendConversionCache, ElementUnpacker elementUnpacker ) { _revitContext = revitContext; - _apiContext = apiContext; _elementUnpacker = elementUnpacker; _sendConversionCache = sendConversionCache; } diff --git a/Connectors/Revit/Speckle.Connectors.RevitShared/Plugin/RevitExternalApplication.cs b/Connectors/Revit/Speckle.Connectors.RevitShared/Plugin/RevitExternalApplication.cs index 0cd5c1b60..3eb15365b 100644 --- a/Connectors/Revit/Speckle.Connectors.RevitShared/Plugin/RevitExternalApplication.cs +++ b/Connectors/Revit/Speckle.Connectors.RevitShared/Plugin/RevitExternalApplication.cs @@ -2,7 +2,6 @@ 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; @@ -48,7 +47,6 @@ 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/RevitIdleManager.cs b/Connectors/Revit/Speckle.Connectors.RevitShared/Plugin/RevitIdleManager.cs index cce89b5dd..3154fb3bd 100644 --- a/Connectors/Revit/Speckle.Connectors.RevitShared/Plugin/RevitIdleManager.cs +++ b/Connectors/Revit/Speckle.Connectors.RevitShared/Plugin/RevitIdleManager.cs @@ -6,12 +6,7 @@ namespace Speckle.Connectors.Revit.Plugin; -public interface IRevitIdleManager : IAppIdleManager -{ - public void RunAsync(Action action); -} - -public sealed class RevitIdleManager : AppIdleManager, IRevitIdleManager +public sealed class RevitIdleManager : AppIdleManager { private readonly UIApplication _uiApplication; private readonly IIdleCallManager _idleCallManager; @@ -42,13 +37,4 @@ protected override void AddEvent() private void RevitAppOnIdle(object? sender, IdlingEventArgs e) => _idleCallManager.AppOnIdle(() => OnIdle -= RevitAppOnIdle); - - public void RunAsync(Action action) - { -#if REVIT2025 - global::Revit.Async.RevitTask.RunAsync(action); -#else - action(); -#endif - } } diff --git a/Connectors/Revit/Speckle.Connectors.RevitShared/Plugin/RevitThreadContext.cs b/Connectors/Revit/Speckle.Connectors.RevitShared/Plugin/RevitThreadContext.cs new file mode 100644 index 000000000..92dd7d521 --- /dev/null +++ b/Connectors/Revit/Speckle.Connectors.RevitShared/Plugin/RevitThreadContext.cs @@ -0,0 +1,16 @@ +using Revit.Async; +using Speckle.Connectors.Common.Threading; + +namespace Speckle.Connectors.Revit.Plugin; + +public class RevitThreadContext : ThreadContext +{ + protected override ValueTask MainToWorkerAsync(Func> action) => action(); + + protected override ValueTask WorkerToMainAsync(Func> action) => + RevitTask.RunAsync(async () => await action().BackToCurrent()).AsValueTask(); + + protected override ValueTask MainToWorker(Func action) => new(action()); + + protected override ValueTask WorkerToMain(Func action) => new(RevitTask.RunAsync(action)); +} diff --git a/Connectors/Revit/Speckle.Connectors.RevitShared/Speckle.Connectors.RevitShared.projitems b/Connectors/Revit/Speckle.Connectors.RevitShared/Speckle.Connectors.RevitShared.projitems index 350c552f7..cc39c34ae 100644 --- a/Connectors/Revit/Speckle.Connectors.RevitShared/Speckle.Connectors.RevitShared.projitems +++ b/Connectors/Revit/Speckle.Connectors.RevitShared/Speckle.Connectors.RevitShared.projitems @@ -19,7 +19,6 @@ - @@ -49,6 +48,7 @@ + diff --git a/Connectors/Rhino/Speckle.Connectors.Rhino7/packages.lock.json b/Connectors/Rhino/Speckle.Connectors.Rhino7/packages.lock.json index c3f235270..4bef71d15 100644 --- a/Connectors/Rhino/Speckle.Connectors.Rhino7/packages.lock.json +++ b/Connectors/Rhino/Speckle.Connectors.Rhino7/packages.lock.json @@ -284,8 +284,7 @@ "Microsoft.Extensions.Logging.Abstractions": "[2.2.0, )", "Speckle.Connectors.Common": "[1.0.0, )", "Speckle.Sdk": "[3.1.0-dev.205, )", - "Speckle.Sdk.Dependencies": "[3.1.0-dev.205, )", - "System.Threading.Tasks.Dataflow": "[6.0.0, )" + "Speckle.Sdk.Dependencies": "[3.1.0-dev.205, )" } }, "speckle.connectors.dui.webview": { @@ -376,12 +375,6 @@ "requested": "[3.1.0-dev.205, )", "resolved": "3.1.0-dev.205", "contentHash": "MjWRxQhXYZxfpc1JwKc33jjBH3mLLxnbCNx5mcYAdGRJDFb/6ebHzQqWE1abxxCa5k1GJBbH8RpMscFm3w6oVQ==" - }, - "System.Threading.Tasks.Dataflow": { - "type": "CentralTransitive", - "requested": "[6.0.0, )", - "resolved": "6.0.0", - "contentHash": "+tyDCU3/B1lDdOOAJywHQoFwyXIUghIaP2BxG79uvhfTnO+D9qIgjVlL/JV2NTliYbMHpd6eKDmHp2VHpij7MA==" } } } diff --git a/Connectors/Rhino/Speckle.Connectors.Rhino8/packages.lock.json b/Connectors/Rhino/Speckle.Connectors.Rhino8/packages.lock.json index 023f72d82..ac9b9a8e6 100644 --- a/Connectors/Rhino/Speckle.Connectors.Rhino8/packages.lock.json +++ b/Connectors/Rhino/Speckle.Connectors.Rhino8/packages.lock.json @@ -284,8 +284,7 @@ "Microsoft.Extensions.Logging.Abstractions": "[2.2.0, )", "Speckle.Connectors.Common": "[1.0.0, )", "Speckle.Sdk": "[3.1.0-dev.205, )", - "Speckle.Sdk.Dependencies": "[3.1.0-dev.205, )", - "System.Threading.Tasks.Dataflow": "[6.0.0, )" + "Speckle.Sdk.Dependencies": "[3.1.0-dev.205, )" } }, "speckle.connectors.dui.webview": { @@ -376,12 +375,6 @@ "requested": "[3.1.0-dev.205, )", "resolved": "3.1.0-dev.205", "contentHash": "MjWRxQhXYZxfpc1JwKc33jjBH3mLLxnbCNx5mcYAdGRJDFb/6ebHzQqWE1abxxCa5k1GJBbH8RpMscFm3w6oVQ==" - }, - "System.Threading.Tasks.Dataflow": { - "type": "CentralTransitive", - "requested": "[6.0.0, )", - "resolved": "6.0.0", - "contentHash": "+tyDCU3/B1lDdOOAJywHQoFwyXIUghIaP2BxG79uvhfTnO+D9qIgjVlL/JV2NTliYbMHpd6eKDmHp2VHpij7MA==" } } } diff --git a/Connectors/Rhino/Speckle.Connectors.RhinoShared/Bindings/RhinoBasicConnectorBinding.cs b/Connectors/Rhino/Speckle.Connectors.RhinoShared/Bindings/RhinoBasicConnectorBinding.cs index 432c2c81f..d15cfb094 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().ConfigureAwait(false); // 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..98b1c9480 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 23bf041ff..a74b5c053 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; @@ -56,7 +56,6 @@ public sealed class RhinoSendBinding : ISendBinding public RhinoSendBinding( DocumentModelStore store, - IAppIdleManager idleManager, IBrowserBridge parent, IEnumerable sendFilters, IServiceProvider serviceProvider, @@ -66,11 +65,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; @@ -79,15 +78,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) => { @@ -97,26 +95,30 @@ private void SubscribeToRhinoEvents() ChangedObjectIds[selectedObject.Id.ToString()] = 1; } }; - - 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().ConfigureAwait(false); - } - }; + await InvalidateAllSender().ConfigureAwait(false); + } + }); - RhinoDoc.AddRhinoObject += (_, e) => - _topLevelExceptionHandler.CatchUnhandled(() => + eventAggregator + .GetEvent() + .Subscribe(e => { // NOTE: This does not work if rhino starts and opens a blank doc; // These events always happen in a doc. Why guard agains a null doc? @@ -124,13 +126,13 @@ private void SubscribeToRhinoEvents() // { // return; // } - 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 => { // NOTE: This does not work if rhino starts and opens a blank doc; // These events always happen in a doc. Why guard agains a null doc? @@ -140,33 +142,36 @@ 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 (args is RhinoDoc.RenderMaterialAssignmentChangedEventArgs changedEventArgs) { ChangedObjectIds[changedEventArgs.ObjectId.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 (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 => { // NOTE: This does not work if rhino starts and opens a blank doc; // These events always happen in a doc. Why guard agains a null doc? @@ -184,12 +189,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 => { // NOTE: This does not work if rhino starts and opens a blank doc; // These events always happen in a doc. Why guard agains a null doc? @@ -200,7 +206,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..d5639ed3f --- /dev/null +++ b/Connectors/Rhino/Speckle.Connectors.RhinoShared/Events.cs @@ -0,0 +1,69 @@ +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 static class RhinoEvents +{ + public static void Register(IEventAggregator eventAggregator) + { + RhinoApp.Idle += (_, e) => eventAggregator.GetEvent().Publish(e); + + RhinoDoc.BeginOpenDocument += (_, e) => eventAggregator.GetEvent().Publish(e); + RhinoDoc.EndOpenDocument += (_, e) => eventAggregator.GetEvent().Publish(e); + RhinoDoc.SelectObjects += (_, e) => eventAggregator.GetEvent().Publish(e); + RhinoDoc.DeselectObjects += (_, e) => eventAggregator.GetEvent().Publish(e); + RhinoDoc.DeselectAllObjects += (_, e) => eventAggregator.GetEvent().Publish(e); + RhinoDoc.ActiveDocumentChanged += (_, e) => eventAggregator.GetEvent().Publish(e); + RhinoDoc.DocumentPropertiesChanged += (_, e) => eventAggregator.GetEvent().Publish(e); + RhinoDoc.AddRhinoObject += (_, e) => eventAggregator.GetEvent().Publish(e); + RhinoDoc.DeleteRhinoObject += (_, e) => eventAggregator.GetEvent().Publish(e); + RhinoDoc.RenderMaterialsTableEvent += (_, e) => eventAggregator.GetEvent().Publish(e); + RhinoDoc.MaterialTableEvent += (_, e) => eventAggregator.GetEvent().Publish(e); + RhinoDoc.ModifyObjectAttributes += (_, e) => eventAggregator.GetEvent().Publish(e); + RhinoDoc.ReplaceRhinoObject += (_, e) => eventAggregator.GetEvent().Publish(e); + } +} diff --git a/Connectors/Rhino/Speckle.Connectors.RhinoShared/HostApp/RhinoDocumentStore.cs b/Connectors/Rhino/Speckle.Connectors.RhinoShared/HostApp/RhinoDocumentStore.cs index a921eb153..2af952d34 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(e => { if (e.Merge) { @@ -29,7 +31,7 @@ public RhinoDocumentStore(IJsonSerializer jsonSerializer, ITopLevelExceptionHand IsDocumentInit = true; LoadState(); - OnDocumentChanged(); + eventAggregator.GetEvent().Publish(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/Operations/Receive/RhinoHostObjectBuilder.cs b/Connectors/Rhino/Speckle.Connectors.RhinoShared/Operations/Receive/RhinoHostObjectBuilder.cs index fbcf65ae5..1a42a4dfb 100644 --- a/Connectors/Rhino/Speckle.Connectors.RhinoShared/Operations/Receive/RhinoHostObjectBuilder.cs +++ b/Connectors/Rhino/Speckle.Connectors.RhinoShared/Operations/Receive/RhinoHostObjectBuilder.cs @@ -6,6 +6,7 @@ using Speckle.Connectors.Common.Extensions; using Speckle.Connectors.Common.Operations; using Speckle.Connectors.Common.Operations.Receive; +using Speckle.Connectors.Common.Threading; using Speckle.Connectors.Rhino.HostApp; using Speckle.Converters.Common; using Speckle.Converters.Rhino; @@ -32,6 +33,7 @@ public class RhinoHostObjectBuilder : IHostObjectBuilder private readonly RhinoGroupBaker _groupBaker; private readonly RootObjectUnpacker _rootObjectUnpacker; private readonly ISdkActivityFactory _activityFactory; + private readonly IThreadContext _threadContext; public RhinoHostObjectBuilder( IRootToHostConverter converter, @@ -42,7 +44,8 @@ public RhinoHostObjectBuilder( RhinoMaterialBaker materialBaker, RhinoColorBaker colorBaker, RhinoGroupBaker groupBaker, - ISdkActivityFactory activityFactory + ISdkActivityFactory activityFactory, + IThreadContext threadContext ) { _converter = converter; @@ -54,10 +57,11 @@ ISdkActivityFactory activityFactory _layerBaker = layerBaker; _groupBaker = groupBaker; _activityFactory = activityFactory; + _threadContext = threadContext; } #pragma warning disable CA1506 - public Task Build( + public HostObjectBuilderResult Build( #pragma warning restore CA1506 Base rootObject, string projectName, @@ -112,13 +116,16 @@ CancellationToken cancellationToken onOperationProgressed.Report(new("Baking layers (redraw disabled)", null)); using (var _ = _activityFactory.Start("Pre baking layers")) { - RhinoApp.InvokeAndWait(() => - { - using var layerNoDraw = new DisableRedrawScope(_converterSettings.Current.Document.Views); - var paths = atomicObjectsWithoutInstanceComponentsWithPath.Select(t => t.path).ToList(); - paths.AddRange(instanceComponentsWithPath.Select(t => t.path)); - _layerBaker.CreateAllLayersForReceive(paths, baseLayerName); - }); + //TODO what is this? This is going to the UI thread + _threadContext + .RunOnMain(() => + { + using var layerNoDraw = new DisableRedrawScope(_converterSettings.Current.Document.Views); + var paths = atomicObjectsWithoutInstanceComponentsWithPath.Select(t => t.path).ToList(); + paths.AddRange(instanceComponentsWithPath.Select(t => t.path)); + _layerBaker.CreateAllLayersForReceive(paths, baseLayerName); + }) + .Wait(); } // 5 - Convert atomic objects @@ -136,6 +143,7 @@ CancellationToken cancellationToken onOperationProgressed.Report( new("Converting objects", (double)++count / atomicObjectsWithoutInstanceComponentsForConverter.Count) ); + cancellationToken.ThrowIfCancellationRequested(); try { // 0: get pre-created layer from cache in layer baker @@ -229,7 +237,7 @@ CancellationToken cancellationToken } _converterSettings.Current.Document.Views.Redraw(); - return Task.FromResult(new HostObjectBuilderResult(bakedObjectIds, conversionResults)); + return new HostObjectBuilderResult(bakedObjectIds, conversionResults); } private void PreReceiveDeepClean(string baseLayerName) @@ -241,35 +249,37 @@ private void PreReceiveDeepClean(string baseLayerName) RhinoMath.UnsetIntIndex ); - RhinoApp.InvokeAndWait(() => - { - _instanceBaker.PurgeInstances(baseLayerName); - _materialBaker.PurgeMaterials(baseLayerName); - - var doc = _converterSettings.Current.Document; - // Cleans up any previously received objects - if (rootLayerIndex != RhinoMath.UnsetIntIndex) + _threadContext + .RunOnMain(() => { - var documentLayer = doc.Layers[rootLayerIndex]; - var childLayers = documentLayer.GetChildren(); - if (childLayers != null) + _instanceBaker.PurgeInstances(baseLayerName); + _materialBaker.PurgeMaterials(baseLayerName); + + var doc = _converterSettings.Current.Document; + // Cleans up any previously received objects + if (rootLayerIndex != RhinoMath.UnsetIntIndex) { - using var layerNoDraw = new DisableRedrawScope(doc.Views); - foreach (var layer in childLayers) + var documentLayer = doc.Layers[rootLayerIndex]; + var childLayers = documentLayer.GetChildren(); + if (childLayers != null) { - var purgeSuccess = doc.Layers.Purge(layer.Index, true); - if (!purgeSuccess) + using var layerNoDraw = new DisableRedrawScope(doc.Views); + foreach (var layer in childLayers) { - Console.WriteLine($"Failed to purge layer: {layer}"); + var purgeSuccess = doc.Layers.Purge(layer.Index, true); + if (!purgeSuccess) + { + Console.WriteLine($"Failed to purge layer: {layer}"); + } } } + doc.Layers.Purge(documentLayer.Index, true); } - doc.Layers.Purge(documentLayer.Index, true); - } - // Cleans up any previously received group - _groupBaker.PurgeGroups(baseLayerName); - }); + // Cleans up any previously received group + _groupBaker.PurgeGroups(baseLayerName); + }) + .Wait(); } /// diff --git a/Connectors/Rhino/Speckle.Connectors.RhinoShared/Operations/Send/RhinoRootObjectBuilder.cs b/Connectors/Rhino/Speckle.Connectors.RhinoShared/Operations/Send/RhinoRootObjectBuilder.cs index 5f337e491..1d15c5d3b 100644 --- a/Connectors/Rhino/Speckle.Connectors.RhinoShared/Operations/Send/RhinoRootObjectBuilder.cs +++ b/Connectors/Rhino/Speckle.Connectors.RhinoShared/Operations/Send/RhinoRootObjectBuilder.cs @@ -60,11 +60,10 @@ ISdkActivityFactory activityFactory _activityFactory = activityFactory; } - public async Task Build( + public RootObjectBuilderResult Build( IReadOnlyList rhinoObjects, SendInfo sendInfo, - IProgress onOperationProgressed, - CancellationToken cancellationToken = default + IProgress onOperationProgressed ) { using var activity = _activityFactory.Start("Build"); @@ -96,7 +95,6 @@ public async Task Build( foreach (RhinoObject rhinoObject in atomicObjects) { using var _2 = _activityFactory.Start("Convert"); - cancellationToken.ThrowIfCancellationRequested(); // handle layer Layer layer = _converterSettings.Current.Document.Layers[rhinoObject.Attributes.LayerIndex]; @@ -130,7 +128,6 @@ public async Task Build( rootObjectCollection[ProxyKeys.COLOR] = _colorUnpacker.UnpackColors(atomicObjects, versionLayers.ToList()); } - await Task.Yield(); return new RootObjectBuilderResult(rootObjectCollection, results); } 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 a172c693e..61052675c 100644 --- a/Connectors/Rhino/Speckle.Connectors.RhinoShared/Plugin/Speckle.Connectors.RhinoPlugin.cs +++ b/Connectors/Rhino/Speckle.Connectors.RhinoShared/Plugin/Speckle.Connectors.RhinoPlugin.cs @@ -1,8 +1,9 @@ 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; @@ -20,7 +21,6 @@ namespace Speckle.Connectors.Rhino.Plugin; /// public class SpeckleConnectorsRhinoPlugin : PlugIn { - private IRhinoPlugin? _rhinoPlugin; private IDisposable? _disposableLogger; protected override string LocalPlugInName => "Speckle (Beta) for Rhino"; @@ -52,11 +52,8 @@ protected override LoadReturnCode OnLoad(ref string errorMessage) // but the Rhino connector has `.rhp` as it is extension. Container = services.BuildServiceProvider(); - Container.UseDUI(); - // Resolve root plugin object and initialise. - _rhinoPlugin = Container.GetRequiredService(); - _rhinoPlugin.Initialise(); + RhinoEvents.Register(Container.GetRequiredService()); return LoadReturnCode.Success; } @@ -80,7 +77,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 086c513a8..82bf48c49 100644 --- a/Connectors/Rhino/Speckle.Connectors.RhinoShared/Registration/ServiceRegistration.cs +++ b/Connectors/Rhino/Speckle.Connectors.RhinoShared/Registration/ServiceRegistration.cs @@ -8,9 +8,9 @@ using Speckle.Connectors.Common.Cancellation; using Speckle.Connectors.Common.Instances; using Speckle.Connectors.Common.Operations; +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; @@ -32,23 +32,14 @@ public static void AddRhino(this IServiceCollection serviceCollection) serviceCollection.AddSingleton(SpeckleConnectorsRhinoCommand.Instance); serviceCollection.AddConnectorUtils(); - serviceCollection.AddDUI(); + serviceCollection.AddDUI(); serviceCollection.AddDUIView(); - // POC: Overwriting the SyncToMainThread to SyncToCurrentThread for Rhino! - // builder.AddSingletonInstance(); - - // 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.Tekla2023/packages.lock.json b/Connectors/Tekla/Speckle.Connector.Tekla2023/packages.lock.json index 7fb13b30d..d64bfc320 100644 --- a/Connectors/Tekla/Speckle.Connector.Tekla2023/packages.lock.json +++ b/Connectors/Tekla/Speckle.Connector.Tekla2023/packages.lock.json @@ -343,8 +343,7 @@ "Microsoft.Extensions.Logging.Abstractions": "[2.2.0, )", "Speckle.Connectors.Common": "[1.0.0, )", "Speckle.Sdk": "[3.1.0-dev.205, )", - "Speckle.Sdk.Dependencies": "[3.1.0-dev.205, )", - "System.Threading.Tasks.Dataflow": "[6.0.0, )" + "Speckle.Sdk.Dependencies": "[3.1.0-dev.205, )" } }, "speckle.connectors.dui.webview": { @@ -427,12 +426,6 @@ "requested": "[3.1.0-dev.205, )", "resolved": "3.1.0-dev.205", "contentHash": "MjWRxQhXYZxfpc1JwKc33jjBH3mLLxnbCNx5mcYAdGRJDFb/6ebHzQqWE1abxxCa5k1GJBbH8RpMscFm3w6oVQ==" - }, - "System.Threading.Tasks.Dataflow": { - "type": "CentralTransitive", - "requested": "[6.0.0, )", - "resolved": "6.0.0", - "contentHash": "+tyDCU3/B1lDdOOAJywHQoFwyXIUghIaP2BxG79uvhfTnO+D9qIgjVlL/JV2NTliYbMHpd6eKDmHp2VHpij7MA==" } } } diff --git a/Connectors/Tekla/Speckle.Connector.Tekla2024/packages.lock.json b/Connectors/Tekla/Speckle.Connector.Tekla2024/packages.lock.json index ca198365a..649665b75 100644 --- a/Connectors/Tekla/Speckle.Connector.Tekla2024/packages.lock.json +++ b/Connectors/Tekla/Speckle.Connector.Tekla2024/packages.lock.json @@ -424,8 +424,7 @@ "Microsoft.Extensions.Logging.Abstractions": "[2.2.0, )", "Speckle.Connectors.Common": "[1.0.0, )", "Speckle.Sdk": "[3.1.0-dev.205, )", - "Speckle.Sdk.Dependencies": "[3.1.0-dev.205, )", - "System.Threading.Tasks.Dataflow": "[6.0.0, )" + "Speckle.Sdk.Dependencies": "[3.1.0-dev.205, )" } }, "speckle.connectors.dui.webview": { @@ -508,12 +507,6 @@ "requested": "[3.1.0-dev.205, )", "resolved": "3.1.0-dev.205", "contentHash": "MjWRxQhXYZxfpc1JwKc33jjBH3mLLxnbCNx5mcYAdGRJDFb/6ebHzQqWE1abxxCa5k1GJBbH8RpMscFm3w6oVQ==" - }, - "System.Threading.Tasks.Dataflow": { - "type": "CentralTransitive", - "requested": "[6.0.0, )", - "resolved": "6.0.0", - "contentHash": "+tyDCU3/B1lDdOOAJywHQoFwyXIUghIaP2BxG79uvhfTnO+D9qIgjVlL/JV2NTliYbMHpd6eKDmHp2VHpij7MA==" } } } diff --git a/Connectors/Tekla/Speckle.Connector.TeklaShared/Bindings/TeklaBasicConnectorBinding.cs b/Connectors/Tekla/Speckle.Connector.TeklaShared/Bindings/TeklaBasicConnectorBinding.cs index bc1449c7c..0856f6996 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().ConfigureAwait(false); }); diff --git a/Connectors/Tekla/Speckle.Connector.TeklaShared/Bindings/TeklaSelectionBinding.cs b/Connectors/Tekla/Speckle.Connector.TeklaShared/Bindings/TeklaSelectionBinding.cs index 4c26ab1d1..5cfd403c9 100644 --- a/Connectors/Tekla/Speckle.Connector.TeklaShared/Bindings/TeklaSelectionBinding.cs +++ b/Connectors/Tekla/Speckle.Connector.TeklaShared/Bindings/TeklaSelectionBinding.cs @@ -1,43 +1,38 @@ using Speckle.Connectors.DUI.Bindings; using Speckle.Connectors.DUI.Bridge; +using Speckle.Connectors.DUI.Eventing; +using Speckle.Connectors.RhinoShared; 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 Tekla.Structures.Model.Events _events; private readonly object _selectionEventHandlerLock = new object(); private readonly Tekla.Structures.Model.UI.ModelObjectSelector _selector; + private readonly IEventAggregator _eventAggregator; 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; + _eventAggregator = eventAggregator; - _events.SelectionChange += Events_SelectionChangeEvent; - _events.Register(); - - UpdateSelection(); + eventAggregator.GetEvent().Subscribe(_ => Events_SelectionChangeEvent()); } private void Events_SelectionChangeEvent() { lock (_selectionEventHandlerLock) { - _idleManager.SubscribeToIdle(nameof(TeklaSelectionBinding), UpdateSelection); UpdateSelection(); } } @@ -45,7 +40,7 @@ private void Events_SelectionChangeEvent() private void UpdateSelection() { SelectionInfo selInfo = GetSelection(); - Parent.Send(SELECTION_EVENT, selInfo); + Parent.Send2(SELECTION_EVENT, selInfo); } public SelectionInfo GetSelection() diff --git a/Connectors/Tekla/Speckle.Connector.TeklaShared/Bindings/TeklaSendBinding.cs b/Connectors/Tekla/Speckle.Connector.TeklaShared/Bindings/TeklaSendBinding.cs index 54d99146e..495458cd2 100644 --- a/Connectors/Tekla/Speckle.Connector.TeklaShared/Bindings/TeklaSendBinding.cs +++ b/Connectors/Tekla/Speckle.Connector.TeklaShared/Bindings/TeklaSendBinding.cs @@ -6,12 +6,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.Connectors.TeklaShared.Operations.Send.Settings; using Speckle.Converters.Common; using Speckle.Converters.TeklaShared; @@ -24,14 +26,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 +43,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 +59,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 +78,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 @@ -198,15 +190,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..24b749eef --- /dev/null +++ b/Connectors/Tekla/Speckle.Connector.TeklaShared/Events.cs @@ -0,0 +1,26 @@ +using Speckle.Connectors.Common.Threading; +using Speckle.Connectors.DUI.Bridge; +using Speckle.Connectors.DUI.Eventing; + +namespace Speckle.Connectors.RhinoShared; + +public class SelectionChange(IThreadContext threadContext, ITopLevelExceptionHandler exceptionHandler) + : ThreadedEvent(threadContext, exceptionHandler); + +public class ModelObjectChanged(IThreadContext threadContext, ITopLevelExceptionHandler exceptionHandler) + : ThreadedEvent>(threadContext, exceptionHandler); + +public class ModelLoad(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 += () => eventAggregator.GetEvent().Publish(new object()); + events.ModelObjectChanged += x => eventAggregator.GetEvent().Publish(x); + events.ModelLoad += () => eventAggregator.GetEvent().Publish(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 bae477566..5cd425b1f 100644 --- a/Connectors/Tekla/Speckle.Connector.TeklaShared/HostApp/TeklaDocumentModelStore.cs +++ b/Connectors/Tekla/Speckle.Connector.TeklaShared/HostApp/TeklaDocumentModelStore.cs @@ -1,6 +1,8 @@ using Microsoft.Extensions.Logging; +using Speckle.Connectors.DUI.Eventing; using Speckle.Connectors.DUI.Models; using Speckle.Connectors.DUI.Utils; +using Speckle.Connectors.RhinoShared; using Speckle.Sdk; using Speckle.Sdk.Helpers; using Speckle.Sdk.SQLite; @@ -11,33 +13,33 @@ public class TeklaDocumentModelStore : DocumentModelStore { private readonly ILogger _logger; 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; _jsonCacheManager = jsonCacheManagerFactory.CreateForUser("ConnectorsFileData"); - _events = new TSM.Events(); _model = new TSM.Model(); GenerateKey(); - _events.ModelLoad += () => - { - GenerateKey(); - LoadState(); - OnDocumentChanged(); - }; - _events.Register(); + eventAggregator + .GetEvent() + .Publish(() => + { + GenerateKey(); + LoadState(); + eventAggregator.GetEvent().Publish(new object()); + }); if (SpeckleTeklaPanelHost.IsInitialized) { LoadState(); - OnDocumentChanged(); + eventAggregator.GetEvent().Publish(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/Operations/Send/TeklaRootObjectBuilder.cs b/Connectors/Tekla/Speckle.Connector.TeklaShared/Operations/Send/TeklaRootObjectBuilder.cs index 69da6664c..e7e4fef87 100644 --- a/Connectors/Tekla/Speckle.Connector.TeklaShared/Operations/Send/TeklaRootObjectBuilder.cs +++ b/Connectors/Tekla/Speckle.Connector.TeklaShared/Operations/Send/TeklaRootObjectBuilder.cs @@ -11,7 +11,6 @@ using Speckle.Sdk.Logging; using Speckle.Sdk.Models; using Speckle.Sdk.Models.Collections; -using Task = System.Threading.Tasks.Task; namespace Speckle.Connectors.TeklaShared.Operations.Send; @@ -44,11 +43,10 @@ TeklaMaterialUnpacker materialUnpacker _materialUnpacker = materialUnpacker; } - public async Task Build( + public RootObjectBuilderResult Build( IReadOnlyList teklaObjects, SendInfo sendInfo, - IProgress onOperationProgressed, - CancellationToken cancellationToken = default + IProgress onOperationProgressed ) { using var activity = _activityFactory.Start("Build"); @@ -67,7 +65,6 @@ public async Task Build( foreach (TSM.ModelObject teklaObject in teklaObjects) { using var _2 = _activityFactory.Start("Convert"); - cancellationToken.ThrowIfCancellationRequested(); var result = ConvertTeklaObject(teklaObject, rootObjectCollection, sendInfo.ProjectId); results.Add(result); @@ -88,7 +85,6 @@ public async Task Build( rootObjectCollection[ProxyKeys.RENDER_MATERIAL] = renderMaterialProxies; } - await Task.Yield(); return new RootObjectBuilderResult(rootObjectCollection, results); } diff --git a/Connectors/Tekla/Speckle.Connector.TeklaShared/ServiceRegistration.cs b/Connectors/Tekla/Speckle.Connector.TeklaShared/ServiceRegistration.cs index b112365cb..f058061e8 100644 --- a/Connectors/Tekla/Speckle.Connector.TeklaShared/ServiceRegistration.cs +++ b/Connectors/Tekla/Speckle.Connector.TeklaShared/ServiceRegistration.cs @@ -4,6 +4,7 @@ using Speckle.Connectors.Common.Caching; using Speckle.Connectors.Common.Cancellation; using Speckle.Connectors.Common.Operations; +using Speckle.Connectors.Common.Threading; using Speckle.Connectors.DUI; using Speckle.Connectors.DUI.Bindings; using Speckle.Connectors.DUI.Bridge; @@ -31,18 +32,14 @@ public static IServiceCollection AddTekla(this IServiceCollection services) services.AddSingleton(); services.AddConnectorUtils(); - services.AddDUI(); + 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..b7a938698 100644 --- a/Connectors/Tekla/Speckle.Connector.TeklaShared/SpeckleTeklaPanelHost.cs +++ b/Connectors/Tekla/Speckle.Connector.TeklaShared/SpeckleTeklaPanelHost.cs @@ -5,7 +5,9 @@ using System.Windows.Forms.Integration; using Microsoft.Extensions.DependencyInjection; using Speckle.Connectors.Common; +using Speckle.Connectors.DUI.Eventing; using Speckle.Connectors.DUI.WebView; +using Speckle.Connectors.RhinoShared; using Speckle.Converters.TeklaShared; using Speckle.Sdk.Host; using Tekla.Structures.Dialog; @@ -89,6 +91,7 @@ private void InitializeInstance() services.AddTeklaConverters(); Container = services.BuildServiceProvider(); + TeklaEvents.Register(Container.GetRequiredService(), Container.GetRequiredService()); Model = new Model(); if (!Model.GetConnectionStatus()) diff --git a/Converters/ArcGIS/Speckle.Converters.ArcGIS3/ArcGISConversionSettingsFactory.cs b/Converters/ArcGIS/Speckle.Converters.ArcGIS3/ArcGISConversionSettingsFactory.cs index 5ae9ae250..e0464be1b 100644 --- a/Converters/ArcGIS/Speckle.Converters.ArcGIS3/ArcGISConversionSettingsFactory.cs +++ b/Converters/ArcGIS/Speckle.Converters.ArcGIS3/ArcGISConversionSettingsFactory.cs @@ -1,7 +1,6 @@ using ArcGIS.Core.Data; using ArcGIS.Core.Data.DDL; using ArcGIS.Desktop.Core; -using ArcGIS.Desktop.Framework.Threading.Tasks; using ArcGIS.Desktop.Mapping; using Speckle.Converters.ArcGIS3.Utils; using Speckle.Converters.Common; @@ -137,8 +136,7 @@ public Uri AddDatabaseToProject(Uri databasePath) var parentFolder = Path.GetDirectoryName(databasePath.AbsolutePath); var fGdbName = databasePath.Segments[^1]; Item folderToAdd = ItemFactory.Instance.Create(parentFolder); - // POC: QueuedTask - QueuedTask.Run(() => Project.Current.AddItem(folderToAdd as IProjectItem)); + Project.Current.AddItem(folderToAdd as IProjectItem); // Add a file geodatabase or a SQLite or enterprise database connection to a project try @@ -149,8 +147,7 @@ public Uri AddDatabaseToProject(Uri databasePath) if (gdbToAdd is not null) { - // POC: QueuedTask - var addedGeodatabase = QueuedTask.Run(() => Project.Current.AddItem(gdbToAdd as IProjectItem)); + var addedGeodatabase = Project.Current.AddItem(gdbToAdd as IProjectItem); } } catch (NullReferenceException ex) diff --git a/Converters/ArcGIS/Speckle.Converters.ArcGIS3/Utils/FeatureClassUtils.cs b/Converters/ArcGIS/Speckle.Converters.ArcGIS3/Utils/FeatureClassUtils.cs index 0cd9690f1..0795d4550 100644 --- a/Converters/ArcGIS/Speckle.Converters.ArcGIS3/Utils/FeatureClassUtils.cs +++ b/Converters/ArcGIS/Speckle.Converters.ArcGIS3/Utils/FeatureClassUtils.cs @@ -73,7 +73,7 @@ private Geodatabase GetDatabase() return geodatabase; } - public async Task>> GroupConversionTrackers( + public Dictionary> GroupConversionTrackers( Dictionary conversionTracker, Action onOperationProgressed ) @@ -132,13 +132,12 @@ private Geodatabase GetDatabase() ClearExistingDataset(uniqueKey); onOperationProgressed.Invoke("Grouping features into layers", count++ / conversionTracker.Count); - await Task.Yield(); } return geometryGroups; } - public async Task CreateDatasets( + public void CreateDatasets( Dictionary conversionTracker, Dictionary> featureClassElements, Action onOperationProgressed @@ -201,7 +200,6 @@ public async Task CreateDatasets( } onOperationProgressed.Invoke("Writing to Database", count++ / featureClassElements.Count); - await Task.Yield(); } } diff --git a/Converters/Autocad/Speckle.Converters.Autocad2024/packages.lock.json b/Converters/Autocad/Speckle.Converters.Autocad2024/packages.lock.json index 1fa0a3914..237c74826 100644 --- a/Converters/Autocad/Speckle.Converters.Autocad2024/packages.lock.json +++ b/Converters/Autocad/Speckle.Converters.Autocad2024/packages.lock.json @@ -275,8 +275,7 @@ "Microsoft.Extensions.Logging.Abstractions": "[2.2.0, )", "Speckle.Connectors.Common": "[1.0.0, )", "Speckle.Sdk": "[3.1.0-dev.205, )", - "Speckle.Sdk.Dependencies": "[3.1.0-dev.205, )", - "System.Threading.Tasks.Dataflow": "[6.0.0, )" + "Speckle.Sdk.Dependencies": "[3.1.0-dev.205, )" } }, "speckle.connectors.dui.webview": { @@ -360,12 +359,6 @@ "requested": "[3.1.0-dev.205, )", "resolved": "3.1.0-dev.205", "contentHash": "MjWRxQhXYZxfpc1JwKc33jjBH3mLLxnbCNx5mcYAdGRJDFb/6ebHzQqWE1abxxCa5k1GJBbH8RpMscFm3w6oVQ==" - }, - "System.Threading.Tasks.Dataflow": { - "type": "CentralTransitive", - "requested": "[6.0.0, )", - "resolved": "6.0.0", - "contentHash": "+tyDCU3/B1lDdOOAJywHQoFwyXIUghIaP2BxG79uvhfTnO+D9qIgjVlL/JV2NTliYbMHpd6eKDmHp2VHpij7MA==" } } } diff --git a/Converters/Autocad/Speckle.Converters.Autocad2025/packages.lock.json b/Converters/Autocad/Speckle.Converters.Autocad2025/packages.lock.json index b2ba2ecff..ce05db8b7 100644 --- a/Converters/Autocad/Speckle.Converters.Autocad2025/packages.lock.json +++ b/Converters/Autocad/Speckle.Converters.Autocad2025/packages.lock.json @@ -231,8 +231,7 @@ "Microsoft.Extensions.Logging.Abstractions": "[2.2.0, )", "Speckle.Connectors.Common": "[1.0.0, )", "Speckle.Sdk": "[3.1.0-dev.205, )", - "Speckle.Sdk.Dependencies": "[3.1.0-dev.205, )", - "System.Threading.Tasks.Dataflow": "[6.0.0, )" + "Speckle.Sdk.Dependencies": "[3.1.0-dev.205, )" } }, "speckle.connectors.dui.webview": { @@ -315,12 +314,6 @@ "requested": "[3.1.0-dev.205, )", "resolved": "3.1.0-dev.205", "contentHash": "MjWRxQhXYZxfpc1JwKc33jjBH3mLLxnbCNx5mcYAdGRJDFb/6ebHzQqWE1abxxCa5k1GJBbH8RpMscFm3w6oVQ==" - }, - "System.Threading.Tasks.Dataflow": { - "type": "CentralTransitive", - "requested": "[6.0.0, )", - "resolved": "6.0.0", - "contentHash": "+tyDCU3/B1lDdOOAJywHQoFwyXIUghIaP2BxG79uvhfTnO+D9qIgjVlL/JV2NTliYbMHpd6eKDmHp2VHpij7MA==" } } } diff --git a/Converters/Civil3d/Speckle.Converters.Civil3d2025/packages.lock.json b/Converters/Civil3d/Speckle.Converters.Civil3d2025/packages.lock.json index 11f5bfbf4..e15b1abdd 100644 --- a/Converters/Civil3d/Speckle.Converters.Civil3d2025/packages.lock.json +++ b/Converters/Civil3d/Speckle.Converters.Civil3d2025/packages.lock.json @@ -240,8 +240,7 @@ "Microsoft.Extensions.Logging.Abstractions": "[2.2.0, )", "Speckle.Connectors.Common": "[1.0.0, )", "Speckle.Sdk": "[3.1.0-dev.205, )", - "Speckle.Sdk.Dependencies": "[3.1.0-dev.205, )", - "System.Threading.Tasks.Dataflow": "[6.0.0, )" + "Speckle.Sdk.Dependencies": "[3.1.0-dev.205, )" } }, "speckle.connectors.dui.webview": { @@ -324,12 +323,6 @@ "requested": "[3.1.0-dev.205, )", "resolved": "3.1.0-dev.205", "contentHash": "MjWRxQhXYZxfpc1JwKc33jjBH3mLLxnbCNx5mcYAdGRJDFb/6ebHzQqWE1abxxCa5k1GJBbH8RpMscFm3w6oVQ==" - }, - "System.Threading.Tasks.Dataflow": { - "type": "CentralTransitive", - "requested": "[6.0.0, )", - "resolved": "6.0.0", - "contentHash": "+tyDCU3/B1lDdOOAJywHQoFwyXIUghIaP2BxG79uvhfTnO+D9qIgjVlL/JV2NTliYbMHpd6eKDmHp2VHpij7MA==" } } } diff --git a/DUI3/Speckle.Connectors.DUI.Tests/Bridge/IdleCallManagerTests.cs b/DUI3/Speckle.Connectors.DUI.Tests/Bridge/IdleCallManagerTests.cs index ecbb6ebaa..9c8397023 100644 --- a/DUI3/Speckle.Connectors.DUI.Tests/Bridge/IdleCallManagerTests.cs +++ b/DUI3/Speckle.Connectors.DUI.Tests/Bridge/IdleCallManagerTests.cs @@ -56,7 +56,7 @@ public async Task AppOnIdleInternalTest() handler .Setup(m => m.CatchUnhandledAsync(It.IsAny>())) .Callback>(a => a.Invoke()) - .Returns(Task.CompletedTask); + .ReturnsAsync(new Result()); var removeEvent = Create(); removeEvent.Setup(x => x.Invoke()); diff --git a/DUI3/Speckle.Connectors.DUI.Tests/Bridge/TopLevelExceptionHandlerTests.cs b/DUI3/Speckle.Connectors.DUI.Tests/Bridge/TopLevelExceptionHandlerTests.cs index 8ba16dc14..2a4957745 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,8 +72,8 @@ 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( () => sut.CatchUnhandled(new Func(() => throw new AppDomainUnloadedException())) @@ -85,8 +86,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 +99,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 +117,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/packages.lock.json b/DUI3/Speckle.Connectors.DUI.Tests/packages.lock.json index c5ea60cdf..09785068b 100644 --- a/DUI3/Speckle.Connectors.DUI.Tests/packages.lock.json +++ b/DUI3/Speckle.Connectors.DUI.Tests/packages.lock.json @@ -335,8 +335,7 @@ "Microsoft.Extensions.Logging.Abstractions": "[2.2.0, )", "Speckle.Connectors.Common": "[1.0.0, )", "Speckle.Sdk": "[3.1.0-dev.205, )", - "Speckle.Sdk.Dependencies": "[3.1.0-dev.205, )", - "System.Threading.Tasks.Dataflow": "[6.0.0, )" + "Speckle.Sdk.Dependencies": "[3.1.0-dev.205, )" } }, "speckle.connectors.logging": { @@ -406,12 +405,6 @@ "requested": "[3.1.0-dev.205, )", "resolved": "3.1.0-dev.205", "contentHash": "MjWRxQhXYZxfpc1JwKc33jjBH3mLLxnbCNx5mcYAdGRJDFb/6ebHzQqWE1abxxCa5k1GJBbH8RpMscFm3w6oVQ==" - }, - "System.Threading.Tasks.Dataflow": { - "type": "CentralTransitive", - "requested": "[6.0.0, )", - "resolved": "6.0.0", - "contentHash": "+tyDCU3/B1lDdOOAJywHQoFwyXIUghIaP2BxG79uvhfTnO+D9qIgjVlL/JV2NTliYbMHpd6eKDmHp2VHpij7MA==" } } } diff --git a/DUI3/Speckle.Connectors.DUI.WebView/DUI3ControlWebView.xaml.cs b/DUI3/Speckle.Connectors.DUI.WebView/DUI3ControlWebView.xaml.cs index c401a3289..59307e4bb 100644 --- a/DUI3/Speckle.Connectors.DUI.WebView/DUI3ControlWebView.xaml.cs +++ b/DUI3/Speckle.Connectors.DUI.WebView/DUI3ControlWebView.xaml.cs @@ -26,7 +26,7 @@ public DUI3ControlWebView(IServiceProvider serviceProvider) public object BrowserElement => Browser; - public Task ExecuteScriptAsyncMethod(string script, CancellationToken cancellationToken) + public void ExecuteScript(string script) { if (!Browser.IsInitialized) { @@ -39,7 +39,6 @@ public Task ExecuteScriptAsyncMethod(string script, CancellationToken cancellati () => Browser.ExecuteScriptAsync(script), DispatcherPriority.Background ); - return Task.CompletedTask; } private void OnInitialized(object? sender, CoreWebView2InitializationCompletedEventArgs e) diff --git a/DUI3/Speckle.Connectors.DUI.WebView/packages.lock.json b/DUI3/Speckle.Connectors.DUI.WebView/packages.lock.json index b752ec8d1..ac89c1082 100644 --- a/DUI3/Speckle.Connectors.DUI.WebView/packages.lock.json +++ b/DUI3/Speckle.Connectors.DUI.WebView/packages.lock.json @@ -275,8 +275,7 @@ "Microsoft.Extensions.Logging.Abstractions": "[2.2.0, )", "Speckle.Connectors.Common": "[1.0.0, )", "Speckle.Sdk": "[3.1.0-dev.205, )", - "Speckle.Sdk.Dependencies": "[3.1.0-dev.205, )", - "System.Threading.Tasks.Dataflow": "[6.0.0, )" + "Speckle.Sdk.Dependencies": "[3.1.0-dev.205, )" } }, "speckle.connectors.logging": { @@ -340,12 +339,6 @@ "requested": "[3.1.0-dev.205, )", "resolved": "3.1.0-dev.205", "contentHash": "MjWRxQhXYZxfpc1JwKc33jjBH3mLLxnbCNx5mcYAdGRJDFb/6ebHzQqWE1abxxCa5k1GJBbH8RpMscFm3w6oVQ==" - }, - "System.Threading.Tasks.Dataflow": { - "type": "CentralTransitive", - "requested": "[6.0.0, )", - "resolved": "6.0.0", - "contentHash": "+tyDCU3/B1lDdOOAJywHQoFwyXIUghIaP2BxG79uvhfTnO+D9qIgjVlL/JV2NTliYbMHpd6eKDmHp2VHpij7MA==" } }, "net6.0-windows7.0": { @@ -583,8 +576,7 @@ "Microsoft.Extensions.Logging.Abstractions": "[2.2.0, )", "Speckle.Connectors.Common": "[1.0.0, )", "Speckle.Sdk": "[3.1.0-dev.205, )", - "Speckle.Sdk.Dependencies": "[3.1.0-dev.205, )", - "System.Threading.Tasks.Dataflow": "[6.0.0, )" + "Speckle.Sdk.Dependencies": "[3.1.0-dev.205, )" } }, "speckle.connectors.logging": { @@ -648,12 +640,6 @@ "requested": "[3.1.0-dev.205, )", "resolved": "3.1.0-dev.205", "contentHash": "MjWRxQhXYZxfpc1JwKc33jjBH3mLLxnbCNx5mcYAdGRJDFb/6ebHzQqWE1abxxCa5k1GJBbH8RpMscFm3w6oVQ==" - }, - "System.Threading.Tasks.Dataflow": { - "type": "CentralTransitive", - "requested": "[6.0.0, )", - "resolved": "6.0.0", - "contentHash": "+tyDCU3/B1lDdOOAJywHQoFwyXIUghIaP2BxG79uvhfTnO+D9qIgjVlL/JV2NTliYbMHpd6eKDmHp2VHpij7MA==" } } } diff --git a/DUI3/Speckle.Connectors.DUI/Bindings/OperationProgressManager.cs b/DUI3/Speckle.Connectors.DUI/Bindings/OperationProgressManager.cs index 992aaf669..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; @@ -30,20 +30,18 @@ CancellationToken cancellationToken ) { var progress = new NonUIThreadProgress(args => - bridge.TopLevelExceptionHandler.FireAndForget( - () => - SetModelProgress( - bridge, - modelCardId, - new ModelCardProgress(modelCardId, args.Status, args.Progress), - cancellationToken - ) - ) - ); + { + SetModelProgress( + bridge, + modelCardId, + new ModelCardProgress(modelCardId, args.Status, args.Progress), + cancellationToken + ); + }); return progress; } - public async Task SetModelProgress( + public void SetModelProgress( IBrowserBridge bridge, string modelCardId, ModelCardProgress progress, @@ -60,7 +58,7 @@ CancellationToken cancellationToken t.Item1 = DateTime.Now; s_lastProgressValues[modelCardId] = (t.Item1, progress.Status); // Since it's the first time we get a call for this model card, we should send it out - await SendProgress(bridge, modelCardId, progress).ConfigureAwait(false); + SendProgress(bridge, modelCardId, progress); return; } @@ -72,9 +70,9 @@ CancellationToken cancellationToken return; } s_lastProgressValues[modelCardId] = (currentTime, progress.Status); - await SendProgress(bridge, modelCardId, progress).ConfigureAwait(false); + SendProgress(bridge, modelCardId, progress); } - private static async Task SendProgress(IBrowserBridge bridge, string modelCardId, ModelCardProgress progress) => - await bridge.Send(SET_MODEL_PROGRESS_UI_COMMAND_NAME, new { modelCardId, progress }).ConfigureAwait(false); + private static void SendProgress(IBrowserBridge bridge, string modelCardId, ModelCardProgress progress) => + bridge.Send2(SET_MODEL_PROGRESS_UI_COMMAND_NAME, new { modelCardId, progress }); } 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 252119ab2..6b72c6e99 100644 --- a/DUI3/Speckle.Connectors.DUI/Bridge/BrowserBridge.cs +++ b/DUI3/Speckle.Connectors.DUI/Bridge/BrowserBridge.cs @@ -3,9 +3,10 @@ using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Runtime.InteropServices; -using System.Threading.Tasks.Dataflow; 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,15 +28,16 @@ public sealed class BrowserBridge : IBrowserBridge /// private readonly ConcurrentDictionary _resultsStore = new(); - private readonly SynchronizationContext _mainThreadContext; - public ITopLevelExceptionHandler TopLevelExceptionHandler { get; } + + private readonly ITopLevelExceptionHandler _topLevelExceptionHandler; + private readonly IEventAggregator _eventAggregator; + private readonly IThreadContext _threadContext; + private readonly IThreadOptions _threadOptions; private readonly IBrowserScriptExecutor _browserScriptExecutor; private readonly IJsonSerializer _jsonSerializer; private IReadOnlyDictionary _bindingMethodCache = new Dictionary(); - - private ActionBlock? _actionBlock; private IBinding? _binding; private Type? _bindingType; @@ -57,26 +59,43 @@ private set } } - private struct RunMethodArgs - { - public string MethodName; - public string RequestId; - public string MethodArgs; - } - public BrowserBridge( + IThreadContext threadContext, IJsonSerializer jsonSerializer, ILogger logger, - ILogger topLogger, - IBrowserScriptExecutor browserScriptExecutor + IBrowserScriptExecutor browserScriptExecutor, + IThreadOptions threadOptions, + IEventAggregator eventAggregator, + ITopLevelExceptionHandler topLevelExceptionHandler ) { + _threadContext = threadContext; _jsonSerializer = jsonSerializer; _logger = logger; - TopLevelExceptionHandler = new TopLevelExceptionHandler(topLogger, this); // Capture the main thread's SynchronizationContext - _mainThreadContext = SynchronizationContext.Current.NotNull("No UI thread to capture?"); _browserScriptExecutor = browserScriptExecutor; + _threadOptions = threadOptions; + _eventAggregator = eventAggregator; + _topLevelExceptionHandler = topLevelExceptionHandler; + eventAggregator + .GetEvent() + .Subscribe( + ex => + { + 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) @@ -95,30 +114,9 @@ public void AssociateWithBinding(IBinding binding) bindingMethodCache[m.Name] = m; } _bindingMethodCache = bindingMethodCache; - - // Whenever the ui will call run method inside .net, it will post a message to this action block. - // This conveniently executes the code outside the UI thread and does not block during long operations (such as sending). - _actionBlock = new ActionBlock( - OnActionBlock, - new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 1000 } - ); - _logger.LogInformation("Bridge bound to front end name {FrontEndName}", binding.Name); } - private async Task OnActionBlock(RunMethodArgs args) - { - Result result = await TopLevelExceptionHandler - .CatchUnhandledAsync(async () => await ExecuteMethod(args.MethodName, args.MethodArgs).ConfigureAwait(false)) - .ConfigureAwait(false); - - string resultJson = result.IsSuccess - ? _jsonSerializer.Serialize(result.Value) - : SerializeFormattedException(result.Exception); - - await NotifyUIMethodCallResultReady(args.RequestId, resultJson).ConfigureAwait(false); - } - /// /// Used by the Frontend bridge logic to understand which methods are available. /// @@ -130,81 +128,29 @@ public string[] GetBindingsMethodNames() return bindingNames; } - /// - /// This method posts the requested call to our action block executor. - /// - /// - /// - /// - public void RunMethod(string methodName, string requestId, string args) - { - TopLevelExceptionHandler.CatchUnhandled(Post); - return; - - void Post() - { - bool wasAccepted = _actionBlock - .NotNull() - .Post( - new RunMethodArgs + //don't wait for browser runs on purpose + public void RunMethod(string methodName, string requestId, string methodArgs) => + _threadContext + .RunOnThreadAsync( + async () => + { + 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) { - MethodName = methodName, - RequestId = requestId, - MethodArgs = args + string resultJson = SerializeFormattedException(task.Exception); + NotifyUIMethodCallResultReady(requestId, resultJson); } - ); - if (!wasAccepted) - { - throw new InvalidOperationException($"Action block declined to Post ({methodName} {requestId} {args})"); - } - } - } - - public void RunOnMainThread(Action action) - { - _mainThreadContext.Post( - _ => - { - // Execute the action on the main thread - TopLevelExceptionHandler.CatchUnhandled(action); - }, - null - ); - } - - public async Task RunOnMainThreadAsync(Func action) - { - await RunOnMainThreadAsync(async () => - { - await action.Invoke().ConfigureAwait(false); - return null; - }) - .ConfigureAwait(false); - } - - [SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "TaskCompletionSource")] - public Task RunOnMainThreadAsync(Func> action) - { - TaskCompletionSource tcs = new(); - - _mainThreadContext.Post( - async _ => - { - try - { - T result = await action.Invoke().ConfigureAwait(false); - tcs.SetResult(result); - } - catch (Exception ex) - { - tcs.SetException(ex); - } - }, - null - ); - - return tcs.Task; - } + }, + _threadOptions.RunCommandsOnMainThread + ) + .FireAndForget(); /// /// Used by the action block to invoke the actual method called by the UI. @@ -296,16 +242,12 @@ private string SerializeFormattedException(Exception e) /// /// /// - /// - private async Task NotifyUIMethodCallResultReady( - string requestId, - string? serializedData = null, - CancellationToken cancellationToken = default - ) + /// + private void NotifyUIMethodCallResultReady(string requestId, string? serializedData = null) { _resultsStore[requestId] = serializedData; string script = $"{FrontendBoundName}.responseReady('{requestId}')"; - await _browserScriptExecutor.ExecuteScriptAsyncMethod(script, cancellationToken).ConfigureAwait(false); + _browserScriptExecutor.ExecuteScript(script); } /// @@ -339,7 +281,7 @@ public void OpenUrl(string url) Process.Start(new ProcessStartInfo { FileName = url, UseShellExecute = true }); } - public async Task Send(string eventName, CancellationToken cancellationToken = default) + public Task Send(string eventName, CancellationToken cancellationToken = default) { if (_binding is null) { @@ -348,10 +290,27 @@ public async Task Send(string eventName, CancellationToken cancellationToken = d var script = $"{FrontendBoundName}.emit('{eventName}')"; - await _browserScriptExecutor.ExecuteScriptAsyncMethod(script, cancellationToken).ConfigureAwait(false); + _browserScriptExecutor.ExecuteScript(script); + return Task.CompletedTask; + } + + public Task Send(string eventName, T data, CancellationToken cancellationToken = default) + where T : class + { + if (_binding is null) + { + throw new InvalidOperationException("Bridge was not initialized with a binding"); + } + + string payload = _jsonSerializer.Serialize(data); + string requestId = $"{Guid.NewGuid()}_{eventName}"; + _resultsStore[requestId] = payload; + var script = $"{FrontendBoundName}.emitResponseReady('{eventName}', '{requestId}')"; + _browserScriptExecutor.ExecuteScript(script); + return Task.CompletedTask; } - public async Task Send(string eventName, T data, CancellationToken cancellationToken = default) + public void Send2(string eventName, T data) where T : class { if (_binding is null) @@ -363,6 +322,6 @@ public async Task Send(string eventName, T data, CancellationToken cancellati string requestId = $"{Guid.NewGuid()}_{eventName}"; _resultsStore[requestId] = payload; var script = $"{FrontendBoundName}.emitResponseReady('{eventName}', '{requestId}')"; - await _browserScriptExecutor.ExecuteScriptAsyncMethod(script, cancellationToken).ConfigureAwait(false); + _browserScriptExecutor.ExecuteScript(script); } } diff --git a/DUI3/Speckle.Connectors.DUI/Bridge/IBrowserBridge.cs b/DUI3/Speckle.Connectors.DUI/Bridge/IBrowserBridge.cs index 7bd772978..20c85708f 100644 --- a/DUI3/Speckle.Connectors.DUI/Bridge/IBrowserBridge.cs +++ b/DUI3/Speckle.Connectors.DUI/Bridge/IBrowserBridge.cs @@ -29,22 +29,6 @@ public interface IBrowserBridge /// public void RunMethod(string methodName, string requestId, string args); - /// - /// Posts an onto the main thread - /// Some applications might need to run some operations on main thread as deferred actions. - /// - /// An awaitable - /// Action to run on the main thread - public Task RunOnMainThreadAsync(Func> action); - - /// - /// Posts an onto the main thread - /// Some applications might need to run some operations on main thread as deferred actions. - /// - /// An awaitable - /// Action to run on the main thread - public Task RunOnMainThreadAsync(Func action); - /// /// Bridge was not initialized with a binding public Task Send(string eventName, CancellationToken cancellationToken = default); @@ -56,5 +40,6 @@ public interface IBrowserBridge public Task Send(string eventName, T data, CancellationToken cancellationToken = default) where T : class; - public ITopLevelExceptionHandler TopLevelExceptionHandler { get; } + public void Send2(string eventName, T data) + where T : class; } diff --git a/DUI3/Speckle.Connectors.DUI/Bridge/IBrowserScriptExecutor.cs b/DUI3/Speckle.Connectors.DUI/Bridge/IBrowserScriptExecutor.cs index 6c3fb69f9..c0979f36c 100644 --- a/DUI3/Speckle.Connectors.DUI/Bridge/IBrowserScriptExecutor.cs +++ b/DUI3/Speckle.Connectors.DUI/Bridge/IBrowserScriptExecutor.cs @@ -4,7 +4,7 @@ public interface IBrowserScriptExecutor { /// thrown when is /// The (constant string) script to execute on the browser - public Task ExecuteScriptAsyncMethod(string script, CancellationToken cancellationToken); + public void ExecuteScript(string script); public bool IsBrowserInitialized { get; } diff --git a/DUI3/Speckle.Connectors.DUI/Bridge/SyncToUIThread.cs b/DUI3/Speckle.Connectors.DUI/Bridge/SyncToUIThread.cs deleted file mode 100644 index bb7bbe0c3..000000000 --- a/DUI3/Speckle.Connectors.DUI/Bridge/SyncToUIThread.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using Speckle.Connectors.Common.Operations; - -namespace Speckle.Connectors.DUI.Bridge; - -public class SyncToUIThread : ISyncToThread -{ - private readonly IBrowserBridge _bridge; - - public SyncToUIThread(IBrowserBridge bridge) - { - _bridge = bridge; - } - - [SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Task Completion Source")] - public async Task RunOnThread(Func> func) => - await _bridge.RunOnMainThreadAsync(func).ConfigureAwait(false); -} diff --git a/DUI3/Speckle.Connectors.DUI/Bridge/TopLevelExceptionHandler.cs b/DUI3/Speckle.Connectors.DUI/Bridge/TopLevelExceptionHandler.cs index b8b78ee97..f386d6926 100644 --- a/DUI3/Speckle.Connectors.DUI/Bridge/TopLevelExceptionHandler.cs +++ b/DUI3/Speckle.Connectors.DUI/Bridge/TopLevelExceptionHandler.cs @@ -1,8 +1,8 @@ using Microsoft.Extensions.Logging; -using Speckle.Connectors.DUI.Bindings; +using Speckle.Connectors.Common.Threading; +using Speckle.Connectors.DUI.Eventing; using Speckle.InterfaceGenerator; using Speckle.Sdk; -using Speckle.Sdk.Models.Extensions; namespace Speckle.Connectors.DUI.Bridge; @@ -23,15 +23,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,31 +41,60 @@ 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 + public Result CatchUnhandled(Func function) + { + try + { + try + { + return new Result(function()); + } + catch (Exception ex) when (!ex.IsFatal()) + { + _logger.LogError(ex, UNHANDLED_LOGGER_TEMPLATE); + _eventAggregator.GetEvent().Publish(ex); + return new(ex); + } + } + catch (Exception ex) + { + _logger.LogCritical(ex, UNHANDLED_LOGGER_TEMPLATE); + throw; + } + } /// /// A result pattern struct (where exceptions have been handled) - public async Task CatchUnhandledAsync(Func function) + public async Task CatchUnhandledAsync(Func function) { - _ = await CatchUnhandledAsync(async () => + var r = await CatchUnhandledAsync(async () => { - await function().ConfigureAwait(false); - return null; + await function().BackToCurrent(); + return true; }) - .ConfigureAwait(false); + .BackToCurrent(); + if (r.IsSuccess) + { + return new Result(); + } + return new Result(r.Exception); } /// @@ -75,11 +104,12 @@ public async Task> CatchUnhandledAsync(Func> function) { try { - return new(await function.Invoke().ConfigureAwait(false)); + return new(await function.Invoke().BackToCurrent()); } catch (Exception ex) when (!ex.IsFatal()) { - await HandleException(ex).ConfigureAwait(false); + _logger.LogError(ex, UNHANDLED_LOGGER_TEMPLATE); + _eventAggregator.GetEvent().Publish(ex); return new(ex); } } @@ -90,33 +120,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 - ) - .ConfigureAwait(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
@@ -127,18 +130,4 @@ await SetGlobalNotification( /// /// public async void FireAndForget(Func function) => await CatchUnhandledAsync(function).ConfigureAwait(false); - - 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 - } - ) - .ConfigureAwait(false); } diff --git a/DUI3/Speckle.Connectors.DUI/ContainerRegistration.cs b/DUI3/Speckle.Connectors.DUI/ContainerRegistration.cs index 05f78b842..e9771e951 100644 --- a/DUI3/Speckle.Connectors.DUI/ContainerRegistration.cs +++ b/DUI3/Speckle.Connectors.DUI/ContainerRegistration.cs @@ -1,8 +1,10 @@ using System.Reflection; using Microsoft.Extensions.DependencyInjection; -using Speckle.Connectors.Common.Operations; +using Microsoft.Extensions.DependencyInjection.Extensions; +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; @@ -11,30 +13,40 @@ namespace Speckle.Connectors.DUI; public static class ContainerRegistration { - public static void AddDUI(this IServiceCollection serviceCollection) + public static void AddDUI(this IServiceCollection serviceCollection) where TDocumentStore : DocumentModelStore + where TThreadContext : IThreadContext, new() { + // send operation and dependencies + serviceCollection.AddSingleton(new TThreadContext()); serviceCollection.AddSingleton(); - // send operation and dependencies - serviceCollection.AddSingleton(); serviceCollection.AddTransient(); // POC: Each binding should have it's own bridge instance 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 UseDUI(this IServiceProvider serviceProvider) => - serviceProvider.GetRequiredService(); - - 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; } } diff --git a/DUI3/Speckle.Connectors.DUI/Eventing/DelegateReference.cs b/DUI3/Speckle.Connectors.DUI/Eventing/DelegateReference.cs new file mode 100644 index 000000000..8601359a9 --- /dev/null +++ b/DUI3/Speckle.Connectors.DUI/Eventing/DelegateReference.cs @@ -0,0 +1,97 @@ +using System.Reflection; + +namespace Speckle.Connectors.DUI.Eventing; + +public interface IDelegateReference +{ + /// + /// Gets the referenced object. + /// + /// A instance if the target is valid; otherwise . + Delegate? Target { get; } +} + +public class DelegateReference : IDelegateReference +{ + private readonly Delegate? _delegate; + private readonly WeakReference _weakReference; + private readonly MethodInfo _method; + private readonly Type _delegateType; + + /// + /// Initializes a new instance of . + /// + /// The original to create a reference for. + /// If the class will create a weak reference to the delegate, allowing it to be garbage collected. Otherwise it will keep a strong reference to the target. + /// If the passed is not assignable to . + public DelegateReference(Delegate @delegate, bool keepReferenceAlive) + { + if (@delegate == null) + { + throw new ArgumentNullException(nameof(@delegate)); + } + + if (keepReferenceAlive) + { + _delegate = @delegate; + } + else + { + _weakReference = new WeakReference(@delegate.Target); + _method = @delegate.GetMethodInfo(); + _delegateType = @delegate.GetType(); + } + } + + /// + /// Gets the (the target) referenced by the current object. + /// + /// if the object referenced by the current object has been garbage collected; otherwise, a reference to the referenced by the current object. + public Delegate? Target + { + get + { + if (_delegate != null) + { + return _delegate; + } + else + { + return TryGetDelegate(); + } + } + } + + /// + /// Checks if the (the target) referenced by the current object are equal to another . + /// This is equivalent with comparing with , only more efficient. + /// + /// The other delegate to compare with. + /// True if the target referenced by the current object are equal to . + public bool TargetEquals(Delegate? @delegate) + { + if (_delegate != null) + { + return _delegate == @delegate; + } + if (@delegate == null) + { + return !_method.IsStatic && !_weakReference.IsAlive; + } + return _weakReference.Target == @delegate.Target && Equals(_method, @delegate.GetMethodInfo()); + } + + private Delegate? TryGetDelegate() + { + if (_method.IsStatic) + { + return _method.CreateDelegate(_delegateType, null); + } + object target = _weakReference.Target; + if (target != null) + { + return _method.CreateDelegate(_delegateType, target); + } + return null; + } +} 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..d0186bbce --- /dev/null +++ b/DUI3/Speckle.Connectors.DUI/Eventing/EventBase.cs @@ -0,0 +1,122 @@ +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 ICollection Subscriptions => _subscriptions; + + /// + /// Adds the specified to the subscribers' collection. + /// + /// The subscriber. + /// The that uniquely identifies every subscriber. + /// + /// Adds the subscription to the internal list and assigns it a new . + /// + protected virtual SubscriptionToken InternalSubscribe(IEventSubscription eventSubscription) + { + if (eventSubscription == null) + { + throw new ArgumentNullException(nameof(eventSubscription)); + } + + eventSubscription.SubscriptionToken = new SubscriptionToken(Unsubscribe); + + lock (Subscriptions) + { + Subscriptions.Add(eventSubscription); + } + return eventSubscription.SubscriptionToken; + } + + /// + /// Calls all the execution strategies exposed by the list of . + /// + /// The arguments that will be passed to the listeners. + /// Before executing the strategies, this class will prune all the subscribers from the + /// list that return a when calling the + /// method. + protected virtual void InternalPublish(params object[] arguments) + { + List> executionStrategies = PruneAndReturnStrategies(); + foreach (var executionStrategy in executionStrategies) + { + executionStrategy(arguments); + } + } + + /// + /// Removes the subscriber matching the . + /// + /// The returned by while subscribing to the event. + public virtual void Unsubscribe(SubscriptionToken token) + { + lock (Subscriptions) + { + IEventSubscription subscription = Subscriptions.FirstOrDefault(evt => evt.SubscriptionToken == token); + if (subscription != null) + { + Subscriptions.Remove(subscription); + } + } + } + + /// + /// Returns if there is a subscriber matching . + /// + /// The returned by while subscribing to the event. + /// if there is a that matches; otherwise . + public virtual bool Contains(SubscriptionToken token) + { + lock (Subscriptions) + { + IEventSubscription subscription = Subscriptions.FirstOrDefault(evt => evt.SubscriptionToken == token); + return subscription != null; + } + } + + private List> PruneAndReturnStrategies() + { + List> returnList = new(); + + lock (Subscriptions) + { + for (var i = Subscriptions.Count - 1; i >= 0; i--) + { + Action? listItem = _subscriptions[i].GetExecutionStrategy(); + + if (listItem == null) + { + // Prune from main list. Log? + _subscriptions.RemoveAt(i); + } + else + { + returnList.Add(listItem); + } + } + } + + return returnList; + } + + /// + /// Forces the PubSubEvent to remove any subscriptions that no longer have an execution strategy. + /// + 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/EventSubscription.cs b/DUI3/Speckle.Connectors.DUI/Eventing/EventSubscription.cs new file mode 100644 index 000000000..25543e4cd --- /dev/null +++ b/DUI3/Speckle.Connectors.DUI/Eventing/EventSubscription.cs @@ -0,0 +1,139 @@ +using Speckle.Connectors.DUI.Bridge; + +namespace Speckle.Connectors.DUI.Eventing; + +public interface IEventSubscription +{ + /// + /// Gets or sets a that identifies this . + /// + /// A token that identifies this . + SubscriptionToken SubscriptionToken { get; set; } + + /// + /// Gets the execution strategy to publish this event. + /// + /// An with the execution strategy, or if the is no longer valid. + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate")] + Action? GetExecutionStrategy(); +} + +/// +/// Provides a way to retrieve a to execute an action depending +/// on the value of a second filter predicate that returns true if the action should execute. +/// +/// The type to use for the generic and types. +public class EventSubscription : IEventSubscription +{ + private readonly IDelegateReference _actionReference; + private readonly IDelegateReference _filterReference; + private readonly ITopLevelExceptionHandler _exceptionHandler; + + /// + /// Creates a new instance of . + /// + ///A reference to a delegate of type . + ///A reference to a delegate of type . + ///When or are . + ///When the target of is not of type , + ///or the target of is not of type . + public EventSubscription( + IDelegateReference actionReference, + IDelegateReference filterReference, + ITopLevelExceptionHandler exceptionHandler + ) + { + if (actionReference == null) + { + throw new ArgumentNullException(nameof(actionReference)); + } + + if (actionReference.Target is not Action) + { + throw new ArgumentException(null, nameof(actionReference)); + } + + if (filterReference == null) + { + throw new ArgumentNullException(nameof(filterReference)); + } + + if (filterReference.Target is not Predicate) + { + throw new ArgumentException(null, nameof(filterReference)); + } + + _actionReference = actionReference; + _filterReference = filterReference; + _exceptionHandler = exceptionHandler; + } + + /// + /// Gets the target that is referenced by the . + /// + /// An or if the referenced target is not alive. + public Action? Action => (Action?)_actionReference.Target; + + /// + /// Gets the target that is referenced by the . + /// + /// An or if the referenced target is not alive. + public Predicate? Filter => (Predicate?)_filterReference.Target; + + /// + /// Gets or sets a that identifies this . + /// + /// A token that identifies this . + public SubscriptionToken SubscriptionToken { get; set; } + + /// + /// Gets the execution strategy to publish this event. + /// + /// An with the execution strategy, or if the is no longer valid. + /// + /// If or are no longer valid because they were + /// garbage collected, this method will return . + /// Otherwise it will return a delegate that evaluates the and if it + /// returns will then call . The returned + /// delegate holds hard references to the and target + /// delegates. As long as the returned delegate is not garbage collected, + /// the and references delegates won't get collected either. + /// + public virtual Action? GetExecutionStrategy() + { + Action? action = Action; + if (action is null) + { + return null; + } + Predicate? filter = Filter; + return arguments => + { + TPayload argument = (TPayload)arguments[0]; + if (filter is null) + { + InvokeAction(action, argument); + } + else if (filter(argument)) + { + InvokeAction(action, argument); + } + }; + } + + /// + /// Invokes the specified synchronously when not overridden. + /// + /// The action to execute. + /// The payload to pass while invoking it. + /// An is thrown if is null. + public virtual void InvokeAction(Action action, TPayload argument) + { + if (action == null) + { + throw new ArgumentNullException(nameof(action)); + } + + _exceptionHandler.CatchUnhandled(() => action(argument)); + } +} 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/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/MainThreadEventSubscription.cs b/DUI3/Speckle.Connectors.DUI/Eventing/MainThreadEventSubscription.cs new file mode 100644 index 000000000..1b9519e49 --- /dev/null +++ b/DUI3/Speckle.Connectors.DUI/Eventing/MainThreadEventSubscription.cs @@ -0,0 +1,16 @@ +using Speckle.Connectors.Common.Threading; +using Speckle.Connectors.DUI.Bridge; + +namespace Speckle.Connectors.DUI.Eventing; + +public class MainThreadEventSubscription( + IDelegateReference actionReference, + IDelegateReference filterReference, + IThreadContext threadContext, + ITopLevelExceptionHandler exceptionHandler, + bool isOnce +) : OneTimeEventSubscription(actionReference, filterReference, exceptionHandler, isOnce) +{ + public override void InvokeAction(Action action, T payload) => + threadContext.RunOnMain(() => action.Invoke(payload)).BackToCurrent(); +} diff --git a/DUI3/Speckle.Connectors.DUI/Eventing/OneTimeEventSubscription.cs b/DUI3/Speckle.Connectors.DUI/Eventing/OneTimeEventSubscription.cs new file mode 100644 index 000000000..f58e102e6 --- /dev/null +++ b/DUI3/Speckle.Connectors.DUI/Eventing/OneTimeEventSubscription.cs @@ -0,0 +1,20 @@ +using Speckle.Connectors.DUI.Bridge; + +namespace Speckle.Connectors.DUI.Eventing; + +public class OneTimeEventSubscription( + IDelegateReference actionReference, + IDelegateReference filterReference, + ITopLevelExceptionHandler exceptionHandler, + bool isOnce +) : EventSubscription(actionReference, filterReference, exceptionHandler) +{ + public override void InvokeAction(Action action, T payload) + { + action.Invoke(payload); + if (isOnce) + { + SubscriptionToken.Dispose(); + } + } +} diff --git a/DUI3/Speckle.Connectors.DUI/Eventing/OneTimeThreadedEvent.cs b/DUI3/Speckle.Connectors.DUI/Eventing/OneTimeThreadedEvent.cs new file mode 100644 index 000000000..db3270e79 --- /dev/null +++ b/DUI3/Speckle.Connectors.DUI/Eventing/OneTimeThreadedEvent.cs @@ -0,0 +1,79 @@ +using Speckle.Connectors.Common.Threading; +using Speckle.Connectors.DUI.Bridge; + +namespace Speckle.Connectors.DUI.Eventing; + +public abstract class OneTimeThreadedEvent(IThreadContext threadContext, ITopLevelExceptionHandler exceptionHandler) + : ThreadedEvent(threadContext, exceptionHandler) + where T : notnull +{ + private readonly Dictionary _activeTokens = new(); + + public SubscriptionToken OneTimeSubscribe( + string id, + Func action, + ThreadOption threadOption = ThreadOption.PublisherThread, + bool keepSubscriberReferenceAlive = false, + Predicate? filter = null + ) + { + return OneTimeInternal(id, t => action(t), threadOption, keepSubscriberReferenceAlive, filter); + } + + public SubscriptionToken OneTimeSubscribe( + string id, + Func action, + ThreadOption threadOption = ThreadOption.PublisherThread, + bool keepSubscriberReferenceAlive = false, + Predicate? filter = null + ) + { + return OneTimeInternal(id, _ => action(), threadOption, keepSubscriberReferenceAlive, filter); + } + + public SubscriptionToken OneTimeSubscribe( + string id, + Action action, + ThreadOption threadOption = ThreadOption.PublisherThread, + bool keepSubscriberReferenceAlive = false, + Predicate? filter = null + ) + { + return OneTimeInternal(id, action, threadOption, keepSubscriberReferenceAlive, filter); + } + + public SubscriptionToken OneTimeSubscribe( + string id, + Action action, + ThreadOption threadOption = ThreadOption.PublisherThread, + bool keepSubscriberReferenceAlive = false, + Predicate? filter = null + ) + { + return OneTimeInternal(id, _ => action(), threadOption, keepSubscriberReferenceAlive, filter); + } + + private SubscriptionToken OneTimeInternal( + string id, + Action action, + ThreadOption threadOption, + bool keepSubscriberReferenceAlive, + Predicate? filter + ) + { + lock (_activeTokens) + { + if (_activeTokens.TryGetValue(id, out var token)) + { + if (token.IsActive) + { + return token; + } + _activeTokens.Remove(id); + } + token = SubscribeOnceOrNot(action, threadOption, keepSubscriberReferenceAlive, filter, true); + _activeTokens.Add(id, token); + return token; + } + } +} diff --git a/DUI3/Speckle.Connectors.DUI/Eventing/PubSubEvent.cs b/DUI3/Speckle.Connectors.DUI/Eventing/PubSubEvent.cs new file mode 100644 index 000000000..929015432 --- /dev/null +++ b/DUI3/Speckle.Connectors.DUI/Eventing/PubSubEvent.cs @@ -0,0 +1,138 @@ +namespace Speckle.Connectors.DUI.Eventing; + +/// +/// Defines a class that manages publication and subscription to events. +/// +/// The type of message that will be passed to the subscribers. +public abstract class PubSubEvent : EventBase + where TPayload : notnull +{ + /// + /// Subscribes a delegate to an event that will be published on the . + /// will maintain a to the target of the supplied delegate. + /// + /// The delegate that gets executed when the event is published. + /// A that uniquely identifies the added subscription. + /// + /// The PubSubEvent collection is thread-safe. + /// + public SubscriptionToken Subscribe(Action action) => Subscribe(action, ThreadOption.PublisherThread); + + /// + /// Subscribes a delegate to an event that will be published on the + /// + /// The delegate that gets executed when the event is raised. + /// Filter to evaluate if the subscriber should receive the event. + /// A that uniquely identifies the added subscription. + public virtual SubscriptionToken Subscribe(Action action, Predicate filter) => + Subscribe(action, ThreadOption.PublisherThread, false, filter); + + /// + /// Subscribes a delegate to an event. + /// PubSubEvent will maintain a to the Target of the supplied delegate. + /// + /// The delegate that gets executed when the event is raised. + /// Specifies on which thread to receive the delegate callback. + /// A that uniquely identifies the added subscription. + /// + /// The PubSubEvent collection is thread-safe. + /// + public SubscriptionToken Subscribe(Action action, ThreadOption threadOption) => + Subscribe(action, threadOption, false); + + /// + /// Subscribes a delegate to an event that will be published on the . + /// + /// The delegate that gets executed when the event is published. + /// When , the keeps a reference to the subscriber so it does not get garbage collected. + /// A that uniquely identifies the added subscription. + /// + /// If is set to , will maintain a to the Target of the supplied delegate. + /// If not using a WeakReference ( is ), the user must explicitly call Unsubscribe for the event when disposing the subscriber in order to avoid memory leaks or unexpected behavior. + /// + /// The PubSubEvent collection is thread-safe. + /// + public SubscriptionToken Subscribe(Action action, bool keepSubscriberReferenceAlive) => + Subscribe(action, ThreadOption.PublisherThread, keepSubscriberReferenceAlive); + + /// + /// Subscribes a delegate to an event. + /// + /// The delegate that gets executed when the event is published. + /// Specifies on which thread to receive the delegate callback. + /// When , the keeps a reference to the subscriber so it does not get garbage collected. + /// A that uniquely identifies the added subscription. + /// + /// If is set to , will maintain a to the Target of the supplied delegate. + /// If not using a WeakReference ( is ), the user must explicitly call Unsubscribe for the event when disposing the subscriber in order to avoid memory leaks or unexpected behavior. + /// + /// The PubSubEvent collection is thread-safe. + /// + public SubscriptionToken Subscribe( + Action action, + ThreadOption threadOption, + bool keepSubscriberReferenceAlive + ) => Subscribe(action, threadOption, keepSubscriberReferenceAlive, null); + + /// + /// Subscribes a delegate to an event. + /// + /// The delegate that gets executed when the event is published. + /// Specifies on which thread to receive the delegate callback. + /// When , the keeps a reference to the subscriber so it does not get garbage collected. + /// Filter to evaluate if the subscriber should receive the event. + /// A that uniquely identifies the added subscription. + /// + /// If is set to , will maintain a to the Target of the supplied delegate. + /// If not using a WeakReference ( is ), the user must explicitly call Unsubscribe for the event when disposing the subscriber in order to avoid memory leaks or unexpected behavior. + /// + /// The PubSubEvent collection is thread-safe. + /// + public abstract SubscriptionToken Subscribe( + Action action, + ThreadOption threadOption, + bool keepSubscriberReferenceAlive, + Predicate? filter + ); + + /// + /// Publishes the . + /// + /// Message to pass to the subscribers. + public virtual void Publish(TPayload payload) => InternalPublish(payload); + + /// + /// Removes the first subscriber matching from the subscribers' list. + /// + /// The used when subscribing to the event. + public virtual void Unsubscribe(Action subscriber) + { + lock (Subscriptions) + { + IEventSubscription eventSubscription = Subscriptions + .Cast>() + .FirstOrDefault(evt => evt.Action == subscriber); + if (eventSubscription != null) + { + Subscriptions.Remove(eventSubscription); + } + } + } + + /// + /// Returns if there is a subscriber matching . + /// + /// The used when subscribing to the event. + /// if there is an that matches; otherwise . + public virtual bool Contains(Action subscriber) + { + IEventSubscription eventSubscription; + lock (Subscriptions) + { + eventSubscription = Subscriptions + .Cast>() + .FirstOrDefault(evt => evt.Action == subscriber); + } + return eventSubscription != null; + } +} diff --git a/DUI3/Speckle.Connectors.DUI/Eventing/SubscriptionToken.cs b/DUI3/Speckle.Connectors.DUI/Eventing/SubscriptionToken.cs new file mode 100644 index 000000000..64f92b127 --- /dev/null +++ b/DUI3/Speckle.Connectors.DUI/Eventing/SubscriptionToken.cs @@ -0,0 +1,48 @@ +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 Dispose() + { + // 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 (_unsubscribeAction != null) + { + _unsubscribeAction(this); + _unsubscribeAction = null; + } + } +} 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..30d8ed2de --- /dev/null +++ b/DUI3/Speckle.Connectors.DUI/Eventing/ThreadedEvent.cs @@ -0,0 +1,87 @@ +using Speckle.Connectors.Common.Threading; +using Speckle.Connectors.DUI.Bridge; + +namespace Speckle.Connectors.DUI.Eventing; + +public abstract class ThreadedEvent(IThreadContext threadContext, ITopLevelExceptionHandler exceptionHandler) + : PubSubEvent, + ISpeckleEvent + where T : notnull +{ + public string Name { get; } = typeof(T).Name; + + public SubscriptionToken Subscribe( + Func action, + ThreadOption threadOption, + bool keepSubscriberReferenceAlive, + Predicate? filter + ) + { + return SubscribeOnceOrNot(t => action(t), threadOption, keepSubscriberReferenceAlive, filter, false); + } + + public override SubscriptionToken Subscribe( + Action action, + ThreadOption threadOption, + bool keepSubscriberReferenceAlive, + Predicate? filter + ) + { + return SubscribeOnceOrNot(action, threadOption, keepSubscriberReferenceAlive, filter, false); + } + + protected SubscriptionToken SubscribeOnceOrNot( + Action action, + ThreadOption threadOption, + bool keepSubscriberReferenceAlive, + Predicate? filter, + bool isOnce + ) + { + IDelegateReference actionReference = new DelegateReference(action, keepSubscriberReferenceAlive); + IDelegateReference filterReference; + if (filter != null) + { + filterReference = new DelegateReference(filter, keepSubscriberReferenceAlive); + } + else + { + filterReference = new DelegateReference( + new Predicate( + delegate + { + return true; + } + ), + true + ); + } + EventSubscription subscription; + switch (threadOption) + { + case ThreadOption.WorkerThread: + subscription = new WorkerEventSubscription( + actionReference, + filterReference, + threadContext, + exceptionHandler, + isOnce + ); + break; + case ThreadOption.MainThread: + subscription = new MainThreadEventSubscription( + actionReference, + filterReference, + threadContext, + exceptionHandler, + isOnce + ); + break; + case ThreadOption.PublisherThread: + default: + subscription = new OneTimeEventSubscription(actionReference, filterReference, exceptionHandler, isOnce); + break; + } + return InternalSubscribe(subscription); + } +} diff --git a/DUI3/Speckle.Connectors.DUI/Eventing/WorkerEventSubscription.cs b/DUI3/Speckle.Connectors.DUI/Eventing/WorkerEventSubscription.cs new file mode 100644 index 000000000..82d9c74ea --- /dev/null +++ b/DUI3/Speckle.Connectors.DUI/Eventing/WorkerEventSubscription.cs @@ -0,0 +1,16 @@ +using Speckle.Connectors.Common.Threading; +using Speckle.Connectors.DUI.Bridge; + +namespace Speckle.Connectors.DUI.Eventing; + +public class WorkerEventSubscription( + IDelegateReference actionReference, + IDelegateReference filterReference, + IThreadContext threadContext, + ITopLevelExceptionHandler exceptionHandler, + bool isOnce +) : OneTimeEventSubscription(actionReference, filterReference, exceptionHandler, isOnce) +{ + public override void InvokeAction(Action action, TPayload argument) => + threadContext.RunOnWorker(() => action(argument)).BackToCurrent(); +} diff --git a/DUI3/Speckle.Connectors.DUI/Models/DocumentModelStore.cs b/DUI3/Speckle.Connectors.DUI/Models/DocumentModelStore.cs index 62c4a7984..9b6622c50 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 { @@ -88,8 +82,6 @@ public void RemoveModel(ModelCard model) } } - protected void OnDocumentChanged() => DocumentChanged?.Invoke(this, EventArgs.Empty); - public IEnumerable GetSenders() { lock (_models) diff --git a/DUI3/Speckle.Connectors.DUI/Speckle.Connectors.DUI.csproj b/DUI3/Speckle.Connectors.DUI/Speckle.Connectors.DUI.csproj index 8f3048e1a..e2909423f 100644 --- a/DUI3/Speckle.Connectors.DUI/Speckle.Connectors.DUI.csproj +++ b/DUI3/Speckle.Connectors.DUI/Speckle.Connectors.DUI.csproj @@ -12,7 +12,6 @@ - diff --git a/DUI3/Speckle.Connectors.DUI/packages.lock.json b/DUI3/Speckle.Connectors.DUI/packages.lock.json index a28cd251f..07bb997a8 100644 --- a/DUI3/Speckle.Connectors.DUI/packages.lock.json +++ b/DUI3/Speckle.Connectors.DUI/packages.lock.json @@ -71,12 +71,6 @@ "resolved": "3.1.0-dev.205", "contentHash": "MjWRxQhXYZxfpc1JwKc33jjBH3mLLxnbCNx5mcYAdGRJDFb/6ebHzQqWE1abxxCa5k1GJBbH8RpMscFm3w6oVQ==" }, - "System.Threading.Tasks.Dataflow": { - "type": "Direct", - "requested": "[6.0.0, )", - "resolved": "6.0.0", - "contentHash": "+tyDCU3/B1lDdOOAJywHQoFwyXIUghIaP2BxG79uvhfTnO+D9qIgjVlL/JV2NTliYbMHpd6eKDmHp2VHpij7MA==" - }, "GraphQL.Client": { "type": "Transitive", "resolved": "6.0.0", diff --git a/Directory.Packages.props b/Directory.Packages.props index 3e9d0292a..5b4383318 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -44,7 +44,6 @@ - diff --git a/Sdk/Speckle.Connectors.Common/Builders/IHostObjectBuilder.cs b/Sdk/Speckle.Connectors.Common/Builders/IHostObjectBuilder.cs index 7d9e150dc..112af1952 100644 --- a/Sdk/Speckle.Connectors.Common/Builders/IHostObjectBuilder.cs +++ b/Sdk/Speckle.Connectors.Common/Builders/IHostObjectBuilder.cs @@ -14,11 +14,10 @@ public interface IHostObjectBuilder /// Project of the model. /// Name of the model. /// Action to update UI progress bar. - /// Cancellation token that passed from top -> ReceiveBinding. /// List of application ids. // POC: Where we will return these ids will matter later when we target to also cache received application ids. /// Project and model name are needed for now to construct host app objects into related layers or filters. /// POC: we might consider later to have HostObjectBuilderContext? that might hold all possible data we will need. - Task Build( + HostObjectBuilderResult Build( Base rootObject, string projectName, string modelName, diff --git a/Sdk/Speckle.Connectors.Common/Builders/IRootObjectBuilder.cs b/Sdk/Speckle.Connectors.Common/Builders/IRootObjectBuilder.cs index f3584c726..c018b2b4d 100644 --- a/Sdk/Speckle.Connectors.Common/Builders/IRootObjectBuilder.cs +++ b/Sdk/Speckle.Connectors.Common/Builders/IRootObjectBuilder.cs @@ -6,11 +6,10 @@ namespace Speckle.Connectors.Common.Builders; public interface IRootObjectBuilder { - public Task Build( + public RootObjectBuilderResult Build( IReadOnlyList objects, SendInfo sendInfo, - IProgress onOperationProgressed, - CancellationToken ct = default + IProgress onOperationProgressed ); } diff --git a/Sdk/Speckle.Connectors.Common/Operations/ISyncToThread.cs b/Sdk/Speckle.Connectors.Common/Operations/ISyncToThread.cs deleted file mode 100644 index 2440e642f..000000000 --- a/Sdk/Speckle.Connectors.Common/Operations/ISyncToThread.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Speckle.Connectors.Common.Operations; - -public interface ISyncToThread -{ - public Task RunOnThread(Func> func); -} diff --git a/Sdk/Speckle.Connectors.Common/Operations/ReceiveOperation.cs b/Sdk/Speckle.Connectors.Common/Operations/ReceiveOperation.cs index caaa0162f..4ba8d2708 100644 --- a/Sdk/Speckle.Connectors.Common/Operations/ReceiveOperation.cs +++ b/Sdk/Speckle.Connectors.Common/Operations/ReceiveOperation.cs @@ -1,4 +1,5 @@ using Speckle.Connectors.Common.Builders; +using Speckle.Connectors.Common.Threading; using Speckle.Connectors.Logging; using Speckle.Sdk.Api; using Speckle.Sdk.Credentials; @@ -14,7 +15,9 @@ public sealed class ReceiveOperation( IReceiveProgress receiveProgress, ISdkActivityFactory activityFactory, IOperations operations, - IClientFactory clientFactory + IClientFactory clientFactory, + IThreadContext threadContext, + IThreadOptions threadOptions ) { public async Task Execute( @@ -32,8 +35,35 @@ CancellationToken cancellationToken var version = await apiClient .Version.Get(receiveInfo.SelectedVersionId, receiveInfo.ProjectId, cancellationToken) - .ConfigureAwait(false); + .BackToAny(); + var commitObject = await threadContext + .RunOnWorkerAsync(() => ReceiveData(account, version, receiveInfo, onOperationProgressed, cancellationToken)) + .BackToAny(); + + // 4 - Convert objects + HostObjectBuilderResult res = await threadContext + .RunOnThread( + () => ConvertObjects(commitObject, receiveInfo, onOperationProgressed, cancellationToken), + threadOptions.RunReceiveBuildOnMainThread + ) + .BackToAny(); + + await apiClient + .Version.Received(new(version.id, receiveInfo.ProjectId, receiveInfo.SourceApplication), cancellationToken) + .BackToAny(); + + return res; + } + + private async ValueTask ReceiveData( + Account account, + Speckle.Sdk.Api.GraphQL.Models.Version version, + ReceiveInfo receiveInfo, + IProgress onOperationProgressed, + CancellationToken cancellationToken + ) + { receiveProgress.Begin(); Base? commitObject = await operations .Receive2( @@ -44,27 +74,13 @@ CancellationToken cancellationToken onProgressAction: new PassthroughProgress(args => receiveProgress.Report(onOperationProgressed, args)), cancellationToken: cancellationToken ) - .ConfigureAwait(false); + .BackToAny(); cancellationToken.ThrowIfCancellationRequested(); - - // 4 - Convert objects - HostObjectBuilderResult? res = await ConvertObjects( - commitObject, - receiveInfo, - onOperationProgressed, - cancellationToken - ) - .ConfigureAwait(false); - - await apiClient - .Version.Received(new(version.id, receiveInfo.ProjectId, receiveInfo.SourceApplication), cancellationToken) - .ConfigureAwait(false); - - return res; + return commitObject; } - private async Task ConvertObjects( + private HostObjectBuilderResult ConvertObjects( Base commitObject, ReceiveInfo receiveInfo, IProgress onOperationProgressed, @@ -81,9 +97,13 @@ CancellationToken cancellationToken try { - HostObjectBuilderResult res = await hostObjectBuilder - .Build(commitObject, receiveInfo.ProjectName, receiveInfo.ModelName, onOperationProgressed, cancellationToken) - .ConfigureAwait(false); + HostObjectBuilderResult res = hostObjectBuilder.Build( + commitObject, + receiveInfo.ProjectName, + receiveInfo.ModelName, + onOperationProgressed, + cancellationToken + ); conversionActivity?.SetStatus(SdkActivityStatusCode.Ok); return res; } diff --git a/Sdk/Speckle.Connectors.Common/Operations/SendOperation.cs b/Sdk/Speckle.Connectors.Common/Operations/SendOperation.cs index 19a9d3524..60e9bda7b 100644 --- a/Sdk/Speckle.Connectors.Common/Operations/SendOperation.cs +++ b/Sdk/Speckle.Connectors.Common/Operations/SendOperation.cs @@ -1,6 +1,7 @@ using Speckle.Connectors.Common.Builders; using Speckle.Connectors.Common.Caching; using Speckle.Connectors.Common.Conversion; +using Speckle.Connectors.Common.Threading; using Speckle.Connectors.Logging; using Speckle.Sdk.Api; using Speckle.Sdk.Api.GraphQL.Inputs; @@ -19,7 +20,8 @@ public sealed class SendOperation( ISendProgress sendProgress, IOperations operations, IClientFactory clientFactory, - ISdkActivityFactory activityFactory + ISdkActivityFactory activityFactory, + IThreadContext threadContext ) { public async Task Execute( @@ -29,7 +31,9 @@ public async Task Execute( CancellationToken ct = default ) { - var buildResult = await rootObjectBuilder.Build(objects, sendInfo, onOperationProgressed, ct).ConfigureAwait(false); + var buildResult = await threadContext + .RunOnMain(() => rootObjectBuilder.Build(objects, sendInfo, onOperationProgressed)) + .ConfigureAwait(false); // POC: Jonathon asks on behalf of willow twin - let's explore how this can work // buildResult.RootObject["@report"] = new Report { ConversionResults = buildResult.ConversionResults }; @@ -37,13 +41,14 @@ public async Task Execute( buildResult.RootObject["version"] = 3; // base object handler is separated, so we can do some testing on non-production databases // exact interface may want to be tweaked when we implement this - var (rootObjId, convertedReferences) = await Send(buildResult.RootObject, sendInfo, onOperationProgressed, ct) + var (rootObjId, convertedReferences) = await threadContext + .RunOnWorkerAsync(() => Send(buildResult.RootObject, sendInfo, onOperationProgressed, ct)) .ConfigureAwait(false); return new(rootObjId, convertedReferences, buildResult.ConversionResults); } - public async Task Send( + public async ValueTask 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 new file mode 100644 index 000000000..29057f544 --- /dev/null +++ b/Sdk/Speckle.Connectors.Common/Threading/DefaultThreadContext.cs @@ -0,0 +1,72 @@ +using System.Diagnostics.CodeAnalysis; +using Speckle.Sdk.Common; + +namespace Speckle.Connectors.Common.Threading; + +public class DefaultThreadContext : ThreadContext +{ + private readonly SynchronizationContext _threadContext = SynchronizationContext.Current.NotNull( + "No UI thread to capture?" + ); + + [SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "TaskCompletionSource")] + protected override ValueTask WorkerToMainAsync(Func> action) + { + TaskCompletionSource tcs = new(); + _threadContext.Post( + async _ => + { + try + { + T result = await action().BackToCurrent(); + tcs.SetResult(result); + } + catch (Exception ex) + { + tcs.SetException(ex); + } + }, + null + ); + return new ValueTask(tcs.Task); + } + + protected override ValueTask MainToWorkerAsync(Func> action) + { + Task> f = Task.Factory.StartNew( + async () => await action().BackToCurrent(), + default, + TaskCreationOptions.LongRunning, + TaskScheduler.Default + ); + return new ValueTask(f.Unwrap()); + } + + [SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "TaskCompletionSource")] + protected override ValueTask WorkerToMain(Func action) + { + TaskCompletionSource tcs = new(); + _threadContext.Post( + _ => + { + try + { + T result = action(); + tcs.SetResult(result); + } + catch (Exception ex) + { + tcs.SetException(ex); + } + }, + null + ); + return new ValueTask(tcs.Task); + } + + protected override ValueTask MainToWorker(Func action) + { + Task f = Task.Factory.StartNew(action, default, TaskCreationOptions.LongRunning, TaskScheduler.Default); + return new ValueTask(f); + } +} diff --git a/Sdk/Speckle.Connectors.Common/Threading/TaskExtensions.cs b/Sdk/Speckle.Connectors.Common/Threading/TaskExtensions.cs new file mode 100644 index 000000000..157ec40ff --- /dev/null +++ b/Sdk/Speckle.Connectors.Common/Threading/TaskExtensions.cs @@ -0,0 +1,39 @@ +using System.Runtime.CompilerServices; + +namespace Speckle.Connectors.Common.Threading; + +public static class TaskExtensions +{ + public static ConfiguredValueTaskAwaitable BackToCurrent(this ValueTask valueTask) => + valueTask.ConfigureAwait(true); + + public static ConfiguredValueTaskAwaitable BackToAny(this ValueTask valueTask) => + valueTask.ConfigureAwait(false); + + public static ConfiguredValueTaskAwaitable BackToCurrent(this ValueTask valueTask) => valueTask.ConfigureAwait(true); + + public static ConfiguredValueTaskAwaitable BackToAny(this ValueTask valueTask) => valueTask.ConfigureAwait(false); + + public static ConfiguredTaskAwaitable BackToCurrent(this Task task) => task.ConfigureAwait(true); + + public static ConfiguredTaskAwaitable BackToAny(this Task task) => task.ConfigureAwait(false); + + public static ConfiguredTaskAwaitable BackToCurrent(this Task task) => task.ConfigureAwait(true); + + public static ConfiguredTaskAwaitable BackToAny(this Task task) => task.ConfigureAwait(false); + + public static ValueTask AsValueTask(this Task task) => new(task); + + public static ValueTask AsValueTask(this Task task) => new(task); + + public static void Wait(this Task task) => task.GetAwaiter().GetResult(); + + public static T Wait(this Task task) => task.GetAwaiter().GetResult(); + + public static void Wait(this ValueTask task) => task.GetAwaiter().GetResult(); + + public static T Wait(this ValueTask task) => task.GetAwaiter().GetResult(); +#pragma warning disable CA1030 + public static void FireAndForget(this ValueTask valueTask) => valueTask.BackToAny(); +#pragma warning restore CA1030 +} diff --git a/Sdk/Speckle.Connectors.Common/Threading/ThreadContext.cs b/Sdk/Speckle.Connectors.Common/Threading/ThreadContext.cs new file mode 100644 index 000000000..459f0285d --- /dev/null +++ b/Sdk/Speckle.Connectors.Common/Threading/ThreadContext.cs @@ -0,0 +1,125 @@ +using Speckle.InterfaceGenerator; + +namespace Speckle.Connectors.Common.Threading; + +[GenerateAutoInterface] +public abstract class ThreadContext : IThreadContext +{ + public static bool IsMainThread => Environment.CurrentManagedThreadId == 1 && !Thread.CurrentThread.IsBackground; + + public async ValueTask RunOnThread(Action action, bool useMain) + { + if (useMain) + { + if (IsMainThread) + { + action(); + } + else + { + await WorkerToMainAsync(() => + { + action(); + return new ValueTask(); + }) + .ConfigureAwait(false); + } + } + else + { + if (IsMainThread) + { + await MainToWorkerAsync(() => + { + action(); + return new ValueTask(); + }) + .BackToAny(); + } + else + { + action(); + } + } + } + + public virtual ValueTask RunOnThread(Func action, bool useMain) + { + if (useMain) + { + if (IsMainThread) + { + return new ValueTask(action()); + } + + return WorkerToMain(action); + } + if (IsMainThread) + { + return MainToWorker(action); + } + + return new ValueTask(action()); + } + + public async ValueTask RunOnThreadAsync(Func action, bool useMain) + { + if (useMain) + { + if (IsMainThread) + { + await action().BackToCurrent(); + } + else + { + await WorkerToMainAsync(async () => + { + await action().BackToCurrent(); + return new ValueTask(null); + }) + .BackToCurrent(); + } + } + else + { + if (IsMainThread) + { + await MainToWorkerAsync(async () => + { + await action().BackToCurrent(); + return new ValueTask(null); + }) + .BackToCurrent(); + } + else + { + await action().BackToCurrent(); + } + } + } + + public ValueTask RunOnThreadAsync(Func> action, bool useMain) + { + if (useMain) + { + if (IsMainThread) + { + return action(); + } + + return WorkerToMainAsync(action); + } + if (IsMainThread) + { + return MainToWorkerAsync(action); + } + return action(); + } + + protected abstract ValueTask WorkerToMainAsync(Func> action); + + protected abstract ValueTask MainToWorkerAsync(Func> action); + protected abstract ValueTask WorkerToMain(Func action); + + protected abstract ValueTask MainToWorker(Func action); +} diff --git a/Sdk/Speckle.Connectors.Common/Threading/ThreadContextExtensions.cs b/Sdk/Speckle.Connectors.Common/Threading/ThreadContextExtensions.cs new file mode 100644 index 000000000..f2c915d47 --- /dev/null +++ b/Sdk/Speckle.Connectors.Common/Threading/ThreadContextExtensions.cs @@ -0,0 +1,28 @@ +namespace Speckle.Connectors.Common.Threading; + +public static class ThreadContextExtensions +{ + public static ValueTask RunOnMain(this IThreadContext threadContext, Action action) => + threadContext.RunOnThread(action, true); + + public static ValueTask RunOnWorker(this IThreadContext threadContext, Action action) => + threadContext.RunOnThread(action, false); + + public static ValueTask RunOnMain(this IThreadContext threadContext, Func action) => + threadContext.RunOnThread(action, true); + + public static ValueTask RunOnWorker(this IThreadContext threadContext, Func action) => + threadContext.RunOnThread(action, false); + + public static ValueTask RunOnMainAsync(this IThreadContext threadContext, Func action) => + threadContext.RunOnThreadAsync(action, true); + + public static ValueTask RunOnWorkerAsync(this IThreadContext threadContext, Func action) => + threadContext.RunOnThreadAsync(action, false); + + public static ValueTask RunOnMainAsync(this IThreadContext threadContext, Func> action) => + threadContext.RunOnThreadAsync(action, true); + + public static ValueTask RunOnWorkerAsync(this IThreadContext threadContext, Func> action) => + threadContext.RunOnThreadAsync(action, false); +} diff --git a/Sdk/Speckle.Connectors.Common/Threading/ThreadOptions.cs b/Sdk/Speckle.Connectors.Common/Threading/ThreadOptions.cs new file mode 100644 index 000000000..3ef94b7f2 --- /dev/null +++ b/Sdk/Speckle.Connectors.Common/Threading/ThreadOptions.cs @@ -0,0 +1,12 @@ +using Speckle.InterfaceGenerator; +using Speckle.Sdk; +using Speckle.Sdk.Host; + +namespace Speckle.Connectors.Common.Threading; + +[GenerateAutoInterface] +public class ThreadOptions(ISpeckleApplication speckleApplication) : IThreadOptions +{ + public bool RunReceiveBuildOnMainThread => speckleApplication.HostApplication != HostApplications.Rhino.Name; + public bool RunCommandsOnMainThread => speckleApplication.HostApplication != HostApplications.ArcGIS.Name; +}