OPC UA Placeholder Children: Why Is FindChild Missing Them?

Patricia Callen8 min read
OPC / OPC UAOther ManufacturerTroubleshooting
Licensed PE Working through this on a live machine? A Maine-licensed engineer can take it from here — included with IMD hardware, by the hour for everything else. Book an engineer

A child node that browses with the correct TypeDefinition can still be the wrong .NET class at runtime. Two separate mechanisms decide those two outcomes, and only one of them is driven by the instance section of your ModelDesign XML. Chasing the wrong one is why ModellingRule edits appear to do nothing.

Where does a child node's runtime class actually come from?

Follow the chain. The model compiler reads the <ObjectType> declarations and emits two artifacts: the predefined nodes blob that gets loaded into the address space, and a set of partial *State classes. When the server loads the predefined nodes, NodeState.Initialize walks the serialized child list and, for every child, calls FindChild on the parent state object to obtain the container it should deserialize into.

The generated FindChild override is a switch keyed on the child's BrowseName string. Each case returns the strongly typed member the compiler created — a ThingState, a PropertyState. A BrowseName that the switch does not name falls through to the base implementation, which hands back a generic BaseInstanceState. The HasTypeDefinition reference you wrote in the instance XML is still applied, so a client browsing the server sees COMPANY:ThingType and everything looks correct. Inside the server, the object has no typed accessors, no generated method or event wiring, and nothing you can cast.

So the address-space type and the CLR type are set by different inputs. Read the generated file before you change the XML.

Check 1: does the generated FindChild override name your child?

Open the generated class file for the parent type, find the FindChild override, and look for the BrowseName. That single reading splits the whole problem.

If the case is present and instances still come up untyped, the failure is a name match, not a modelling rule: the switch compares against the BrowseName declared in the type (Thing), and your instances are named THING1 and THING2. They will never hit that case. Also confirm the namespace index on the QualifiedName — a BrowseName in the wrong namespace does not match.

If there is no case at all, the compiler was never told to generate a member. That happens when the child carries MandatoryPlaceholder or OptionalPlaceholder, and when the child appears only in the instance section and not in any type declaration. Both branches lead to the same conclusion: the code generator cannot help you, because the BrowseNames of the children are not knowable at compile time.

What each declaration produces

Declaration in the ObjectType What the compiler emits Runtime result on the instance
Mandatory, fixed BrowseName Typed member plus a matching case in FindChild Exactly one child, created automatically, typed as ThingState. Name is fixed by the type.
Optional, fixed BrowseName, not repeated in the instance Typed member; instantiation skips optional components Child does not exist. Nothing to browse, nothing to cast.
Optional, repeated in the instance tree Typed member; instantiation creates the declared node Child exists and is typed — one per BrowseName you replicated, per instance.
MandatoryPlaceholder / OptionalPlaceholder A placeholder node in the information model. No typed member, no FindChild case. Anything you add is a BaseInstanceState unless you construct it as ThingState in code.
Not declared in the type; only in the instance XML with HasTypeDefinition Nothing FindChild returns null, generic wrapper is built. Browse shows the right TypeDefinition; the server-side object is untyped.

Check 2: is the child count fixed when you ship the model?

This is the branch that decides everything downstream, and it is a question about your product, not about the toolchain.

Mandatory and Optional bind a single child with a BrowseName written into the type. They are the right rules when the model author knows the name and the count — one Configuration folder, one Status property. They give you generated members, a FindChild case, and zero hand-written code.

If customers supply the instances, you do not know the count or the display names at build time, and no combination of Mandatory and Optional expresses "one or more of these." That is what MandatoryPlaceholder and OptionalPlaceholder are for. They produce a correct information model — a client reading the type learns that an arbitrary number of ThingType children may appear — and they produce no generator support whatsoever.

Check 3: for Optional children, did you replicate the instance tree?

Instantiating a type creates only its mandatory components. An optional component is a permission, not an instruction; if the tool created optional children automatically, the designation would carry no meaning. To make one appear, replicate the tree in the instance section down to each optional component you want.

The replication is cheap. Only the BrowseName and the ReferenceTypes are needed — DisplayName, Description, TypeDefinition and every other attribute are copied from the type definition:

<Object>
  <BrowseName>THINGCATEGORY1</BrowseName>
  <DisplayName>Thing Category 1</DisplayName>
  <Children>
    <Object>
      <BrowseName>Thing</BrowseName>
    </Object>
  </Children>
  <References>
    <Reference>
      <ReferenceType>ua:HasTypeDefinition</ReferenceType>
      <TargetId>COMPANY:ThingCategoryType</TargetId>
    </Reference>
  </References>
</Object>

Note the BrowseName of the replicated child: it must be the one declared in the type. This has to be repeated for every instance where the optional child should appear. If you find yourself writing that block with a different BrowseName each time, you are outside what Optional can do — go back to Check 2.

