
June 2, 2026
•
By Bas Nederveen
•
8 min read
Share this article:
Part 1 was about reading Frame Generator's data. Part 2 was about noticing when a user has edited a frame. This part is about what happens to your state when Frame Generator gets done — because a "profile change" in Inventor's UI is, under the hood, an occurrence destruction-and-replacement, and almost everything that pointed at the old occurrence is now dead.
Specifically, when a user changes a beam from an HEA200 to an HEA240:
ComponentOccurrence instances are torn down. Anything you cached against them is a dangling pointer.ReferenceKeys that resolved to the old occurrence return null from BindKeyToObject.HealthStatusEnum.kDriverLostHealth — Inventor's way of saying "this constraint is broken; the user will see a red icon in the browser until you fix it or remove it".Your addin has to repair all of that. The general strategy: tag every dependent thing you create with a GUID-keyed attribute, so that when the underlying anchor dies you can find your dependents purely by attribute search and rebuild them against the new geometry.
BindKeyToObject returns null after a profile changeInventor reference keys are persistent handles to model objects — opaque byte arrays you resolve back to a live object through the document's ReferenceKeyManager, as covered in Part 1. They survive save/load, they survive most edits, and as a result they're the obvious thing to cache when you need to remember "which beam does this dependent belong to?". One pattern is to stringify the parent beam's key with KeyToString and stash it in the dependent's own attribute set, then read it back when you need the beam again:
// Captured at placement with ReferenceKeyManager.KeyToString, the parent
// beam's reference key lives as a string in the dependent's attribute set.
var keyString = dependent.AttributeSets[DependentAttributeSetName]["BeamReferenceKey"]
.Value.ToString();
byte[] key = null;
assembly.ReferenceKeyManager.StringToKey(keyString, ref key);
That works perfectly until somebody changes the beam profile. Then the bind returns null:
// bind through the same context the key was captured in
var beam = assembly.ReferenceKeyManager.BindKeyToObject(ref key, contextId, out _)
as ComponentOccurrence;
// When a beam is changed to a 'different' section, the occurrence is new,
// so the key captured against the old occurrence no longer binds.
if (beam == null) { return; }
A null here doesn't mean the file is corrupt. It means Frame Generator destroyed the occurrence the key pointed to and made a new one. This is exactly why Part 2's event sequence has to capture dependent-to-beam mappings before OnDelete completes — once the old beam is gone, no amount of reference-key recovery will get it back.
After replacement, the addin re-derives the beam from the new occurrence (which you tracked in OnNewOccurrence) and writes a fresh BeamReferenceKey into the dependent's attribute set. The reference-key chain is rebuilt forward, not recovered backward.
kDriverLostHealth and broken constraintsAssembly constraints are even worse. A constraint references specific geometry — a face, an edge, a work-plane — on a specific component definition. When the underlying part document changes (because the beam's profile changed), the constraint can no longer resolve the geometry it was built against. Inventor's response is to set HealthStatus to kDriverLostHealth and leave the constraint sitting in the browser as a red icon.
A kDriverLostHealth constraint can be repaired — Inventor's Design Doctor walks a user through reselecting the lost geometry. What Inventor doesn't expose is an API for that rebind: the constraint's GeometryOne/GeometryTwo are read-only and there's no Redefine method. So an addin recovering automatically — with no user in the loop to drive the wizard — ends up mimicking what the Design Doctor does, in code: delete the broken constraint and recreate it against the equivalent geometry on the new definition. You only do this for constraints you created yourself, which is exactly why you tag them — so you can find your own and leave the user's untouched.
Which means: before you create a custom constraint, you need to record enough metadata on it that you can recreate it from scratch later.
// When placing a constraint, tag it with enough metadata to rebuild later:
attbSet.Add("ConstraintGeometry", ValueTypeEnum.kIntegerType, (int)constraintGeometry);
attbSet.Add("ConstraintType", ValueTypeEnum.kIntegerType, (int)constraintType);
attbSet.Add("DependentID", ValueTypeEnum.kStringType, dependentId);
ConstraintGeometry is an enum whose integer value is the index of a work-plane on the underlying part definition (if your dependent is authored against a small fixed library of named work-planes, the index is stable across regenerations). ConstraintType records flush vs mate. DependentID is a GUID stored both here and in the parent dependent's own tracking attribute set, so you can find all constraints belonging to a given dependent with one attribute-manager query.
The actual recovery routine ends up being short. After the replacer has substituted a new dependent part, it sweeps every constraint with the tagged attribute set and rebuilds any in kDriverLostHealth:
private void FixBrokenConstraints(BeamReplacement replacement, string dependentId)
{
var attbMan = assembly.AttributeManager;
ObjectsEnumerator taggedConstraints =
attbMan.FindObjects(ConstraintAttributeSetName, "ConstraintGeometry");
foreach (AssemblyConstraint constraint in taggedConstraints)
{
if (constraint.HealthStatus != HealthStatusEnum.kDriverLostHealth) continue;
var attrs = constraint.AttributeSets[ConstraintAttributeSetName];
if (attrs["DependentID"].Value.ToString() != dependentId) continue;
var geo = (ConstraintGeometry)attrs["ConstraintGeometry"].Value;
var type = (ConstraintType)attrs["ConstraintType"].Value;
var beamCompdef = (PartComponentDefinition)replacement.BeamOccurrence.Definition;
var dependentCompdef = (PartComponentDefinition)replacement.DependentOccurrence.Definition;
constraint.Delete();
var beamPlane = beamCompdef.WorkPlanes[(int)geo + 1];
replacement.BeamOccurrence.CreateGeometryProxy(beamPlane, out object beamPlaneProxy);
var dependentPlane = dependentCompdef.WorkPlanes[(int)geo + 1];
replacement.DependentOccurrence.CreateGeometryProxy(dependentPlane, out object dependentPlaneProxy);
AssemblyConstraint c = type == ConstraintType.Flush
? (AssemblyConstraint)assembly.ComponentDefinition.Constraints.AddFlushConstraint(dependentPlaneProxy, beamPlaneProxy, 0)
: (AssemblyConstraint)assembly.ComponentDefinition.Constraints.AddMateConstraint (dependentPlaneProxy, beamPlaneProxy, 0);
// Re-tag the new constraint so the next edit can find it too
var newAttrs = c.AttributeSets.Add(ConstraintAttributeSetName);
newAttrs.Add("ConstraintGeometry", ValueTypeEnum.kIntegerType, (int)geo);
newAttrs.Add("ConstraintType", ValueTypeEnum.kIntegerType, (int)type);
newAttrs.Add("DependentID", ValueTypeEnum.kStringType, dependentId);
}
}
The two key moves:
AttributeManager.FindObjects with the attribute-set name as the first argument is how you query for "every assembly object I've ever tagged with this set". This is much faster than walking AllLeafOccurrences and inspecting attributes per object. Use it whenever you have a non-trivial number of tagged objects.CreateGeometryProxy on the new occurrence's freshly-resolved work-plane is what makes the new constraint stick. The proxy is the assembly-context handle for a piece of part-definition geometry; without it you'd be passing raw work-plane references that aren't valid in the assembly's coordinate frame.The new constraint is re-tagged with the same GUID and same metadata so that the next profile change will find it just as readily. The chain is self-healing as long as it isn't broken — and the chain is just Id strings stored in attribute sets, so it's almost impossible to break.
After two years of building on this, here's the cheat-sheet I keep in my head:
| Thing | Survives a profile change? |
|---|---|
ComponentOccurrence of the replaced beam |
No — torn down, even the variable identity |
ComponentOccurrence of dependent (non-frame) parts |
Yes |
ReferenceKey to the replaced beam |
No — BindKeyToObject returns null |
| Custom attribute sets on the replaced beam | No — gone with the occurrence |
| Custom attribute sets on dependent parts | Yes |
| Assembly constraints touching the replaced beam | Sort of — survive in kDriverLostHealth, must be rebuilt |
The frame assembly's Frame.Skeletons XML |
Yes — rewritten by Frame Generator |
MonikerForCC on the frame member definition |
Changes — the whole point of the edit |
FrameMemberID on the path entry |
Yes — Frame Generator preserves frame-local IDs across profile swaps |
Design lesson: anything you want to outlive a Frame Generator edit needs to be either (a) anchored on a non-frame object, or (b) discoverable from scratch by attribute search, with enough metadata in the attributes to fully recreate the dependency. Option (b) is more plumbing but more robust — there's no way for the chain to break silently.
This three-part series is what I wish someone had written before I started building against Frame Generator. Once you understand its data model and event flow it's a pleasant thing to build on — the attribute-set layer is reasonably stable, the moniker is a real durable identifier, and the assembly-event story is consistent if not documented.
It isn't a documented contract, so treat the magic strings as load-bearing, treat kBefore/kAfter ordering as part of your design, tag everything you create with GUIDs so you can find it without object identity, detach your event handlers before mutating the assembly, and log the transaction DisplayNames in dev builds. "Change Frame" has held steady for as long as I've built against it, so this is more precaution than expectation — but if Autodesk ever does rename it, you now know exactly what it is and where it shows up.
If you're building something against Frame Generator and run into something that isn't in this series, drop me a line — I'd love to compare notes.
Previous: Part 2 — Tracking Edits · Part 1 — The Data Model
Share this article: