From cd735c299e26d8dfe3e7f7e2d8ccf1ba733f1976 Mon Sep 17 00:00:00 2001 From: Nice3point Date: Thu, 15 Oct 2026 23:18:14 +0300 Subject: [PATCH 1/3] [Context] Check the API context through the Revit database API --- CHANGELOG.md | 4 ++ .../Internal/UnsafeAccessors.cs | 5 ++ .../Nice3point.Revit.Toolkit.csproj | 1 + .../Nice3point.Revit.Toolkit/RevitContext.cs | 50 +++++++++++++------ .../RevitContextTests.cs | 10 ++++ 5 files changed, 56 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 08b6bce..6b4e34c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# 2027.0.2 + +- `RevitContext.IsRevitInApiMode` no longer allocates and no longer requires `APIUIAPI`. On Revit 2027 it calls `Application.IsInAPIContext()`; on earlier versions it calls the native `RevitDBAPI` functions directly. A call takes 27 ns instead of 62 ns on Revit 2027. + # 2027.0.1 - `AsyncExternalEvent.RaiseAsync()` with `ExternalEventOptions.AllowDirectInvocation` returns a faulted task when the handler throws inside the Revit API context. The exception was previously lost and the task completed successfully. diff --git a/source/Nice3point.Revit.Toolkit/Internal/UnsafeAccessors.cs b/source/Nice3point.Revit.Toolkit/Internal/UnsafeAccessors.cs index 6d6db0d..38cc0f3 100644 --- a/source/Nice3point.Revit.Toolkit/Internal/UnsafeAccessors.cs +++ b/source/Nice3point.Revit.Toolkit/Internal/UnsafeAccessors.cs @@ -11,5 +11,10 @@ internal static class UnsafeAccessors { [UnsafeAccessor(UnsafeAccessorKind.Constructor)] internal static extern Application CreateApplication(object proxy); +#if REVIT2027_OR_GREATER + + [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "IsInAPIContext")] + internal static extern bool IsInApiContext(Application application); +#endif } #endif diff --git a/source/Nice3point.Revit.Toolkit/Nice3point.Revit.Toolkit.csproj b/source/Nice3point.Revit.Toolkit/Nice3point.Revit.Toolkit.csproj index 15e7b77..7d56518 100644 --- a/source/Nice3point.Revit.Toolkit/Nice3point.Revit.Toolkit.csproj +++ b/source/Nice3point.Revit.Toolkit/Nice3point.Revit.Toolkit.csproj @@ -2,6 +2,7 @@ true + true $(DefineConstants);JETBRAINS_ANNOTATIONS Debug.R20;Debug.R21;Debug.R22;Debug.R23;Debug.R24;Debug.R25;Debug.R26;Debug.R27 $(Configurations);Release.R20;Release.R21;Release.R22;Release.R23;Release.R24;Release.R25;Release.R26;Release.R27 diff --git a/source/Nice3point.Revit.Toolkit/RevitContext.cs b/source/Nice3point.Revit.Toolkit/RevitContext.cs index b829142..778df3e 100644 --- a/source/Nice3point.Revit.Toolkit/RevitContext.cs +++ b/source/Nice3point.Revit.Toolkit/RevitContext.cs @@ -3,6 +3,9 @@ using System.Windows; using Autodesk.Revit.UI; using Autodesk.Revit.UI.Events; +#if REVIT2027_OR_GREATER +using Nice3point.Revit.Toolkit.Internal; +#endif namespace Nice3point.Revit.Toolkit; @@ -12,7 +15,10 @@ namespace Nice3point.Revit.Toolkit; [PublicAPI] public class RevitContext : RevitApiContext { - private static readonly Func GetIsRevitInApiMode; +#if !REVIT2027_OR_GREATER + private static readonly IntPtr ApiCallDepthManagerPointer; + private static readonly IntPtr IsRevitInApiModePointer; +#endif private static readonly IntPtr IncrementConstructorPointer; private static readonly IntPtr IncrementDestructorPointer; @@ -23,16 +29,24 @@ public class RevitContext : RevitApiContext static RevitContext() { - var assemblies = FindAssemblies("APIUIAPI", "RevitAPIUI"); - - var apiAssemblyMethods = assemblies[0].ManifestModule.GetMethods(BindingFlags.NonPublic | BindingFlags.Static); - var apiCallDepthManagerMethod = apiAssemblyMethods.FirstOrDefault(static method => method.Name == "APICallDepthManager.singletonfactory"); +#if REVIT2027_OR_GREATER + var uiAssembly = FindAssemblies("RevitAPIUI")[0]; +#else + var assemblies = FindAssemblies("RevitDBAPI", "RevitAPIUI"); + var uiAssembly = assemblies[1]; + + var dbAssemblyMethods = assemblies[0].ManifestModule.GetMethods(BindingFlags.NonPublic | BindingFlags.Static); + var apiCallDepthManagerMethod = dbAssemblyMethods.FirstOrDefault(static method => method.Name == "APICallDepthManager.singletonfactory"); ThrowWhen(apiCallDepthManagerMethod is null); - var isRevitInApiModeMethod = apiAssemblyMethods.FirstOrDefault(static method => method.Name == "APICallDepthManager.isRevitInAPIMode"); + var isRevitInApiModeMethod = dbAssemblyMethods.FirstOrDefault(static method => method.Name == "APICallDepthManager.isRevitInAPIMode"); ThrowWhen(isRevitInApiModeMethod is null); - var uiAssemblyMethods = assemblies[1].ManifestModule.GetMethods(BindingFlags.NonPublic | BindingFlags.Static); + ApiCallDepthManagerPointer = apiCallDepthManagerMethod.MethodHandle.GetFunctionPointer(); + IsRevitInApiModePointer = isRevitInApiModeMethod.MethodHandle.GetFunctionPointer(); +#endif + + var uiAssemblyMethods = uiAssembly.ManifestModule.GetMethods(BindingFlags.NonPublic | BindingFlags.Static); var incrementConstructor = uiAssemblyMethods.FirstOrDefault(static method => method.Name == "IncrementAPICallDepth.{ctor}"); ThrowWhen(incrementConstructor is null); @@ -42,12 +56,6 @@ static RevitContext() IncrementConstructorPointer = incrementConstructor.MethodHandle.GetFunctionPointer(); IncrementDestructorPointer = incrementDestructor.MethodHandle.GetFunctionPointer(); - GetIsRevitInApiMode = () => - { - var apiCallDepthManager = apiCallDepthManagerMethod.Invoke(obj: null, parameters: null); - return (bool)isRevitInApiModeMethod.Invoke(obj: null, [apiCallDepthManager])!; - }; - UiApplication = new UIApplication(Application); } @@ -142,7 +150,21 @@ public static View? ActiveView /// IExternalEventHandler enables safely executing commands and operations from external threads /// or the user interface, ensuring they are synchronized with Revit's main thread. /// - public static bool IsRevitInApiMode => GetIsRevitInApiMode(); +#if REVIT2027_OR_GREATER + public static bool IsRevitInApiMode => UnsafeAccessors.IsInApiContext(Application); +#else + public static bool IsRevitInApiMode + { + get + { + unsafe + { + var apiCallDepthManager = ((delegate* unmanaged[Cdecl])ApiCallDepthManagerPointer)(); + return ((delegate* unmanaged[Thiscall])IsRevitInApiModePointer)(apiCallDepthManager) != 0; + } + } + } +#endif /// /// Begins a scope that suppresses the display of Revit dialogs. diff --git a/tests/Nice3point.Revit.Toolkit.Tests/RevitContextTests.cs b/tests/Nice3point.Revit.Toolkit.Tests/RevitContextTests.cs index 114f6d7..42571dd 100644 --- a/tests/Nice3point.Revit.Toolkit.Tests/RevitContextTests.cs +++ b/tests/Nice3point.Revit.Toolkit.Tests/RevitContextTests.cs @@ -42,6 +42,16 @@ public async Task IsRevitInApiMode_InsideTheApiContext_ReturnsTrue() await Assert.That(isRevitInApiMode).IsTrue(); } + [Test] + public async Task IsRevitInApiMode_OutsideTheRevitThread_ReturnsFalse() + { + // Act + var isRevitInApiMode = await Task.Run(() => RevitContext.IsRevitInApiMode); + + // Assert + await Assert.That(isRevitInApiMode).IsFalse(); + } + [Test] public async Task ActiveDocument_OpenedModel_ReturnsTheActiveDocument() { From 3037e02f66f79a0cf62c03a8d2e66cd1f5cc7e49 Mon Sep 17 00:00:00 2001 From: Nice3point Date: Thu, 15 Oct 2026 23:18:27 +0300 Subject: [PATCH 2/3] [Context] Take the UI application from the Revit 2027 session --- CHANGELOG.md | 1 + .../Nice3point.Revit.Toolkit/Internal/UnsafeUiAccessors.cs | 5 +++++ source/Nice3point.Revit.Toolkit/RevitContext.cs | 6 ++++++ 3 files changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b4e34c..86fd95f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # 2027.0.2 - `RevitContext.IsRevitInApiMode` no longer allocates and no longer requires `APIUIAPI`. On Revit 2027 it calls `Application.IsInAPIContext()`; on earlier versions it calls the native `RevitDBAPI` functions directly. A call takes 27 ns instead of 62 ns on Revit 2027. +- On Revit 2027, `RevitContext.UiApplication` is the session `UIApplication` that Revit provides, instead of one constructed from `RevitApiContext.Application`. # 2027.0.1 diff --git a/source/Nice3point.Revit.Toolkit/Internal/UnsafeUiAccessors.cs b/source/Nice3point.Revit.Toolkit/Internal/UnsafeUiAccessors.cs index c941bee..941f5f1 100644 --- a/source/Nice3point.Revit.Toolkit/Internal/UnsafeUiAccessors.cs +++ b/source/Nice3point.Revit.Toolkit/Internal/UnsafeUiAccessors.cs @@ -13,5 +13,10 @@ internal static class UnsafeUiAccessors { [UnsafeAccessor(UnsafeAccessorKind.Constructor)] internal static extern UIControlledApplication CreateUiControlledApplication(UIApplication uiApplication); +#if REVIT2027_OR_GREATER + + [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "get_Instance")] + internal static extern UIApplication GetUiApplicationInstance(UIApplication? target); +#endif } #endif diff --git a/source/Nice3point.Revit.Toolkit/RevitContext.cs b/source/Nice3point.Revit.Toolkit/RevitContext.cs index 778df3e..3f4d160 100644 --- a/source/Nice3point.Revit.Toolkit/RevitContext.cs +++ b/source/Nice3point.Revit.Toolkit/RevitContext.cs @@ -55,15 +55,21 @@ static RevitContext() IncrementConstructorPointer = incrementConstructor.MethodHandle.GetFunctionPointer(); IncrementDestructorPointer = incrementDestructor.MethodHandle.GetFunctionPointer(); +#if !REVIT2027_OR_GREATER UiApplication = new UIApplication(Application); +#endif } /// /// Represents an active session of the Autodesk Revit user interface, providing access to /// UI customization methods, events, the main window, and the active document. /// +#if REVIT2027_OR_GREATER + public static UIApplication UiApplication { get; } = UnsafeUiAccessors.GetUiApplicationInstance(null); +#else public static UIApplication UiApplication { get; } +#endif /// Represents a currently active Autodesk Revit project at the UI level. /// From ecb49bf2ea3d53b89a52ef5d695546f2183f9f43 Mon Sep 17 00:00:00 2001 From: Nice3point Date: Thu, 15 Oct 2026 23:31:23 +0300 Subject: [PATCH 3/3] [Tests] Cover the API context check on the Revit thread outside the API context --- .../RevitContextTests.cs | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/Nice3point.Revit.Toolkit.Tests/RevitContextTests.cs b/tests/Nice3point.Revit.Toolkit.Tests/RevitContextTests.cs index 42571dd..1d7c3fd 100644 --- a/tests/Nice3point.Revit.Toolkit.Tests/RevitContextTests.cs +++ b/tests/Nice3point.Revit.Toolkit.Tests/RevitContextTests.cs @@ -1,4 +1,5 @@ using System.IO; +using System.Windows.Threading; using Autodesk.Revit.UI; using Nice3point.TUnit.Revit; using Nice3point.TUnit.Revit.Executors; @@ -52,6 +53,26 @@ public async Task IsRevitInApiMode_OutsideTheRevitThread_ReturnsFalse() await Assert.That(isRevitInApiMode).IsFalse(); } + [Test] + [Timeout(10_000)] + public async Task IsRevitInApiMode_RevitThreadOutsideTheApiContext_ReturnsFalse(CancellationToken cancellationToken) + { + // Arrange + var revitThreadId = Environment.CurrentManagedThreadId; + var completion = new TaskCompletionSource<(int ThreadId, bool IsRevitInApiMode)>(TaskCreationOptions.RunContinuationsAsynchronously); + + // Act + Dispatcher.CurrentDispatcher.BeginInvoke(() => completion.SetResult((Environment.CurrentManagedThreadId, RevitContext.IsRevitInApiMode))); + var (threadId, isRevitInApiMode) = await completion.Task.WaitAsync(cancellationToken); + + // Assert + using (Assert.Multiple()) + { + await Assert.That(threadId).IsEqualTo(revitThreadId); + await Assert.That(isRevitInApiMode).IsFalse(); + } + } + [Test] public async Task ActiveDocument_OpenedModel_ReturnsTheActiveDocument() {