Skip to main content

revitlookup-descriptor-model

Teach RevitLookup to inspect a new Revit type by adding a LookupEngine descriptor and registering it in the type map, giving the object resolved members, synthetic extensions, and context-menu actions. USE FOR: creating a descriptor class, resolving members that need arguments or the active view, disabling or marking members unsupported, registering synthetic extension members, redirecting one type to another, adding context-menu commands that touch Revit through an external event, and wiring the descriptor into DescriptorsMap. DO NOT USE FOR: building a UI page or view model (use revitlookup-ui-development), or changing the LookupEngine engine itself in the submodule.

Source facts

Repository
lookup-foundation/RevitLookup
Last source activity
August 2, 2026 at 12:47
Detected SKILL.md language
English
Stars
1,432
Forks
359

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
revitlookup-descriptor-model
description
Teach RevitLookup to inspect a new Revit type by adding a LookupEngine descriptor and registering it in the type map, giving the object resolved members, synthetic extensions, and context-menu actions. USE FOR: creating a descriptor class, resolving members that need arguments or the active view, disabling or marking members unsupported, registering synthetic extension members, redirecting one type to another, adding context-menu commands that touch Revit through an external event, and wiring the descriptor into DescriptorsMap. DO NOT USE FOR: building a UI page or view model (use revitlookup-ui-development), or changing the LookupEngine engine itself in the submodule.
license
MIT
# RevitLookup Descriptor Model RevitLookup decomposes any object through LookupEngine, and a descriptor is how it teaches the engine about one Revit type. Add support for a type by writing a descriptor and adding one `case` to the type map — never by special-casing the type anywhere else. Descriptors live in `source/RevitLookup/Decomposition/Descriptors/`; the worked reference is `ElementDescriptor.cs`. ## When to use - Adding or changing how a Revit type decomposes: resolving members, exposing synthetic members, redirecting, or adding context-menu actions. ## When not to use - Changing the decompose pipeline, the never-throw contract, or the `Variants` API, which live in the `source/LookupEngine` submodule. ## Workflow ### Step 1: Create the descriptor and set its name Add `XxxDescriptor.cs` deriving from `Descriptor` (`LookupEngine.Abstractions.Decomposition`), or from RevitLookup's `ResolvingDescriptor` when you need its protected `Resolve...` helpers. Capture the object in the constructor and set `Name` to a human label. ```csharp public sealed class WallDescriptor : Descriptor { private readonly Wall _wall; public WallDescriptor(Wall wall) { _wall = wall; Name = $"{wall.Name}, ID{wall.Id}"; } } ``` If the type derived from another type and you want to reuse its constructor, inherit from it: ```csharp public sealed class FamilyDescriptor(Family family) : ElementDescriptor(family) { //No constructor, the Name policy derived from ElementDescriptor. } ``` ### Step 2: Opt into behavior through the configuration interfaces Implement only the interfaces the type needs: - `IDescriptorConfigurator` and `IDescriptorConfigurator<Document>` — `Configure(IMemberConfigurator)` to shape members. - `IDescriptorRedirector<Document>` — `TryRedirect(string target, Document context, out object result)` hands off to a richer type, as `ElementIdDescriptor` resolves an `ElementId` to its `Element`. - `IContextMenuConnector` — `RegisterMenu(...)` to add actions in the UI. - `IDescriptorCollector` — a marker for UI that the object exposes members worth drilling into. ### Step 3: Resolve, disable, and register members in Configure Use `configuration.Member(nameof(...))` to `Resolve(...)` a member the engine cannot invoke blindly (it needs arguments or the active view), and `configuration.Extension(name)` to `Register(...)` a synthetic member, `.Map(...)` a display name, or mark it `.NotSupported()`. Build multi-value results with `Variants.Value(...)`, `Variants.Values<T>(capacity).Add(...).Consume()`, or `Variants.Empty<T>()`. Gate a version-specific member for the Revit version it targets. ```csharp public void Configure(IMemberConfigurator configuration) { configuration.Member(nameof(Wall.Dispose)).Disable(); configuration.Member("BoundingBox").Resolve(() => Variants.Values<BoundingBoxXYZ>(2) .Add(_wall.get_BoundingBox(null), "Model") .Add(_wall.get_BoundingBox(RevitContext.ActiveView), "Active view") .Consume()); configuration.Extension(nameof(JoinGeometryUtils.GetJoinedElements)).Register(() => JoinGeometryUtils.GetJoinedElements(_wall.Document, _wall)); } ``` ### Step 4: Call Revit only on the Revit thread A context-menu command that reads or mutates the model raises an `[ExternalEvent(AllowDirectInvocation = true)]`; never call the Revit API from the UI thread or through `Dispatcher.Invoke`. Log on a failure path through a source-generated `[LoggerMessage]`. ```csharp public void RegisterMenu(ContextMenu contextMenu, IServiceProvider serviceProvider) { contextMenu.AddMenuItem("SelectMenuItem") .SetCommand(_wall, wall => SelectElementEvent.Raise(wall)) .SetShortcut(Key.F6); } [ExternalEvent(AllowDirectInvocation = true)] private static void SelectElement(UIApplication application, Element element) { if (application.ActiveUIDocument is null) return; application.ActiveUIDocument.Selection.SetElementIds([element.Id]); } ``` ### Step 5: Register the descriptor in DescriptorsMap Add a `case` to the `FindDescriptor` switch in `source/RevitLookup/Decomposition/DescriptorsMap.cs`, guarding on the exact type; it matches both the approximate (display) and exact (member-resolution) lookups. ```csharp Wall value when type is null || type == typeof(Wall) => new WallDescriptor(value), ``` Place the case in the matching category block; the default arm stays `_ => new ObjectDescriptor(obj)`. ### Step 6: Verify Add a test in `tests/RevitLookup.Tests` covering the new resolution, then run the add-in in a `Debug.R##` configuration and decompose an instance of the type inside Revit. ```shell dotnet test -c Debug.R## ``` ## Validation - [ ] The descriptor lives in `Decomposition/Descriptors/`, captures its object, and sets `Name`. - [ ] It implements only the configuration interfaces it needs; the resolution context is `Document`. - [ ] `Configure` resolves members that need arguments or the active view, disables noise, and registers synthetic extensions; version-specific members are `#if`-gated. - [ ] A menu command that touches Revit raises an `[ExternalEvent]` method, not a direct API call on the UI thread; failures log through a source-generated `[LoggerMessage]`. - [ ] The type is registered with a single `case` in `DescriptorsMap.FindDescriptor`, guarded for the exact type. - [ ] A test covers the change and it decomposes correctly in a `Debug.RNN` run. ## Common Pitfalls | Pitfall | Correct approach | |-------------------------------------------------------------------|-------------------------------------------------------------------------------------------| | Special-casing a type outside a descriptor | Add a descriptor and one `DescriptorsMap` case; keep type knowledge in the descriptor. | | Letting the engine invoke a member that needs arguments or a view | `Resolve` it explicitly with `Variants`, or `Disable` it. | | Calling the Revit API from a menu handler on the UI thread | Raise a static `[ExternalEvent]` method; never `Dispatcher.Invoke` for Revit work. | | Registering the descriptor case without the exact-type guard | Guard with `type is null \|\| type == typeof(T)`; member resolution needs the exact type. | | Silent exception handling | Declare a source-generated `[LoggerMessage]` method. |
View on GitHub