Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
# 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

- `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.
Expand Down
5 changes: 5 additions & 0 deletions source/Nice3point.Revit.Toolkit/Internal/UnsafeAccessors.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
5 changes: 5 additions & 0 deletions source/Nice3point.Revit.Toolkit/Internal/UnsafeUiAccessors.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

<PropertyGroup>
<UseWpf>true</UseWpf>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<DefineConstants>$(DefineConstants);JETBRAINS_ANNOTATIONS</DefineConstants>
<Configurations>Debug.R20;Debug.R21;Debug.R22;Debug.R23;Debug.R24;Debug.R25;Debug.R26;Debug.R27</Configurations>
<Configurations>$(Configurations);Release.R20;Release.R21;Release.R22;Release.R23;Release.R24;Release.R25;Release.R26;Release.R27</Configurations>
Expand Down
56 changes: 42 additions & 14 deletions source/Nice3point.Revit.Toolkit/RevitContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -12,7 +15,10 @@ namespace Nice3point.Revit.Toolkit;
[PublicAPI]
public class RevitContext : RevitApiContext
{
private static readonly Func<bool> 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;

Expand All @@ -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);

Expand All @@ -41,21 +55,21 @@ 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])!;
};
#if !REVIT2027_OR_GREATER

UiApplication = new UIApplication(Application);
#endif
}

/// <summary>
/// Represents an active session of the Autodesk Revit user interface, providing access to
/// UI customization methods, events, the main window, and the active document.
/// </summary>
#if REVIT2027_OR_GREATER
public static UIApplication UiApplication { get; } = UnsafeUiAccessors.GetUiApplicationInstance(null);
#else
public static UIApplication UiApplication { get; }
#endif

/// <summary>Represents a currently active Autodesk Revit project at the UI level.</summary>
/// <remarks>
Expand Down Expand Up @@ -142,7 +156,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.
/// </remarks>
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]<IntPtr>)ApiCallDepthManagerPointer)();
return ((delegate* unmanaged[Thiscall]<IntPtr, byte>)IsRevitInApiModePointer)(apiCallDepthManager) != 0;
}
}
}
#endif

/// <summary>
/// Begins a scope that suppresses the display of Revit dialogs.
Expand Down
31 changes: 31 additions & 0 deletions tests/Nice3point.Revit.Toolkit.Tests/RevitContextTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.IO;
using System.Windows.Threading;
using Autodesk.Revit.UI;
using Nice3point.TUnit.Revit;
using Nice3point.TUnit.Revit.Executors;
Expand Down Expand Up @@ -42,6 +43,36 @@ 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]
[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()
{
Expand Down