# Werte lesen und schreiben

> Anleitung: Werte von Nodes im Server bereitstellen und Lese- sowie Schreibzugriffe von Clients behandeln.

**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.
- Client ([C#](https://github.com/Traeger-GmbH/opcuanet-samples/tree/master/cs/Basic/Client), [VB.NET](https://github.com/Traeger-GmbH/opcuanet-samples/tree/master/vb/Basic/Client)): Verbindet sich mit einem Server, liest einen Node, schreibt einen neuen Wert und liest ihn erneut.

## Werte lesen

Beteiligte Typen: `OpcServer`, `OpcVariableNode`, `OpcDataVariableNode`, `OpcReadAttributeValueCallback`, `OpcAttributeValue`, `OpcReadAttributeValueContext`, `OpcReadVariableValueCallback`, `OpcReadVariableValueContext`, `OpcVariableValue`, `OpcText`

Ein `OpcNode` definiert seine Metadaten durch Attribute. Attribute wie Name, DisplayName oder Description stellen im Allgemeinen alle Nodes bereit, das Value-Attribut dagegen nur Variablen-Nodes. Die Werte der Attribute speichern die Node-Instanzen standardmäßig intern. Wenn der Wert aus einer anderen Datenquelle stammen soll, definieren Sie Callback-Methoden, die die Werte bereitstellen. Die Signatur der ReadVariableValue-Callback-Methode unterscheidet sich dabei von den anderen ReadAttributeValue-Callback-Methoden. Für das Value-Attribut erwartet das SDK statt einer Instanz von `OpcAttributeValue` eine Instanz von `OpcVariableValue`. Sie besteht neben dem eigentlichen Wert aus einem Zeitstempel, zu dem die Quelle den Wert festgestellt hat (`SourceTimestamp`), und aus Statusinformationen über die Qualität des Wertes. Der Server ruft die Read-Callbacks bei jedem Lesevorgang auf, mit dem ein Client die Metadaten über den Read-Service liest. Beim Browsen liefert der Server dagegen die intern gespeicherten Werte (zum Beispiel den DisplayName), ohne die Callbacks aufzurufen. Für das Description-Attribut ist der Wert vom Typ `OpcText`.

**Den Initialwert des Value-Attributs eines Variablen-Nodes festlegen**:

```csharp
var machineIsRunningNode = new OpcDataVariableNode<bool>("IsRunning", false);
```

```vbnet
Dim machineIsRunningNode = New OpcDataVariableNode(Of Boolean)("IsRunning", False)
```

**Den Wert des Value-Attributs eines Variablen-Nodes festlegen**:

```csharp
machineIsRunningNode.Value = true;
```

```vbnet
machineIsRunningNode.Value = True
```

**Den Wert des Description-Attributs festlegen**:

```csharp
machineIsRunningNode.Description = "My description";
```

```vbnet
machineIsRunningNode.Description = "My description"
```

**Alle Clients mit aktiver Subscription über die Attributänderungen informieren und Änderungen übernehmen**:

```csharp
machineIsRunningNode.ApplyChanges(server.SystemContext);
```

```vbnet
machineIsRunningNode.ApplyChanges(server.SystemContext)
```

**Den Wert des Description-Attributs aus einer anderen Datenquelle als der internen ermitteln**:

```csharp
machineIsRunningNode.ReadDescriptionCallback = HandleReadDescription;
...
private OpcAttributeValue<OpcText> HandleReadDescription(
        OpcReadAttributeValueContext context,
        OpcAttributeValue<OpcText> value)
{
    return ReadDescriptionFromDataSource(context.Node) ?? value;
}
```

```vbnet
machineIsRunningNode.ReadDescriptionCallback = AddressOf HandleReadDescription
...
Private Function HandleReadDescription( _
        ByVal context As OpcReadAttributeValueContext, _
        ByVal value As OpcAttributeValue(Of OpcText)) As OpcAttributeValue(Of OpcText)
    Return If(ReadDescriptionFromDataSource(context.Node), value)
End Function

```

**Den Wert des Value-Attributs eines Variablen-Nodes aus einer anderen Datenquelle als der internen ermitteln**:

```csharp
machineIsRunningNode.ReadVariableValueCallback = HandleReadVariableValue;
...
private OpcVariableValue<object> HandleReadVariableValue(
        OpcReadVariableValueContext context,
        OpcVariableValue<object> value)
{
    return ReadValueFromDataSource(context.Node) ?? value;
}

```

```vbnet
machineIsRunningNode.ReadVariableValueCallback = AddressOf HandleReadVariableValue
...
Private Function HandleReadVariableValue( _
        ByVal context As OpcReadVariableValueContext, _
        ByVal value As OpcVariableValue(Of Object)) As OpcVariableValue(Of Object)
    Return If(ReadValueFromDataSource(context.Node), value)
End Function

```

## Werte schreiben

Beteiligte Typen: `OpcServer`, `OpcVariableNode`, `OpcDataVariableNode`, `OpcWriteAttributeValueCallback`, `OpcAttributeValue`, `OpcWriteAttributeValueContext`, `OpcWriteVariableValueCallback`, `OpcWriteVariableValueContext`, `OpcVariableValue`, `OpcText`

Ein `OpcNode` definiert seine Metadaten durch Attribute. Attribute wie Name, DisplayName oder Description stellen im Allgemeinen alle Nodes bereit, das Value-Attribut dagegen nur Variablen-Nodes. Die Werte der Attribute speichern die Node-Instanzen standardmäßig intern. Wenn der Wert in einer anderen Datenquelle gespeichert werden soll, definieren Sie Callback-Methoden, die die Werte speichern. Die Signatur der WriteVariableValue-Callback-Methode unterscheidet sich dabei von den anderen WriteAttributeValue-Callback-Methoden. Für das Value-Attribut verwendet das SDK statt einer Instanz von `OpcAttributeValue` eine Instanz von `OpcVariableValue`. Sie besteht neben dem eigentlichen Wert aus einem Zeitstempel, zu dem die Quelle den Wert festgestellt hat (`SourceTimestamp`), und aus Statusinformationen über die Qualität des Wertes. Der Server ruft die Write-Callbacks bei jedem Schreibvorgang auf, mit dem ein Client Metadaten schreibt. Das geschieht über den Write-Service.

**Den Initialwert des Value-Attributs eines Variablen-Nodes festlegen**:

```csharp
var machineIsRunningNode = new OpcDataVariableNode<bool>("IsRunning", false);
```

```vbnet
Dim machineIsRunningNode = New OpcDataVariableNode(Of Boolean)("IsRunning", False)
```

**Den Wert des Value-Attributs eines Variablen-Nodes festlegen**:

```csharp
machineIsRunningNode.Value = true;
```

```vbnet
machineIsRunningNode.Value = True
```

**Den Wert des Description-Attributs festlegen**:

```csharp
machineIsRunningNode.Description = "My description";
```

```vbnet
machineIsRunningNode.Description = "My description"
```

**Alle Clients mit aktiver Subscription über die Attributänderungen informieren und Änderungen übernehmen**:

```csharp
machineIsRunningNode.ApplyChanges(server.SystemContext);
```

```vbnet
machineIsRunningNode.ApplyChanges(server.SystemContext)
```

**Den Wert des Description-Attributs in einer anderen Datenquelle als der internen speichern**:

```csharp
machineIsRunningNode.WriteDescriptionCallback = HandleWriteDescription;
...
private OpcAttributeValue<OpcText> HandleWriteDescription(
        OpcWriteAttributeValueContext context,
        OpcAttributeValue<OpcText> value)
{
    return WriteDescriptionToDataSource(context.Node, value) ?? value;
}
```

```vbnet
machineIsRunningNode.WriteDescriptionCallback = AddressOf HandleWriteDescription
...
Private Function HandleWriteDescription( _
        ByVal context As OpcWriteAttributeValueContext, _
        ByVal value As OpcAttributeValue(Of OpcText)) As OpcAttributeValue(Of OpcText)
    Return If(WriteDescriptionToDataSource(context.Node, value), value)
End Function

```

**Den Wert des Value-Attributs eines Variablen-Nodes in einer anderen Datenquelle als der internen speichern**:

```csharp
machineIsRunningNode.WriteVariableValueCallback = HandleWriteVariableValue;
...
private OpcVariableValue<object> HandleWriteVariableValue(
        OpcWriteVariableValueContext context,
        OpcVariableValue<object> value)
{
    return WriteValueToDataSource(context.Node, value) ?? value;
}
```

```vbnet
machineIsRunningNode.WriteVariableValueCallback = AddressOf HandleWriteVariableValue
...
Private Function HandleWriteVariableValue( _
        ByVal context As OpcWriteVariableValueContext, _
        ByVal value As OpcVariableValue(Of Object)) As OpcVariableValue(Of Object)
    Return If(WriteValueToDataSource(context.Node, value), value)
End Function

```

## Werte nur bei Bedarf erfassen

Beteiligte Typen: `OpcNodeManager`, `OpcMonitoredItem`, `OpcMonitoredItemEventArgs`, `OpcAttribute`

Wenn die Werte aus einer Steuerung oder einem anderen Feldgerät stammen, ist es oft unnötig, alle Variablen zyklisch abzufragen. Der Node-Manager erfährt über die Ereignisse `MonitoredItemCreated`, `MonitoredItemModified` und `MonitoredItemDeleted`, welche seiner Nodes Clients gerade abonniert haben. Die Ereignisdaten enthalten das `OpcMonitoredItem` mit NodeId, `Attribute`, `SamplingInterval` und der `Session` des Clients sowie den betroffenen Node (`Node`). Für Anfragen, die mehrere MonitoredItems auf einmal anlegen oder entfernen, stehen zusätzlich die Ereignisse `MonitoredItemsCreated`, `MonitoredItemsModified` und `MonitoredItemsDeleted` zur Verfügung.

Mehrere Clients können denselben Node abonnieren, auch ein Client mehrfach. Das folgende Beispiel zählt deshalb die Abonnements je Node. Abonnements von Ereignissen schließt es über das Attribut aus:

```csharp
public class MachineNodeManager : OpcNodeManager
{
    private readonly Dictionary<OpcNodeId, int> monitored
            = new Dictionary<OpcNodeId, int>();
 
    public MachineNodeManager()
        : base("http://example.com/machine")
    {
        this.MonitoredItemCreated += this.HandleMonitoredItemCreated;
        this.MonitoredItemDeleted += this.HandleMonitoredItemDeleted;
    }
 
    public bool IsMonitored(OpcNodeId nodeId)
    {
        lock (this.monitored)
            return this.monitored.ContainsKey(nodeId);
    }
 
    private void HandleMonitoredItemCreated(
            object sender,
            OpcMonitoredItemEventArgs e)
    {
        if (e.Item.Attribute != OpcAttribute.Value)
            return;
 
        lock (this.monitored) {
            int count;
            this.monitored.TryGetValue(e.Item.NodeId, out count);
            this.monitored[e.Item.NodeId] = count + 1;
        }
    }
 
    private void HandleMonitoredItemDeleted(
            object sender,
            OpcMonitoredItemEventArgs e)
    {
        if (e.Item.Attribute != OpcAttribute.Value)
            return;
 
        lock (this.monitored) {
            int count;
 
            if (!this.monitored.TryGetValue(e.Item.NodeId, out count))
                return;
 
            if (count > 1)
                this.monitored[e.Item.NodeId] = count - 1;
            else
                this.monitored.Remove(e.Item.NodeId);
        }
    }
}

```

```vbnet
Public Class MachineNodeManager
    Inherits OpcNodeManager

    Private ReadOnly monitored As New Dictionary(Of OpcNodeId, Integer)()

    Public Sub New()
        MyBase.New("http://example.com/machine")

        AddHandler Me.MonitoredItemCreated, AddressOf Me.HandleMonitoredItemCreated
        AddHandler Me.MonitoredItemDeleted, AddressOf Me.HandleMonitoredItemDeleted
    End Sub

    Public Function IsMonitored(ByVal nodeId As OpcNodeId) As Boolean
        SyncLock Me.monitored
            Return Me.monitored.ContainsKey(nodeId)
        End SyncLock
    End Function

    Private Sub HandleMonitoredItemCreated( _
            ByVal sender As Object, _
            ByVal e As OpcMonitoredItemEventArgs)
        If e.Item.Attribute <> OpcAttribute.Value Then
            Return
        End If

        SyncLock Me.monitored
            Dim count As Integer
            Me.monitored.TryGetValue(e.Item.NodeId, count)
            Me.monitored(e.Item.NodeId) = count + 1
        End SyncLock
    End Sub

    Private Sub HandleMonitoredItemDeleted( _
            ByVal sender As Object, _
            ByVal e As OpcMonitoredItemEventArgs)
        If e.Item.Attribute <> OpcAttribute.Value Then
            Return
        End If

        SyncLock Me.monitored
            Dim count As Integer

            If Not Me.monitored.TryGetValue(e.Item.NodeId, count) Then
                Return
            End If

            If count > 1 Then
                Me.monitored(e.Item.NodeId) = count - 1
            Else
                Me.monitored.Remove(e.Item.NodeId)
            End If
        End SyncLock
    End Sub
End Class
```

Die Abfrage der Datenquelle beschränkt sich dann auf die Nodes, für die `IsMonitored` den Wert `true` liefert. Ein vollständiges Beispiel zeigt die Anleitung [Steuerungsdaten bereitstellen](https://www.traeger.de/docs/development/opcua/server/device-gateway).

## Zugriffe gebündelt verarbeiten

Beteiligte Typen: `OpcNodeManager`, `OpcNodeAccessEventArgs`, `OpcNodeAccessToken`, `OpcAttribute`, `OpcResult`

Der Server ruft Callbacks wie den `ReadVariableValueCallback` je Node auf. Wenn ein Client mit einer Anfrage 500 Variablen liest, ergeben sich daraus 500 einzelne Aufrufe an die Datenquelle. Für solche Fälle stellt der Node-Manager die Ereignisse `BeforeRead`, `AfterRead`, `BeforeWrite` und `AfterWrite` bereit. Sie treten je Anfrage einmal auf und enthalten in `Tokens` alle Zugriffe der Anfrage auf Nodes dieses Node-Managers. Jedes `OpcNodeAccessToken` nennt den Node (`Node`), das Attribut (`Attribute`), einen optionalen Teilbereich (`Range`), den Wert (`Value`) und das Ergebnis des Zugriffs (`Result`).

- `BeforeRead` tritt auf, bevor der Server die Attribute liest. Werte, die der Handler hier in die Nodes schreibt, liefert der Server mit derselben Anfrage aus.
- `AfterRead` tritt auf, nachdem der Server alle Attribute gelesen hat.
- `BeforeWrite` tritt auf, bevor der Server die Werte in die Nodes schreibt.
- `AfterWrite` tritt auf, nachdem der Server die Werte geschrieben hat. `Result` gibt dann je Zugriff an, ob das Schreiben erfolgreich war.

Im folgenden Beispiel steht `Device` für die Anbindung an die Datenquelle. Der Node-Manager liest vor dem Lesen alle angefragten Werte mit einem Aufruf und gibt nach dem Schreiben alle erfolgreich geschriebenen Werte mit einem Aufruf weiter:

```csharp
public class DeviceNodeManager : OpcNodeManager
{
    public DeviceNodeManager()
        : base("http://example.com/device")
    {
        this.BeforeRead += this.HandleBeforeRead;
        this.AfterWrite += this.HandleAfterWrite;
    }
 
    private void HandleBeforeRead(object sender, OpcNodeAccessEventArgs e)
    {
        var nodes = new List<OpcDataVariableNode>();
 
        foreach (var token in e.Tokens) {
            var node = token.Node as OpcDataVariableNode;
 
            if (token.Attribute == OpcAttribute.Value && node != null && !nodes.Contains(node))
                nodes.Add(node);
        }
 
        var names = new string[nodes.Count];
 
        for (int index = 0; index < nodes.Count; index++)
            names[index] = nodes[index].SymbolicName;
 
        // Device stands for the connection to the data source:
        // one request for all nodes of the client request.
        object[] values = Device.Read(names);
 
        for (int index = 0; index < nodes.Count; index++)
            nodes[index].Value = values[index];
    }
 
    private void HandleAfterWrite(object sender, OpcNodeAccessEventArgs e)
    {
        var names = new List<string>();
        var values = new List<object>();
 
        foreach (var token in e.Tokens) {
            if (token.Attribute == OpcAttribute.Value && token.Result.IsGood) {
                names.Add(token.Node.SymbolicName);
                values.Add(token.Value.Value);
            }
        }
 
        // One request to the device for all values written by the client.
        Device.Write(names.ToArray(), values.ToArray());
    }
}

```

```vbnet
Public Class DeviceNodeManager
    Inherits OpcNodeManager

    Public Sub New()
        MyBase.New("http://example.com/device")

        AddHandler Me.BeforeRead, AddressOf Me.HandleBeforeRead
        AddHandler Me.AfterWrite, AddressOf Me.HandleAfterWrite
    End Sub

    Private Sub HandleBeforeRead(ByVal sender As Object, ByVal e As OpcNodeAccessEventArgs)
        Dim nodes = New List(Of OpcDataVariableNode)()

        For Each token In e.Tokens
            Dim node = TryCast(token.Node, OpcDataVariableNode)

            If token.Attribute = OpcAttribute.Value AndAlso node IsNot Nothing _
                    AndAlso Not nodes.Contains(node) Then
                nodes.Add(node)
            End If
        Next

        Dim names = New String(nodes.Count - 1) {}

        For index As Integer = 0 To nodes.Count - 1
            names(index) = nodes(index).SymbolicName
        Next

        'Device stands for the connection to the data source:
        'one request for all nodes of the client request.
        Dim values As Object() = Device.Read(names)

        For index As Integer = 0 To nodes.Count - 1
            nodes(index).Value = values(index)
        Next
    End Sub

    Private Sub HandleAfterWrite(ByVal sender As Object, ByVal e As OpcNodeAccessEventArgs)
        Dim names = New List(Of String)()
        Dim values = New List(Of Object)()

        For Each token In e.Tokens
            If token.Attribute = OpcAttribute.Value AndAlso token.Result.IsGood Then
                names.Add(token.Node.SymbolicName)
                values.Add(token.Value.Value)
            End If
        Next

        'One request to the device for all values written by the client.
        Device.Write(names.ToArray(), values.ToArray())
    End Sub
End Class
```

Die Ereignisse betreffen alle Attribute. Das Beispiel filtert deshalb auf das Value-Attribut.
