.NET Based OPC UA Client/Server SDK  3.3.3.537
Collections and dynamic object creation

So far, all examples have dealt with static NodeSets that were previously created with UaModeler. But in some cases, objects or variables need to be created dynamically, in particular for collections. That can be order lists or like in this example the list of current jobs. In OPC UA, those lists may be of the type OrderedListType. The SDK does not provide a default implementation for the OrderedListModel. Fortunately, it is not difficult to write it by hand.

Those collection types must implement at least the INotifyCollectionChanged interface, but it is advisable to also implement the IEnumerable interface. Than a model can have a non-empty collection on model activation.

public partial class ProductionPlanModel : INotifyCollectionChanged, IEnumerable<ProductionJobModel>
{
public event NotifyCollectionChangedEventHandler CollectionChanged;

When now a new job is added to the production plan, it will be stored in the internal job list and a CollectionChanged event is raised. This event will be observed by the BindModel mechanism, the nodes for this object will be created and added to the parent node. Finally, an OPC UA GeneralModelChangeEventType event will be emitted to inform the clients that the model has changed.

public void Add(ProductionJobModel item)
{
m_jobs.Add(item);
Version++;
CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item));
item.NumberInList = ushort.MaxValue;
item.State.SwitchToState(ProductionStateMachineModel.State.Initializing);
ReorderList();
}

The precise settings for the creation are not coming from the model but from the delegate passed to BindModel. There, the reference type, the browse name and optional browse paths can be defined. The node manager will set the proper ParentNodeId and TypeDefinitionId. The delegate has a return type of CreateInstanceSettings, however, only settings of the subtype CreateObjectSettings or CreateVariableSettings are support.

private static CreateInstanceSettings CreateInstanceSettingsFor(object obj)
{
switch (obj)
{
case ProcessingJobModel job:
return new CreateObjectSettings()
{
ReferenceTypeId = UaBase.ReferenceTypeIds.HasOrderedComponent,
BrowseName = (new AbsoluteName(job.Identifier, Namespaces.MachineDemoServer)).ToQualifiedName(Server.NamespaceUris),
OptionalBrowsePaths = new string[]
{
new QualifiedName(OpcUa.Glass.BrowseNames.Name, (ushort)Server.NamespaceUris.IndexOf(OpcUa.Glass.Namespaces.Glass)).ToString(),
new QualifiedName(OpcUa.Glass.BrowseNames.QueueJob, (ushort)Server.NamespaceUris.IndexOf(OpcUa.Glass.Namespaces.Glass)).ToString(),
new QualifiedName(OpcUa.Glass.BrowseNames.ReleaseJob, (ushort)Server.NamespaceUris.IndexOf(OpcUa.Glass.Namespaces.Glass)).ToString(),
}
};
default:
return default;
}
}

In this example we do not specify RequestedNodeId, hence the default mechanism will be used, that is a Guid-based NodeId will be generated. If a specific NodeId is desired, the RequestedNodeId can be set or the method CreateNodeId of the BaseNodeManager can be overloaded.

There is a second way to inform the client about model changes and a server must mandatorily support both variants. The server needs to maintain a NodeVersion property that changes, whenever an object was added or removed. Here the internal Version property is copied onto the NodeVersion property, whenever the Version changes.

public ProductionPlanModel(ProductionPlanModel template) : this(template, default(DummyArgument))
{
PropertyChanged += (o, e) =>
{
switch (e.PropertyName)
{
case nameof(Version):
NodeVersion = Version.ToString();
break;
}
};
Version++;
}

Per default, the default values stored in NodeSet are used as the start values of the model. So it is necessary to prevent BindModel to override the NodeVersion property on model activation. This can be achieved with a static ModelAnnotator. When the model gets bind to the nodeset, the node manager will first look for a static property named ModelAnnotator of the type ModelAnnotator. If it finds one, it will respect the override settings there.

public static ModelAnnotator ModelAnnotator
{
get
{
var annotator = new ModelAnnotator();
annotator.Instance(nameof(NodeVersion))
.HasInitMode(InitMode.FromModelToNode);
return annotator;
}
}