Repository navigation
[Events] Run an external event created outside a Revit callback as the add-in of its handler - #135
Conversation
|
Adds The proposal removes nothing from the current change: The conceptRevit binds an external event to the add-in on top of the add-in data stack of the creating thread, at The current change copies the active add-in data inside a callback and pushes the copy when an event is created outside one.
The designpublic static void RegisterAddInAssembly(this UIControlledApplication application, Assembly assembly);
public static void RegisterAddInAssembly(this UIApplication application, Assembly assembly);// OnStartup of ExternalApplication, or of a raw IExternalApplication
Application.RegisterAddInAssembly(typeof(OpeningStore).Assembly);
Name and placementThe rules come from
Alternatives evaluated
MeasurementsLookup of the captured add-in, BenchmarkDotNet ShortRun:
Where to look
Open questions
|
|
Proposes a second step after The step follows the ASP.NET Core model: MVC generates an The design
// <auto-generated/>
[assembly: global::Nice3point.Revit.Toolkit.External.AddInAssembly("MyAddIn.Core")]
[assembly: global::Nice3point.Revit.Toolkit.External.AddInAssembly("MyAddIn.Services")]
[assembly: global::Nice3point.Revit.Toolkit.External.AddInAssembly("MyAddIn.UI")]
The MSBuild target: <ItemGroup>
<CompilerVisibleProperty Include="AddInProjectReferences" />
</ItemGroup>
<Target Name="CollectAddInProjectReferences" AfterTargets="ResolveAssemblyReferences" BeforeTargets="GenerateMSBuildEditorConfigFileShouldRun">
<PropertyGroup>
<AddInProjectReferences>@(ReferencePath->WithMetadataValue('ReferenceSourceTarget', 'ProjectReference')->'%(Filename)', ',')</AddInProjectReferences>
</PropertyGroup>
</Target>How verifiedA prototype generator and the target above ran against an add-in with this reference graph:
Live, Revit 2027, a raw
Where to look
Open questions
|
… assemblies as the add-in
…ly and publish captures atomically
Summary of the Pull Request
What is this about:
Runs the handler of an external event created outside a Revit callback as the add-in that uses the event.
Description:
What changed
ExternalApplication.OnStartupandExternalCommand.Executecapture the active add-in for their assembly.AddInDataScopewith the captured add-in aroundExternalEvent.Create.ExternalEventHandlersubclass.private protectedconstructor ofExternalEventHandler.RevitClientadd-in with a vendor throughNice3point.TUnit.Revitx.1.8 and TUnit 1.76.45.RegisterAddInAssembly(Assembly)extendsUIControlledApplicationandUIApplication. Inside a Revit callback it captures the active add-in for the assembly and replaces an earlier capture. Outside a callback it throwsInvalidOperationException.build/Nice3point.Revit.Toolkit.targets. The_RevitToolkitCollectAddInProjectReferencestarget passes the project references of the project, transitive ones included, to the compiler.AddInAssemblyGeneratordeclares an internal attribute type in the assembly and one[assembly: AddInAssembly("…")]per project reference. The output compiles with C# 7.3.AddInAssemblyattributes by full name. A listed assembly is matched by name at its first external event created outside a callback.ExternalApplicationorExternalCommandwith project referencesIExternalApplicationapplication.RegisterAddInAssembly(typeof(Application).Assembly)Assembly.LoadFromRegisterAddInAssemblyfor that assemblyThe native side of Revit 2023 to 2027, read from
RevitAPIUI,RevitAPIFoundation, andUtility:ExternalEvent.Createregisters the handler for the add-in on top of the add-in data stackregisterAddInDataregisters nothing on an empty stackThreadLocalPointerE42FF806-491D-4B17-9AFB-EA051D5EBB76in 2027ArrStack<AddInData>::empty(getAddInDataStack()), as Revit doesAddInDataandAddInDataScopetake 24 and 16 bytes in 2020 to 2022, 24 and 2 in 2023 to 2026, 56 and 56 in 2027AddInDataScopekeeps an analytics timer in its own memorystackallocof the creating frameWhere to look
AssemblyLoadContextor on .NET Framework keys the captured add-in by assembly. A toolkit in the context of an isolated add-in keeps one add-in.AssemblyLoadContextmatch: an isolated add-in can load its own assembly with a listed name.StrongBox<nint>in the table. A capture publishes its copy withInterlocked.CompareExchange, and a registration replaces it withVolatile.Write. A lock guards the listed names only.%(Filename)of a project reference. The compiler names an assembly after its output file,TargetNameincluded._RevitToolkitprefix. A target of the same name in a consumer build replaces the package target.Delegate.Methodcosts 70–600 ns and 160 bytes on every new delegate. The target costs 9–24 ns with no allocation.RevitAPIFoundation2020 to 2022 are unverified.How verified
Release.R20toRelease.R27, zero errors.Nice3point.Revit.Toolkit.Testson Revit 2023, 2024, 2025, and 2027: 124 passed on each.RegisterAddInAssemblyand the generator: 132 of 132 Revit 2027 tests and 192 of 192 source generator tests passed.ListedAddInAssembliesimplements the name matching without native calls, and its tests run on the Revit thread. The package containsbuild/Nice3point.Revit.Toolkit.targets.net48consumer withLangVersion7.3 andTreatWarningsAsErrorsbuilds with the package targets and the generator.TargetNameset apart fromAssemblyName.AssemblyLoadContext, each built against the package targets and the generator, gave the same result:ExternalApplication, project reference, no registration codeIExternalApplication, project reference, after registrationIExternalApplication, transitive project referenceLoadFromand registeredLoadFrom, not registerednullnullRegisterAddInAssemblyon a thread-pool threadInvalidOperationExceptionWriting of Entities of this Schema is not allowed to the current add-in.gcrootExternalEvent.CreateandDispose, for scaledevelopfails too. The installedRevitAPIis 26.5, and the tests request 26.4.10.Quality Checklist