Skip to content

Commit

Permalink
Use Event Aggregator with Autocad/Civil (#517)
Browse files Browse the repository at this point in the history
* Rework doc selection

* add more events

* use idle events

* Fix autocad events and format
  • Loading branch information
adamhathcock authored Jan 28, 2025
1 parent 81c3c42 commit 79a6062
Show file tree
Hide file tree
Showing 12 changed files with 136 additions and 91 deletions.
Original file line number Diff line number Diff line change
@@ -1,42 +1,48 @@
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Speckle.Connectors.Autocad.HostApp.Extensions;
using Speckle.Connectors.Common.Threading;
using Speckle.Connectors.Autocad.Plugin;
using Speckle.Connectors.DUI.Bindings;
using Speckle.Connectors.DUI.Bridge;
using Speckle.Connectors.DUI.Eventing;

namespace Speckle.Connectors.Autocad.Bindings;

public class AutocadSelectionBinding : ISelectionBinding
{
private const string SELECTION_EVENT = "setSelection";
private readonly ITopLevelExceptionHandler _topLevelExceptionHandler;
private readonly IThreadContext _threadContext;
private readonly HashSet<Document> _visitedDocuments = new();
private readonly IEventAggregator _eventAggregator;
private readonly HashSet<string> _visitedDocuments = new();

public string Name => "selectionBinding";

public IBrowserBridge Parent { get; }

public AutocadSelectionBinding(
IBrowserBridge parent,
IThreadContext threadContext,
ITopLevelExceptionHandler topLevelExceptionHandler
)
public AutocadSelectionBinding(IBrowserBridge parent, IEventAggregator eventAggregator)
{
_topLevelExceptionHandler = topLevelExceptionHandler;
_eventAggregator = eventAggregator;
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
// This is valid when user opens acad file directly double clicking
TryRegisterDocumentForSelection(Application.DocumentManager.MdiActiveDocument);
Application.DocumentManager.DocumentActivated += (_, e) =>
_topLevelExceptionHandler.CatchUnhandled(() => OnDocumentChanged(e.Document));
eventAggregator.GetEvent<DocumentActivatedEvent>().Subscribe(OnDocumentChanged);
eventAggregator.GetEvent<ImpliedSelectionChangedEvent>().Subscribe(OnSelectionChanged);
eventAggregator.GetEvent<DocumentToBeDestroyedEvent>().Subscribe(OnDocumentDestroyed);
}

private void OnDocumentChanged(Document? document) => TryRegisterDocumentForSelection(document);
private void OnDocumentDestroyed(DocumentCollectionEventArgs e)
{
if (!_visitedDocuments.Contains(e.Document.Name))
{
e.Document.ImpliedSelectionChanged -= DocumentOnImpliedSelectionChanged;
_visitedDocuments.Remove(e.Document.Name);
}
}

private void OnDocumentChanged(DocumentCollectionEventArgs e) => TryRegisterDocumentForSelection(e.Document);

private void TryRegisterDocumentForSelection(Document? document)
{
Expand All @@ -45,21 +51,24 @@ private void TryRegisterDocumentForSelection(Document? document)
return;
}

if (!_visitedDocuments.Contains(document))
if (!_visitedDocuments.Contains(document.Name))
{
document.ImpliedSelectionChanged += (_, _) =>
_topLevelExceptionHandler.FireAndForget(async () => await _threadContext.RunOnMainAsync(OnSelectionChanged));
document.ImpliedSelectionChanged += DocumentOnImpliedSelectionChanged;

_visitedDocuments.Add(document);
_visitedDocuments.Add(document.Name);
}
}

// ReSharper disable once AsyncVoidMethod
private async void DocumentOnImpliedSelectionChanged(object? sender, EventArgs e) =>
await _eventAggregator.GetEvent<ImpliedSelectionChangedEvent>().PublishAsync(e);

// NOTE: Autocad 2022 caused problems, so we need to refactor things a bit in here to always store
// selection info locally (and get it updated by the event, which we can control to run on the main thread).
// 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 Task OnSelectionChanged(EventArgs _)
{
_selectionInfo = GetSelectionInternal();
await Parent.Send(SELECTION_EVENT, _selectionInfo);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Speckle.Connectors.Autocad.HostApp;
using Speckle.Connectors.Autocad.HostApp.Extensions;
using Speckle.Connectors.Autocad.Operations.Send;
using Speckle.Connectors.Autocad.Plugin;
using Speckle.Connectors.Common.Caching;
using Speckle.Connectors.Common.Cancellation;
using Speckle.Connectors.Common.Operations;
Expand All @@ -23,6 +25,7 @@

namespace Speckle.Connectors.Autocad.Bindings;

[SuppressMessage("ReSharper", "AsyncVoidMethod")]
public abstract class AutocadSendBaseBinding : ISendBinding
{
public string Name => "sendBinding";
Expand All @@ -31,16 +34,15 @@ public abstract class AutocadSendBaseBinding : ISendBinding
public IBrowserBridge Parent { get; }

private readonly DocumentModelStore _store;
private readonly IAutocadIdleManager _idleManager;
private readonly List<ISendFilter> _sendFilters;
private readonly CancellationManager _cancellationManager;
private readonly IServiceProvider _serviceProvider;
private readonly ISendConversionCache _sendConversionCache;
private readonly IOperationProgressManager _operationProgressManager;
private readonly ILogger<AutocadSendBinding> _logger;
private readonly ITopLevelExceptionHandler _topLevelExceptionHandler;
private readonly ISpeckleApplication _speckleApplication;
private readonly IThreadContext _threadContext;
private readonly IEventAggregator _eventAggregator;

/// <summary>
/// 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:
Expand All @@ -52,7 +54,6 @@ public abstract class AutocadSendBaseBinding : ISendBinding

protected AutocadSendBaseBinding(
DocumentModelStore store,
IAutocadIdleManager idleManager,
IBrowserBridge parent,
IEnumerable<ISendFilter> sendFilters,
CancellationManager cancellationManager,
Expand All @@ -61,13 +62,11 @@ protected AutocadSendBaseBinding(
IOperationProgressManager operationProgressManager,
ILogger<AutocadSendBinding> logger,
ISpeckleApplication speckleApplication,
ITopLevelExceptionHandler topLevelExceptionHandler,
IThreadContext threadContext,
IEventAggregator eventAggregator
)
{
_store = store;
_idleManager = idleManager;
_serviceProvider = serviceProvider;
_cancellationManager = cancellationManager;
_sendFilters = sendFilters.ToList();
Expand All @@ -76,52 +75,80 @@ IEventAggregator eventAggregator
_logger = logger;
_speckleApplication = speckleApplication;
_threadContext = threadContext;
_topLevelExceptionHandler = topLevelExceptionHandler;
_eventAggregator = eventAggregator;
Parent = parent;
Commands = new SendBindingUICommands(parent);

Application.DocumentManager.DocumentActivated += (_, args) =>
_topLevelExceptionHandler.CatchUnhandled(() => SubscribeToObjectChanges(args.Document));

if (Application.DocumentManager.CurrentDocument != null)
{
// catches the case when autocad just opens up with a blank new doc
SubscribeToObjectChanges(Application.DocumentManager.CurrentDocument);
TryRegisterSubscribeToObjectChanges(Application.DocumentManager.CurrentDocument);
}
// Since ids of the objects generates from same seed, we should clear the cache always whenever doc swapped.

eventAggregator.GetEvent<DocumentActivatedEvent>().Subscribe(SubscribeToObjectChanges);
eventAggregator.GetEvent<DocumentStoreChangedEvent>().Subscribe(OnDocumentStoreChangedEvent);
eventAggregator.GetEvent<DocumentToBeDestroyedEvent>().Subscribe(OnDocumentDestroyed);
eventAggregator.GetEvent<ObjectAppendedEvent>().Subscribe(OnObjectAppended);
eventAggregator.GetEvent<ObjectErasedEvent>().Subscribe(ObjectErased);
eventAggregator.GetEvent<ObjectModifiedEvent>().Subscribe(ObjectModified);
}

private void OnDocumentDestroyed(DocumentCollectionEventArgs args)
{
Document doc = args.Document;
if (!_docSubsTracker.Contains(doc.Name))
{
doc.Database.ObjectAppended -= DatabaseOnObjectAppended;
doc.Database.ObjectErased -= DatabaseOnObjectErased;
doc.Database.ObjectModified -= DatabaseObjectModified;

_docSubsTracker.Remove(doc.Name);
}
}

private void OnDocumentStoreChangedEvent(object _) => _sendConversionCache.ClearCache();

private readonly List<string> _docSubsTracker = new();

private void SubscribeToObjectChanges(Document doc)
private void SubscribeToObjectChanges(DocumentCollectionEventArgs e) =>
TryRegisterSubscribeToObjectChanges(e.Document);

private void TryRegisterSubscribeToObjectChanges(Document? doc)
{
if (doc == null || doc.Database == null || _docSubsTracker.Contains(doc.Name))
{
return;
}

_docSubsTracker.Add(doc.Name);
doc.Database.ObjectAppended += (_, e) => OnObjectChanged(e.DBObject);
doc.Database.ObjectErased += (_, e) => OnObjectChanged(e.DBObject);
doc.Database.ObjectModified += (_, e) => OnObjectChanged(e.DBObject);
doc.Database.ObjectAppended += DatabaseOnObjectAppended;
doc.Database.ObjectErased += DatabaseOnObjectErased;
doc.Database.ObjectModified += DatabaseObjectModified;
}

private void OnObjectChanged(DBObject dbObject)
{
_topLevelExceptionHandler.CatchUnhandled(() => OnChangeChangedObjectIds(dbObject));
}
private async void DatabaseOnObjectAppended(object sender, ObjectEventArgs e) =>
await _eventAggregator.GetEvent<ObjectAppendedEvent>().PublishAsync(e);

private async void DatabaseOnObjectErased(object sender, ObjectErasedEventArgs e) =>
await _eventAggregator.GetEvent<ObjectErasedEvent>().PublishAsync(e);

private async void DatabaseObjectModified(object sender, ObjectEventArgs e) =>
await _eventAggregator.GetEvent<ObjectModifiedEvent>().PublishAsync(e);

private void OnObjectAppended(ObjectEventArgs e) => OnChangeChangedObjectIds(e.DBObject);

private void ObjectErased(ObjectErasedEventArgs e) => OnChangeChangedObjectIds(e.DBObject);

private void ObjectModified(ObjectEventArgs e) => OnChangeChangedObjectIds(e.DBObject);

private void OnChangeChangedObjectIds(DBObject dBObject)
{
ChangedObjectIds[dBObject.GetSpeckleApplicationId()] = 1;
_idleManager.SubscribeToIdle(nameof(AutocadSendBinding), async () => await RunExpirationChecks());
_eventAggregator.GetEvent<IdleEvent>().OneTimeSubscribe(nameof(AutocadSendBinding), RunExpirationChecks);
}

private async Task RunExpirationChecks()
private async Task RunExpirationChecks(object _)
{
var senders = _store.GetSenders();
string[] objectIdsList = ChangedObjectIds.Keys.ToArray();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Speckle.Connectors.Autocad.HostApp;
using Speckle.Connectors.Common.Caching;
using Speckle.Connectors.Common.Cancellation;
using Speckle.Connectors.Common.Threading;
Expand All @@ -21,7 +20,6 @@ public sealed class AutocadSendBinding : AutocadSendBaseBinding

public AutocadSendBinding(
DocumentModelStore store,
IAutocadIdleManager idleManager,
IBrowserBridge parent,
IEnumerable<ISendFilter> sendFilters,
CancellationManager cancellationManager,
Expand All @@ -31,13 +29,11 @@ public AutocadSendBinding(
ILogger<AutocadSendBinding> logger,
IAutocadConversionSettingsFactory autocadConversionSettingsFactory,
ISpeckleApplication speckleApplication,
ITopLevelExceptionHandler topLevelExceptionHandler,
IThreadContext threadContext,
IEventAggregator eventAggregator
)
: base(
store,
idleManager,
parent,
sendFilters,
cancellationManager,
Expand All @@ -46,7 +42,6 @@ IEventAggregator eventAggregator
operationProgressManager,
logger,
speckleApplication,
topLevelExceptionHandler,
threadContext,
eventAggregator
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
using Speckle.Connectors.Common.Threading;
using Speckle.Connectors.DUI;
using Speckle.Connectors.DUI.Bindings;
using Speckle.Connectors.DUI.Bridge;
using Speckle.Connectors.DUI.Models.Card.SendFilter;
using Speckle.Connectors.DUI.WebView;
using Speckle.Sdk.Models.GraphTraversal;
Expand Down Expand Up @@ -49,8 +48,6 @@ public static void AddAutocadBase(this IServiceCollection serviceCollection)
serviceCollection.AddScoped<AutocadMaterialUnpacker>();
serviceCollection.AddScoped<IAutocadMaterialBaker, AutocadMaterialBaker>();

serviceCollection.AddSingleton<IAppIdleManager, AutocadIdleManager>();

// operation progress manager
serviceCollection.AddSingleton<IOperationProgressManager, OperationProgressManager>();

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Speckle.Connectors.DUI.Bridge;
using Autodesk.AutoCAD.ApplicationServices;
using Speckle.Connectors.Autocad.Plugin;
using Speckle.Connectors.DUI.Eventing;
using Speckle.Connectors.DUI.Models;
using Speckle.Connectors.DUI.Utils;
Expand All @@ -7,44 +8,47 @@ namespace Speckle.Connectors.Autocad.HostApp;

public class AutocadDocumentStore : DocumentModelStore
{
private readonly string _nullDocumentName = "Null Doc";
private const string NULL_DOCUMENT_NAME = "Null Doc";
private string _previousDocName;
private readonly AutocadDocumentManager _autocadDocumentManager;
private readonly IEventAggregator _eventAggregator;

public AutocadDocumentStore(
IJsonSerializer jsonSerializer,
AutocadDocumentManager autocadDocumentManager,
ITopLevelExceptionHandler topLevelExceptionHandler,
IEventAggregator eventAggregator
)
: base(jsonSerializer)
{
_autocadDocumentManager = autocadDocumentManager;
_eventAggregator = eventAggregator;
_previousDocName = _nullDocumentName;
_previousDocName = NULL_DOCUMENT_NAME;

eventAggregator.GetEvent<DocumentActivatedEvent>().Subscribe(DocChanged);

// since below event triggered as secondary, it breaks the logic in OnDocChangeInternal function, leaving it here for now.
// Autodesk.AutoCAD.ApplicationServices.Application.DocumentWindowCollection.DocumentWindowActivated += (_, args) =>
// OnDocChangeInternal((Document)args.DocumentWindow.Document);
}

public override async Task OnDocumentStoreInitialized()
{
// POC: Will be addressed to move it into AutocadContext!
if (Application.DocumentManager.MdiActiveDocument != null)
{
IsDocumentInit = true;
// POC: this logic might go when we have document management in context
// It is with the case of if binding created with already a document
// This is valid when user opens acad file directly double clicking
OnDocChangeInternal(Application.DocumentManager.MdiActiveDocument);
await TryDocChanged(Application.DocumentManager.MdiActiveDocument);
}

Application.DocumentManager.DocumentActivated += (_, e) =>
topLevelExceptionHandler.CatchUnhandled(() => OnDocChangeInternal(e.Document));

// since below event triggered as secondary, it breaks the logic in OnDocChangeInternal function, leaving it here for now.
// Autodesk.AutoCAD.ApplicationServices.Application.DocumentWindowCollection.DocumentWindowActivated += (_, args) =>
// OnDocChangeInternal((Document)args.DocumentWindow.Document);
}

private async void OnDocChangeInternal(Document? doc)
private async Task DocChanged(DocumentCollectionEventArgs e) => await TryDocChanged(e.Document);

private async Task TryDocChanged(Document? doc)
{
var currentDocName = doc != null ? doc.Name : _nullDocumentName;
var currentDocName = doc != null ? doc.Name : NULL_DOCUMENT_NAME;
if (_previousDocName == currentDocName)
{
return;
Expand Down

This file was deleted.

Loading

0 comments on commit 79a6062

Please sign in to comment.