Why not one type per child count?

Generating ThingCategory1Type with one child, ThingCategory2Type with two, and so on is mechanically possible when the ModelDesign is produced programmatically, and it is the wrong shape. Cardinality becomes part of type identity, so every customer count spawns a type and every model revision churns the namespace. Clients lose the ability to write one handler against ThingCategoryType. The generated code grows with the largest customer.

It also does not solve the original problem. The BrowseNames in those types are still fixed — Thing1, Thing2 — so customer instances have to be renamed to match the switch, and the customer's own naming disappears from the address space. The modelling problem moves; it does not go away.

The resolving branch: placeholder in the type, instances in code

Declare the placeholder, then build the children yourself. The generated code stops at the category; you take over below it.

  1. Declare the child with a placeholder rule in the type. Keep the full definition of ThingType itself — its members stay Mandatory so its generated state class does the real work:
    <ObjectType SymbolicName="COMPANY:ThingCategoryType" BaseType="ua:FolderType">
      <BrowseName>Thing Category</BrowseName>
      <Children>
        <Object SymbolicName="COMPANY:Thing"
                ModellingRule="MandatoryPlaceholder"
                TypeDefinition="COMPANY:ThingType"
                SupportsEvents="true">
          <BrowseName>Thing</BrowseName>
        </Object>
      </Children>
    </ObjectType>
  2. Remove the hard-coded child objects from the instance section. They cannot resolve to a typed class and they will only produce BaseInstanceState nodes.
  3. Put hand-written members in a separate file. The generated classes are partial, so anything you add survives regeneration.
  4. Construct each child as its generated type, assign any optional members before Create so they receive NodeIds, then create it in your namespace:
    ThingState thing = new ThingState(category);
    thing.OptionalProperty = new PropertyState(thing);   // only if you need it
    
    string name = Utils.Format("Thing #{0}", unitNumber);
    thing.Create(
        context,
        null,
        new QualifiedName(name, m_namespaceIndex),
        new LocalizedText(displayName),
        true);
    
    category.AddChild(thing);
    The same pattern applies one level up if the category itself is created at runtime.
  5. Set the reference type from the parent explicitly. The base type here is FolderType, so decide whether children hang off Organizes or HasComponent and set ReferenceTypeId on the child to match what the placeholder declares. Do not rely on a default.
  6. Set EventNotifier yourself on code-built nodes. SupportsEvents="true" configures nodes the compiler creates; nodes you construct get exactly the attributes you assign.
  7. Hand the finished subtree to the node manager so the NodeIds are registered and the nodes are exposed — AddPredefinedNode in the standard node manager base, or the equivalent entry point on whichever base class you derived from.

How do you verify the children came up typed?

  1. Browse the category with any UA client. Each child must show HasTypeDefinition to COMPANY:ThingType, and the placeholder declaration must be visible on ThingCategoryType with its ModellingRule reference — that is what tells a client to expect an arbitrary number of children.
  2. In the server, retrieve a child and cast it to ThingState. A non-null cast is the only proof that the object is the generated class rather than a BaseInstanceState that merely browses correctly.
  3. Read one of ThingType's own mandatory members through the typed reference. If the member is present and readable, the type's generated wiring is intact below the placeholder.
  4. Confirm BrowseNames are unique within the category and carry your namespace index, then re-browse after a server restart to prove the nodes are built by your code path and not by a stale predefined-nodes blob.
  5. If events are in scope, subscribe on the category and fire one from a child to confirm the notifier chain you wired by hand.

Stop debugging your model when the readings contradict the toolchain rather than your XML: a Mandatory child that still initializes as BaseInstanceState after a clean regeneration, or a valid placeholder declaration the compiler rejects outright. At that point capture the ModelDesign fragment, the generated FindChild override, and the compiler version, and raise it through the official OPC Foundation support channels for the model compiler and the .NET stack.

FAQ

What happens if I use OptionalPlaceholder instead of Mandatory?

The compiler emits a placeholder node in the information model but no typed member and no case in the generated FindChild override. Every instance has to be constructed in code as its generated state class and attached with AddChild; put that hand-written code in a separate file, since the generated classes are partial.

What happens if I declare a child only in the instance XML with a HasTypeDefinition reference?

NodeState.Initialize calls FindChild, finds no matching case, and builds a generic BaseInstanceState. Clients browse the correct TypeDefinition, but the server-side object has no typed accessors and none of the generated method or event wiring.

What happens if I mark a child Optional but do not repeat it in the instance tree?

It is not created — instantiation builds only the mandatory components. Replicate the tree down to that child in every instance where it should appear, supplying just the BrowseName and the ReferenceTypes; all remaining attributes are copied from the TypeDefinition.

Back to blog