Transaction cannot be started after async code (Revit.Async)

Transaction cannot be started after async code (Revit.Async)

christev7HTEL
Explorer Explorer
285 Views
2 Replies
Message 1 of 3

Transaction cannot be started after async code (Revit.Async)

christev7HTEL
Explorer
Explorer
I've created a minimal reproduceable example here. Can someone help me understand what's going on here, I'm not sure what I'm missing exactly.
 
I'm using Revit.Async because I need to call some async code.
- I create a task inside my command.
- I have an `AsyncGenericExternalEventHandler` named `TaskRunnerExternalEventHandler` whose express purpose is to be able to run tasks, and then execute a transaction based on the result of those tasks.
- The external event is registered in the `TestCommand` class constructor
 
In this configuration, when trying to start a transaction, it throws the classic "Starting a transaction from an external application running outside of API context is not allowed." However if I comment out `await Task.WhenAll(tasks).ConfigureAwait(true);` it suddenly works.
 
I tried to enforce `ConfigureAwait(true)` to be sure `SynchronizationContext` is the same. I checked the thread id both before and after the awaited task, and it remains `1`, so I'm sure I'm still on the main UI thread. 
 
I'm not sure what I'm missing here...
 
 
using Autodesk.Revit.DB;
using Autodesk.Revit.UI;
using Revit.Async;

[Transaction(TransactionMode.Manual)]
public class TestCommand : IExternalCommand
{
    public TestCommand()
    {
        RevitTask.RegisterGlobal(new TaskRunnerExternalEventHandler());
    }

    protected override Result Execute(ExternalCommandData commandData, ref string message, ElementSet elements)
    {
        var task = Task.Delay(1000);
        RevitTask.RaiseGlobal<TaskRunnerExternalEventHandler, IEnumerable<Task>, bool>([task]);

        return Result.Succeeded;
    }
}
 
 
 
using Autodesk.Revit.DB;
using Autodesk.Revit.UI;
using Revit.Async.ExternalEvents;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Windows.Forms;

public class TaskRunnerExternalEventHandler : AsyncGenericExternalEventHandler<IEnumerable<Task>, bool>
{
    protected override async Task<bool> Handle(UIApplication app, IEnumerable<Task> tasks)
    {
var threadId1 = Thread.CurrentThread.ManagedThreadId;
        await Task.WhenAll(tasks).ConfigureAwait(true);
var threadId2 = Thread.CurrentThread.ManagedThreadId;

        try
        {
            using (var transaction = new Transaction(app.ActiveUIDocument.Document, "async issue"))
            {
                transaction.Start();
                // transaction is empty, symbolic only in this example
                transaction.Commit();
            }
        }
        catch (Exception e)
        {
            MessageBox.Show(e.Message);
            return false;
        }

        return true;
    }

    public override string GetName()
    {
        return GetType().Name;
    }

    public override object Clone()
    {
        return this;
    }
}

 

0 Likes
286 Views
2 Replies
Replies (2)
Message 2 of 3

christev7HTEL
Explorer
Explorer

For the record, there is a simple solution here:

 

Instead of starting the transaction in the current external event, I spin up another external event from the task runner which purely executes the transaction. (Maybe this looks a touch contrived in the current example, but in my application I do already have an event designed to run the exact logic that I was executing here).

 

But... This doesn't really answer my question as to what's happening here under the hood... As I stated in the above post, the thread Id remains constant, so we should be on the main thread, and thus should be a valid revit api context...

 

I suppose the learning here is that as long as the new event is registered from a valid revit context (in the external event constructor), then I can raise it from out-of-context... And that allows me to execute further chains of external events.. Should I ever need to switch between async and sync code repeatedly in revit...

 

using Autodesk.Revit.DB;
using Autodesk.Revit.UI;
using Revit.Async.ExternalEvents;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Windows.Forms;

public class TaskRunnerExternalEventHandler : AsyncGenericExternalEventHandler<IEnumerable<Task>, bool>
{
	public ReweApiCommandRunnerExternalEvent()
	{
	    RevitTask.RegisterGlobal(new ExecuteTransactionExternalEvent());
	}
	
    protected override async Task<bool> Handle(UIApplication app, IEnumerable<Task> tasks)
    {
        await Task.WhenAll(tasks).ConfigureAwait(true);
		
		await RevitTask.RaiseGlobal<ExecuteTransactionExternalEvent, object?, Result>(null);
		
        return true;
    }

    public override string GetName()
    {
        return GetType().Name;
    }

    public override object Clone()
    {
        return this;
    }
}

 

 

 

using Autodesk.Revit.DB;
using Autodesk.Revit.UI;
using Revit.Async.ExternalEvents;
using Rewe.Revit.Data.Components;
using Rewe.Revit.Data.Components.Common;
using Rewe.Revit.Utility;
using Rewe.Revit.Utility.Helpers;
using System.Linq;
using System.Windows;

namespace Rewe.Revit.ExternalEvents;

public class ExecuteTransactionExternalEvent : SyncGenericExternalEventHandler<object?, Result>
{
    protected override Result Handle(UIApplication uiApp, object? parameter)
    {
        var document = uiApp.ActiveUIDocument.Document;

        using (var transaction = new Transaction(app.ActiveUIDocument.Document, "async issue"))
            {
                transaction.Start();
                transaction.Commit();
            }

        return Result.Succeeded;
    }

    public override string GetName()
    {
        return GetType().Name;
    }

    public override object Clone()
    {
        throw new System.NotImplementedException();
    }
}

 

 

 

0 Likes
Message 3 of 3

jeremy_tammik
Alumni
Alumni

You can raise the external event in any thread you like. All other actions, all Revit API calls, all event registrations, every single thing needs to be executed from within the one and only valid Revit API context. This context is only provided within the event handler of an event provided by and called by Revit. 
  
Maybe it will help if you add code using one of the suggested techniques to determine whether you are currently within a valid Revit API context?

  

  

Jeremy Tammik Developer Advocacy and Support + The Building Coder + Autodesk Developer Network + ADN Open
0 Likes