# Nodes verwalten

> Anleitung: Nodes im Adressraum des Servers erstellen, ihre Sichtbarkeit steuern und sie zur Laufzeit aktualisieren.

**Beispielanwendungen:**

- Server ([C#](https://github.com/Traeger-GmbH/opcuanet-samples/tree/master/cs/Basic/Server), [VB.NET](https://github.com/Traeger-GmbH/opcuanet-samples/tree/master/vb/Basic/Server)): Stellt mit einem eigenen Node-Manager eine Maschine mit einigen Nodes bereit.
- Server.DynamicNodes ([C#](https://github.com/Traeger-GmbH/opcuanet-samples/tree/master/cs/Scenarios/Server.DynamicNodes), [VB.NET](https://github.com/Traeger-GmbH/opcuanet-samples/tree/master/vb/Scenarios/Server.DynamicNodes)): Legt Nodes zur Laufzeit an, gesteuert über Pfade, die an der Konsole eingegeben werden.
- Server.DynamicNamespaces ([C#](https://github.com/Traeger-GmbH/opcuanet-samples/tree/master/cs/Scenarios/Server.DynamicNamespaces), [VB.NET](https://github.com/Traeger-GmbH/opcuanet-samples/tree/master/vb/Scenarios/Server.DynamicNamespaces)): Stellt jeden Namespace über einen eigenen Node-Manager bereit und startet den Server mit einer wechselnden Auswahl davon neu.
- Server.Localization ([C#](https://github.com/Traeger-GmbH/opcuanet-samples/tree/master/cs/Scenarios/Server.Localization), [VB.NET](https://github.com/Traeger-GmbH/opcuanet-samples/tree/master/vb/Scenarios/Server.Localization)): Stellt Anzeigenamen und Beschreibungen in mehreren Sprachen bereit, über die Globalisierungsressourcen des Servers.

## Nodes anlegen

Beteiligte Typen: `OpcServer`, `OpcNode`, `OpcNodeId`, `OpcNodeManager`, `OpcAddressSpace`, `OpcFolderNode`, `OpcObjectNode`, `OpcVariableNode`, `OpcDataVariableNode`, `OpcMethodNode`, `OpcFileNode`

Ein `OpcNode` definiert einen Datenpunkt des Servers. Das kann ein logischer Ordner (`OpcFolderNode`), eine Variable (`OpcVariableNode`), eine Methode (`OpcMethodNode`), eine Datei (`OpcFileNode`) und vieles mehr sein. Eine `OpcNodeId` identifiziert einen `OpcNode` eindeutig. Sie besteht aus einem Wert, der eigentlichen ID, und dem Index des Namespaces, dem der Node zugeordnet ist. Der Wert ist zum Beispiel ein Text oder eine Zahl. Eine URI (= Uniform Resource Identifier) legt den Namespace fest. Die Node-Manager des Servers bestimmen, welche Namespaces verfügbar sind.

Jeder Node-Manager definiert mindestens einen Namespace. Über diese Namespaces ordnet der Server die Nodes eines Node-Managers ein. So lässt sich jeder Node einem bestimmten Node-Manager zuordnen. Der Standard-Namespace (engl. Default Namespace) eines Node-Managers gilt immer dann, wenn ein Node keinen anderen Namespace erhält. Die Nodes, die ein Node-Manager definiert, heißen auch *Nodes im Adressraum (engl. Address Space) des Node-Managers*.

Beim Start fordert der Server seine Node-Manager auf, ihren Adressraum zu erstellen, also ihre statischen Nodes. Während der Ausführung des Servers lassen sich weitere, dynamische Nodes zum Adressraum eines Node-Managers hinzufügen und wieder daraus entfernen. Einen stets statischen Adressraum können Sie auch ohne eigenen Node-Manager erzeugen. Dazu übergeben Sie dem Server die statischen Nodes für den Namespace `http://{host}/{path}/nodes/` direkt. Der Port ist dabei nicht Teil des Namespaces. Statt der Nodes des statischen Adressraums können Sie auch benutzerdefinierte Node-Manager definieren.

**Einen benutzerdefinierten Adressraum mit einem Root-Node für den Standard-Namespace `http://{host}/{path}/nodes/` erstellen**:

```csharp
var machineNode = new OpcFolderNode("Machine");
var machineIsRunningNode = new OpcDataVariableNode<bool>(machineNode, "IsRunning");
 
// Note: An enumerable of nodes can be also passed.
var server = new OpcServer("opc.tcp://localhost:4840/", machineNode);
```

```vbnet
Dim machineNode As New OpcFolderNode("Machine")
Dim machineIsRunningNode = New OpcDataVariableNode(Of Boolean)(machineNode, "IsRunning")
 
'Note: An enumerable of nodes can be also passed.
Dim server = New OpcServer("opc.tcp://localhost:4840/", machineNode)
```

**Einen Adressraum über eine Methode erstellen**: Alternativ übergeben Sie dem Server eine Methode, die den Adressraum beim Start erstellt. Sie erhält eine Instanz von `OpcAddressSpace`. Über deren Methode `AddNode` trägt sie die Nodes unterhalb des ObjectsFolder oder eines anderen Eltern-Nodes ein:

```csharp
var server = new OpcServer("opc.tcp://localhost:4840/", addressSpace => {
    var machineNode = new OpcFolderNode("Machine");
    var isRunningNode = new OpcDataVariableNode<bool>(machineNode, "IsRunning");
 
    // Adds the node beneath the Objects folder (the root of all server nodes).
    addressSpace.AddNode(machineNode);
});
```

```vbnet
Dim server = New OpcServer("opc.tcp://localhost:4840/", Sub(addressSpace)
    Dim machineNode = New OpcFolderNode("Machine")
    Dim isRunningNode = New OpcDataVariableNode(Of Boolean)(machineNode, "IsRunning")

    'Adds the node beneath the Objects folder (the root of all server nodes).
    addressSpace.AddNode(machineNode)
End Sub)
```

**Einen Adressraum aus einer Konfiguration aufbauen**: Weil der Server die Methode beim Start ausführt, kann sie die Nodes auch aus einer Konfiguration erzeugen, zum Beispiel aus einer JSON-Datei, einer CSV-Datei oder einer Datenbank. Eine eigene Node-Manager-Klasse ist dafür nicht notwendig. Das folgende Beispiel legt für jeden Eintrag einer Liste einen Variablen-Node des passenden Typs an:

```csharp
// Name and data type of each variable, e.g. read from a configuration file.
var tags = new Dictionary<string, string> {
    { "Temperature", "double" },
    { "IsRunning", "bool" },
    { "JobName", "string" }
};
 
var server = new OpcServer("opc.tcp://localhost:4840/", addressSpace => {
    var machine = new OpcFolderNode("Machine");
 
    foreach (var tag in tags) {
        switch (tag.Value) {
            case "double":
                new OpcDataVariableNode<double>(machine, tag.Key);
                break;
            case "bool":
                new OpcDataVariableNode<bool>(machine, tag.Key);
                break;
            default:
                new OpcDataVariableNode<string>(machine, tag.Key);
                break;
        }
    }
 
    addressSpace.AddNode(machine);
});
```

```vbnet
'Name and data type of each variable, e.g. read from a configuration file.
Dim tags = New Dictionary(Of String, String) From { _
    {"Temperature", "double"}, _
    {"IsRunning", "bool"}, _
    {"JobName", "string"} _
}

Dim server = New OpcServer("opc.tcp://localhost:4840/", Sub(addressSpace)
    Dim machine = New OpcFolderNode("Machine")

    For Each tag In tags
        Select Case tag.Value
            Case "double"
                Dim node = New OpcDataVariableNode(Of Double)(machine, tag.Key)
            Case "bool"
                Dim node = New OpcDataVariableNode(Of Boolean)(machine, tag.Key)
            Case Else
                Dim node = New OpcDataVariableNode(Of String)(machine, tag.Key)
        End Select
    Next

    addressSpace.AddNode(machine)
End Sub)

```

Wenn auch die Datentypen erst aus der Konfiguration hervorgehen sollen, beschreibt die Seite [Datentypen zur Laufzeit](https://www.traeger.de/docs/development/opcua/server/dynamic-data-types) das Vorgehen.

**Einen benutzerdefinierten Node-Manager definieren**:

```csharp
public class MyNodeManager : OpcNodeManager
{
    public MyNodeManager()
        : base("http://mynamespace/")
    {
    }
}

```

```vbnet
Public Class MyNodeManager
    Inherits OpcNodeManager

    Public Sub New()
        MyBase.New("http://mynamespace/")
    End Sub
End Class

```

**Einen benutzerdefinierten Adressraum mit einem Root-Node über einen benutzerdefinierten Node-Manager erstellen**:

```csharp
protected override IEnumerable<IOpcNode> CreateNodes(OpcNodeReferenceCollection references)
{
    // Define custom root node.
    var machineNode = new OpcFolderNode(new OpcName("Machine", this.DefaultNamespaceIndex));
 
    // Add custom root node to the Objects-Folder (the root of all server nodes):
    references.Add(machineNode, OpcObjectTypes.ObjectsFolder);
 
    // Add custom sub node beneath of the custom root node:
    var isMachineRunningNode = new OpcDataVariableNode<bool>(machineNode, "IsRunning");
 
    // Return each custom root node using yield return.
    yield return machineNode;
}
```

```vbnet
Protected Overrides Iterator Function CreateNodes(ByVal references As OpcNodeReferenceCollection) As IEnumerable(Of IOpcNode)
    'Define custom root node.
    Dim machineNode = New OpcFolderNode(New OpcName("Machine", Me.DefaultNamespaceIndex))

    'Add custom root node to the Objects-Folder (the root of all server nodes):
    references.Add(machineNode, OpcObjectTypes.ObjectsFolder)

    'Add custom sub node beneath of the custom root node:
    Dim isMachineRunningNode = New OpcDataVariableNode(Of Boolean)(machineNode, "IsRunning")

    'Return each custom root node using yield return.
    Yield machineNode
End Function

```

**Einen benutzerdefinierten Node-Manager dem Server bekannt machen**:

```csharp
// Note: An enumerable of node managers can be also passed.
var server = new OpcServer("opc.tcp://localhost:4840/", new MyNodeManager());
```

```vbnet
'Note: An enumerable of node managers can be also passed.
Dim server = New OpcServer("opc.tcp://localhost:4840/", New MyNodeManager())
```

**Nodes zur Laufzeit hinzufügen und entfernen**: Nach dem Start des Servers fügen Sie weitere Nodes über die Methode `AddNode` des Node-Managers hinzu und entfernen sie über die Methode `RemoveNode` wieder. Der Node-Manager trägt einen hinzugefügten Node unterhalb seines Eltern-Nodes ein. Der Eltern-Node muss bereits Teil des Adressraums sein:

```csharp
// The parent node is already part of the address space of the node manager.
var jobNode = new OpcDataVariableNode<string>(machineNode, "Job", value: "JOB0815");
nodeManager.AddNode(jobNode);
 
// Removes the node from the address space.
nodeManager.RemoveNode(jobNode);
```

```vbnet
'The parent node is already part of the address space of the node manager.
Dim jobNode = New OpcDataVariableNode(Of String)(machineNode, "Job", value:="JOB0815")
nodeManager.AddNode(jobNode)

'Removes the node from the address space.
nodeManager.RemoveNode(jobNode)
```

## Sichtbarkeit von Nodes einschränken

Beteiligte Typen: `OpcServer`, `IOpcNode`, `OpcNodeId`, `OpcNodeManager`, `OpcObjectNode`, `OpcDataVariableNode`

Nicht immer sollen alle Nodes für alle Benutzer des Servers sichtbar sein. Im eigenen Node-Manager können Sie die Sichtbarkeit der Nodes nach beliebigen Kriterien einschränken. Häufig reicht auch die Einschränkung über die Benutzeridentität und einen bestimmten Node. Der folgende einfache Baum veranschaulicht das:

```csharp
protected override IEnumerable<IOpcNode> CreateNodes(OpcNodeReferenceCollection references)
{
    var machine = new OpcObjectNode(
            "Machine",
            new OpcDataVariableNode<int>("Speed", value: 123),
            new OpcDataVariableNode<string>("Job", value: "JOB0815"));

    references.Add(machine, OpcObjectTypes.ObjectsFolder);
    yield return machine;
}

```

```vbnet
Protected Overrides Iterator Function CreateNodes(ByVal references As OpcNodeReferenceCollection) As IEnumerable(Of IOpcNode)
    Dim machine = New OpcObjectNode( _
            "Machine", _
            New OpcDataVariableNode(Of Integer)("Speed", value:=123), _
            New OpcDataVariableNode(Of String)("Job", value:="JOB0815"))

    references.Add(machine, OpcObjectTypes.ObjectsFolder)
    Yield machine
End Function

```

Wenn ein Benutzer auf alle Nodes außer dem Node „Speed“ zugreifen darf, kann die Implementierung der Methode `IsNodeAccessible` so aussehen. Rufen Sie für alle übrigen Nodes die Basisimplementierung auf. Sie blendet unter anderem Variablen aus, deren AccessLevel beziehungsweise UserAccessLevel den Wert `None` hat:

```csharp
protected override bool IsNodeAccessible(OpcContext context, OpcNodeId viewId, IOpcNodeInfo node)
{
    if (context.Identity.DisplayName == "a")
        return true;

    if (context.Identity.DisplayName == "b" && node.Name.Value == "Speed")
        return false;

    return base.IsNodeAccessible(context, viewId, node);
}

```

```vbnet
Protected Overrides Function IsNodeAccessible(ByVal context As OpcContext, ByVal viewId As OpcNodeId, ByVal node As IOpcNodeInfo) As Boolean
    If context.Identity.DisplayName = "a" Then
        Return True
    End If
 
    If context.Identity.DisplayName = "b" AndAlso node.Name.Value = "Speed" Then
        Return False
    End If
 
    Return MyBase.IsNodeAccessible(context, viewId, node)
End Function
```

## Nodes aktualisieren

Beteiligte Typen: `OpcDataVariableNode`, `OpcStatusCode`, `OpcContext`

Während der Ausführung des Servers müssen Sie häufig Nodes passend zum zugrunde liegenden System aktualisieren. Einfache Objekt-, Ordner- oder Methoden-Nodes ändern sich eher selten, Variablen-Nodes dagegen regelmäßig. Je nachdem, welche Informationen vorliegen, hinterlegen oder aktualisieren Sie im Variablen-Node neben dem eigentlichen Wert auch einen Zeitstempel und die Güte des Wertes. Das folgende Beispiel skizziert die Handhabung:

```csharp
speedNode.Status.Update(OpcStatusCode.Good);
speedNode.Timestamp = DateTime.UtcNow;
speedNode.Value = 1200;
 
// Notifies subscribed clients about the changes.
speedNode.ApplyChanges(server.SystemContext);
```

```vbnet
speedNode.Status.Update(OpcStatusCode.Good)
speedNode.Timestamp = DateTime.UtcNow
speedNode.Value = 1200

'Notifies subscribed clients about the changes.
speedNode.ApplyChanges(server.SystemContext)
```

Erst mit dem Aufruf von `ApplyChanges` informiert der Server auch Clientanwendungen über die Änderung des Nodes, sofern diese eine aktive Subscription für den Node und das Value-Attribut eingerichtet haben. Als Kontext dient außerhalb eines Node-Managers die Eigenschaft `SystemContext` des Servers, innerhalb eines Node-Managers dessen eigene Eigenschaft `SystemContext`. Die Überladung `ApplyChanges(context, recursive: true)` übernimmt zudem die Änderungen aller Kind-Nodes.

## Namespaces festlegen

Beteiligte Typen: `OpcServer`, `OpcNamespace`

Die Indizes der Namespaces vergibt der Server beim Start in der Reihenfolge, in der seine Node-Manager ihre Namespaces anmelden. Clients, die Nodes über einen festen NamespaceIndex ansprechen (zum Beispiel konfigurierte SCADA- oder Steuerungs-Clients), sind deshalb auf eine feste Reihenfolge angewiesen. Mit `RegisterNamespace` beziehungsweise `RegisterNamespaces` tragen Sie die eigenen Namespaces vor dem Start in einer festen Reihenfolge ein. Einen bereits eingetragenen Namespace fügt der Server nicht erneut hinzu. Die Methoden liefern Instanzen von `OpcNamespace`, deren Eigenschaft `Index` den vergebenen Index enthält:

```csharp
using (var server = new OpcServer("opc.tcp://localhost:4840/")) {
    OpcNamespace[] namespaces = server.RegisterNamespaces(
            "http://example.com/machine",
            "http://example.com/plant");
 
    Console.WriteLine("Machine: ns={0}", namespaces[0].Index);
    Console.WriteLine("Plant: ns={0}", namespaces[1].Index);
 
    server.Start();
}
```

```vbnet
Using server = New OpcServer("opc.tcp://localhost:4840/")
    Dim namespaces As OpcNamespace() = server.RegisterNamespaces( _
            "http://example.com/machine", _
            "http://example.com/plant")

    Console.WriteLine("Machine: ns={0}", namespaces(0).Index)
    Console.WriteLine("Plant: ns={0}", namespaces(1).Index)

    server.Start()
End Using
```

Standardmäßig liegen alle Nodes im benutzerdefinierten Namespace mit dem Index zwei. Der Namespace mit dem Index eins ist der Namespace der Serveranwendung: Das SDK trägt dort die Eigenschaft `OpcServer.ApplicationUri` ein, sofern Sie vor dem Start des Servers keine Namespaces registrieren. Damit Sie Nodes im Namespace mit dem Index eins verwalten können, muss Ihr eigener `OpcNodeManager` den Wert von `OpcServer.ApplicationUri` als NamespaceUri verwenden.

## Mehrsprachige Texte

Beteiligte Typen: `OpcServer`, `OpcServerGlobalization`, `OpcText`

Anzeigenamen, Beschreibungen und andere Texte eines Nodes (`OpcText`) lassen sich in mehreren Sprachen bereitstellen, ohne dass für jede Sprache eigene Nodes entstehen. Dazu erhält der Text neben dem Standardtext und dessen Sprache einen Schlüssel. Über die Eigenschaft `Globalization` des Servers hinterlegen Sie zu jedem Schlüssel die Übersetzungen je Sprache (`AddResource` für einen Eintrag, `AddResources` für mehrere). Geben Sie als Sprache eine konkrete Kultur an (zum Beispiel `de-DE`, nicht nur `de`):

```csharp
var temperature = new OpcDataVariableNode<double>("Temperature");
 
temperature.DisplayName = new OpcText(
        "Temperature",
        cultureName: "en-US",
        key: "Temperature.DisplayName");
 
temperature.Description = new OpcText(
        "Temperature of the machine",
        cultureName: "en-US",
        key: "Temperature.Description");
 
using (var server = new OpcServer("opc.tcp://localhost:4840/", temperature)) {
    server.Globalization.AddResource(
            cultureName: "de-DE",
            key: "Temperature.DisplayName",
            value: "Temperatur");
 
    server.Globalization.AddResource(
            cultureName: "de-DE",
            key: "Temperature.Description",
            value: "Temperatur der Maschine");
 
    server.Start();
}
```

```vbnet
Dim temperature = New OpcDataVariableNode(Of Double)("Temperature")

temperature.DisplayName = New OpcText( _
        "Temperature", _
        cultureName:="en-US", _
        key:="Temperature.DisplayName")

temperature.Description = New OpcText( _
        "Temperature of the machine", _
        cultureName:="en-US", _
        key:="Temperature.Description")

Using server = New OpcServer("opc.tcp://localhost:4840/", temperature)
    server.Globalization.AddResource( _
            cultureName:="de-DE", _
            key:="Temperature.DisplayName", _
            value:="Temperatur")

    server.Globalization.AddResource( _
            cultureName:="de-DE", _
            key:="Temperature.Description", _
            value:="Temperatur der Maschine")

    server.Start()
End Using
```

Wenn ein Client einen solchen Text liest, übersetzt der Server ihn anhand der Sprachen, die der Client beim Erstellen der Sitzung bevorzugt. Im SDK legt der Client diese Sprachen über `OpcClient.Locales` fest. Das beschreibt im Client-Kapitel der Abschnitt [Anwendung und Sitzung konfigurieren](https://www.traeger.de/docs/development/opcua/client/configuration#anwendung-und-sitzung-konfigurieren). Wenn es für keine der bevorzugten Sprachen eine Übersetzung gibt, liefert der Server den Standardtext.

## Änderungen am Adressraum melden

Beteiligte Typen: `OpcAutomatism`, `OpcNode`, `OpcNodeManager`

Wenn sich der Adressraum während der Laufzeit ändert, zum Beispiel weil eine Maschine oder ein Modul hinzukommt, kann der Server Clients über Model-Change-Ereignisse (GeneralModelChangeEvent) informieren. Standardmäßig erzeugt der Server diese Ereignisse nicht. Für alle Nodes außerhalb der Namespaces der OPC Foundation schalten Sie sie über die statische Eigenschaft `OpcAutomatism.RaiseModelChangeEvents` ein:

```csharp
OpcAutomatism.RaiseModelChangeEvents = true;
```

```vbnet
OpcAutomatism.RaiseModelChangeEvents = True
```

Danach meldet der Server zum Beispiel das Hinzufügen eines Nodes zur Laufzeit an die Clients, die Ereignisse des Servers abonniert haben:

```csharp
var machine = new OpcObjectNode(machines, "Machine3");
var temperature = new OpcDataVariableNode<double>(machine, "Temperature");
 
manager.AddNode(server.SystemContext, machine);
```

```vbnet
Dim machine = New OpcObjectNode(machines, "Machine3")
Dim temperature = New OpcDataVariableNode(Of Double)(machine, "Temperature")

manager.AddNode(server.SystemContext, machine)
```

Wenn Sie viele Nodes auf einmal ändern, setzen Sie die Erfassung der Änderungen für einen Node mit `SuspendModelChangeEvents` vorübergehend aus und nehmen sie mit `ResumeModelChangeEvents` wieder auf. Mit `recursive: true` gilt das auch für alle seine Kind-Nodes:

```csharp
machines.SuspendModelChangeEvents(recursive: true);
 
foreach (var name in new string[] { "Machine4", "Machine5", "Machine6" })
    manager.AddNode(server.SystemContext, new OpcObjectNode(machines, name));
 
machines.ResumeModelChangeEvents(recursive: true);

```

```vbnet
machines.SuspendModelChangeEvents(recursive:=True)

For Each name In New String() {"Machine4", "Machine5", "Machine6"}
    manager.AddNode(server.SystemContext, New OpcObjectNode(machines, name))
Next

machines.ResumeModelChangeEvents(recursive:=True)
```
