git:20261003.5127062 to git:20261016.7360e78

15 added, 215 removed. Audit A to A.

---
name: tia-csharp-common
description: Internal reference skill — do NOT load directly from user queries.
license: MIT
---
# TIA Portal Openness C# — Common Foundation
Source: TIA Portal Openness General Functions Manual (03/2026, V21);
V21 IntelliSense XML documentation files (PublicAPI\V21\net48\*.xml)
---
## V21 modular assembly architecture
V21 splits the API across multiple DLLs instead of a single `Siemens.Engineering.dll`.
Every project needs `Siemens.Engineering.Base.dll`; add domain DLLs as required.
Referencing the wrong DLL (or omitting one) causes `TypeLoadException` at runtime.
> **Full mapping:** See `references/assembly-namespace-map.md` for the V21
> namespace → DLL → domain skill table, cross-assembly warnings, and csproj patterns.
### Namespaces by assembly
- Only import what is actually used. The list below covers the most commonly needed
- namespaces. The mapping file summarizes the XML-documented namespaces across all 16
- installed V21 PublicAPI DLLs.
-
- **Siemens.Engineering.Base.dll** (always required):
-
- ```csharp
- using Siemens.Engineering; // TiaPortal, Project, ExclusiveAccess, Transaction, events, exceptions
- using Siemens.Engineering.Compiler; // CompilerResult
- using Siemens.Engineering.Compare; // CompareResult
- using Siemens.Engineering.Connection; // connection objects between devices
- using Siemens.Engineering.CrossReference; // cross-reference queries
- using Siemens.Engineering.Download; // DownloadProvider, DownloadConfiguration
- using Siemens.Engineering.Download.Configurations; // device-specific download configs (★ also in Step7 + Startdrive)
- using Siemens.Engineering.HW; // Device, DeviceItem, Subnet, Node, IoSystem (★ also in Step7)
- using Siemens.Engineering.HW.CommunicationConnections; // communication connection configs
- using Siemens.Engineering.HW.Extensions; // extension methods on HW objects
- using Siemens.Engineering.HW.Features; // SoftwareContainer, NetworkInterface (★ also in Step7)
- using Siemens.Engineering.HW.HardwareCatalog; // hardware catalog access
- using Siemens.Engineering.HW.Utilities; // HW utility helpers
- using Siemens.Engineering.Library; // ProjectLibrary, GlobalLibrary
- using Siemens.Engineering.Library.MasterCopies; // MasterCopy, MasterCopyFolder
- using Siemens.Engineering.Library.Types; // LibraryType, LibraryTypeVersion
- using Siemens.Engineering.Multiuser; // LocalSession, ProjectServer
- using Siemens.Engineering.Online; // OnlineProvider, OnlineState
- using Siemens.Engineering.Online.Configurations; // OnlineConfiguration, TLS/authentication callbacks
- using Siemens.Engineering.Security; // SecurityController, certificates
- using Siemens.Engineering.Settings; // TIA Portal settings
- using Siemens.Engineering.Umac; // UmacRole, UmacUser, UmacFunctionRight
- using Siemens.Engineering.Upload; // StationUploadProvider, ParameterUploadProvider
- using Siemens.Engineering.Upload.Configurations; // upload configs (★ also in Startdrive)
- using Siemens.Engineering.VersionControl; // workspace and version control providers
- ```
-
- **Siemens.Engineering.Step7.dll** (PLC operations):
-
- ```csharp
- using Siemens.Engineering.Cax; // CAx import/export
- using Siemens.Engineering.SW; // PlcSoftware
- using Siemens.Engineering.SW.Alarm; // PlcAlarm, AlarmClass
- using Siemens.Engineering.SW.Alarm.TextLists; // alarm text lists
- using Siemens.Engineering.SW.Blocks; // PlcBlock, OB, FB, FC, GlobalDB, InstanceDB, ArrayDB
- using Siemens.Engineering.SW.Blocks.Interface; // block interface access
- using Siemens.Engineering.SW.ExternalSources; // PlcExternalSource
- using Siemens.Engineering.SW.Loader; // program loader
- using Siemens.Engineering.SW.OpcUa; // OPC UA server interface
- using Siemens.Engineering.SW.Supervision; // supervision alarms, operator messages
- using Siemens.Engineering.SW.Tags; // PlcTagTable, PlcTag, PlcConstant
- using Siemens.Engineering.SW.TechnologicalObjects; // technology objects
- using Siemens.Engineering.SW.TechnologicalObjects.Motion; // motion axes (★ also in Startdrive)
- using Siemens.Engineering.SW.Types; // PlcType, PlcStruct
- using Siemens.Engineering.SW.Units; // SoftwareUnit, CodeBlock, DataBlock
- using Siemens.Engineering.SW.WatchAndForceTables; // PlcWatchTable, PlcForceTable
- ```
-
- **Siemens.Engineering.WinCC.dll** (classic HMI):
-
- ```csharp
- using Siemens.Engineering.Hmi; // HmiTarget (★ also in WinCC.Extension)
- using Siemens.Engineering.Hmi.Alarm; // DiscreteAlarm, AnalogAlarm
- using Siemens.Engineering.Hmi.Communication; // HmiConnection
- using Siemens.Engineering.Hmi.Cycle; // cycle settings
- using Siemens.Engineering.Hmi.Dynamic; // dynamization
- using Siemens.Engineering.Hmi.Faceplate; // faceplate types
- using Siemens.Engineering.Hmi.Globalization; // language/translation
- using Siemens.Engineering.Hmi.Logging; // DataLog, LoggingTag
- using Siemens.Engineering.Hmi.Recipe; // Recipe, RecipeElement
- using Siemens.Engineering.Hmi.RuntimeScripting; // VBScript
- using Siemens.Engineering.Hmi.Screen; // HmiScreen, ScreenTemplate, ScreenPopup + all elements
- using Siemens.Engineering.Hmi.Tag; // HmiTag, HmiTagTable
- using Siemens.Engineering.Hmi.TextGraphicList; // text/graphic lists
- using Siemens.Engineering.Hmi.Theming; // HMI themes
- ```
-
- **Siemens.Engineering.Startdrive.dll** (SINAMICS drives):
-
- ```csharp
- using Siemens.Engineering.MC.Drives; // DriveObject, DriveParameter, Telegram
- using Siemens.Engineering.MC.Drives.DFI; // DriveFunctionInterface, Commissioning
- using Siemens.Engineering.MC.Drives.Enums; // drive enumerations
- using Siemens.Engineering.MC.Drives.SecurityObjects; // drive encryption, UMAC
- ```
-
- **Siemens.Engineering.Safety.dll** (F-system):
-
- ```csharp
- using Siemens.Engineering.Safety; // safety operations
- ```
+ Only import what is actually used. Commented `using` blocks for each assembly
+ (Base, Step7, WinCC, Startdrive, Safety) are in `references/assembly-namespace-map.md`
+ under "Common `using` blocks by assembly".
---
## Assembly loading — mandatory for standalone Openness apps
> **Not needed for Add-Ins.** TIA Portal loads Add-In assemblies via its own loader.
> This section applies only to standalone console/desktop Openness applications.
### V21 installation paths
- **Registry key:** `HKLM\SOFTWARE\Siemens\Automation\InstalledApps\Totally Integrated Automation Portal V21` → value `INSTALLPATH`
- **Binary path:** `C:\Program Files\Siemens\Automation\Portal V21\PublicAPI\V21\net48\`
- **Manifest schemas:** `C:\Program Files\Siemens\Automation\Portal V21\PublicAPI\V21\Schemas\`
### Resolver pattern
When using the `AssemblyResolve` approach, register the event before any Openness type is
referenced. This includes method parameters, return types, and class properties — not just
method bodies. A correctly configured `app.config` binding is an alternative.
Recommended pattern: register in a static constructor of `Program`, then move all Openness
code to a separate class so the resolver is active before those classes are loaded.
```csharp
internal static class Program
{
// Register resolver at earliest possible point
static Program()
{
AppDomain.CurrentDomain.AssemblyResolve += OnAssemblyResolve;
}
public static void Main()
{
// All Openness code must be in a separate class
var app = new OpennessApp();
app.Run();
}
private static Assembly OnAssemblyResolve(object sender, ResolveEventArgs args)
{
// Read install path from registry (recommended) or hardcode for simple tools
string opennessFolder = GetOpennessInstallPath(); // implement per project
AssemblyName requestedName = new AssemblyName(args.Name);
string filePath = Path.Combine(opennessFolder,
string.Concat(requestedName.Name, ".dll"));
if (!requestedName.Name.StartsWith("Siemens.Engineering.") ||
!File.Exists(filePath))
return null;
Assembly loaded = Assembly.LoadFrom(filePath);
if (requestedName.FullName != loaded.GetName().FullName)
throw new FileNotFoundException(
"TIA Portal Openness version does not match", filePath);
return loaded;
}
}
```
**Key rules:**
- `Copy Local: False` (`<Private>False</Private>`) must be set for every `Siemens.Engineering` assembly reference.
- V21 uses modular DLLs: `Base.dll` is always needed; add `Step7.dll`, `WinCC.dll`,
`Startdrive.dll`, etc. per task. The resolver handles all of them automatically
because it matches any name starting with `Siemens.Engineering.`.
- The recommended approach reads the install path from the registry key
`HKLM\SOFTWARE\Siemens\Automation\InstalledApps\Totally Integrated Automation Portal V21`
(value `INSTALLPATH`) rather than hardcoding it.
**Registry-based path discovery:**
```csharp
private static string GetOpennessInstallPath()
{
using (var key = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(
@"SOFTWARE\Siemens\Automation\InstalledApps\Totally Integrated Automation Portal V21"))
{
string installPath = key?.GetValue("INSTALLPATH") as string;
if (string.IsNullOrEmpty(installPath))
throw new FileNotFoundException("TIA Portal V21 installation not found in registry.");
return Path.Combine(installPath, @"PublicAPI\V21\net48");
}
}
```
**Alternative — app.config:**
For stable installations where the path is known at build time, an `app.config` file with
`<assemblyBinding>` / `<codeBase>` entries is a simpler option that requires no code.
Use the `AssemblyResolve` event for tools that need to discover the install path at runtime.
---
## TiaPortal instantiation
Always use a `using` statement so the session is disposed on exit or exception:
```csharp
internal class OpennessApp
{
public void Run()
{
using (TiaPortal tiaPortal = new TiaPortal(TiaPortalMode.WithUserInterface))
{
// All Openness work goes here
}
// TIA Portal is disposed (not necessarily closed) after this point
}
}
```
**Modes:**
- `TiaPortalMode.WithUserInterface` — starts a new TIA Portal instance with a visible GUI
- `TiaPortalMode.WithoutUserInterface` — starts a new headless instance, suitable for automation pipelines
**Attaching to a running instance:**
```csharp
// Attach to the first running TIA Portal process of this Openness version
TiaPortal tiaPortal = TiaPortal.GetProcesses().First().Attach();
```
Use `GetProcesses()` when TIA Portal is already open and you do not want to start a new
instance. Returns only processes from the same Openness version as the loaded assembly.
**Dispose vs. close:**
- If started headless and no other Openness client is attached, `Dispose()` closes TIA Portal.
- If started with GUI or other clients are attached, `Dispose()` only disconnects.
- After disposal, the connection is no longer valid. A later API call after the TIA Portal
process is closed externally throws `NonRecoverableException`.
---
- ## Service providers — `GetService<T>()`
-
- V21 uses a service-provider pattern for UI interactions. Call `GetService<T>()` on the
- `TiaPortal` instance to obtain a service. Available services depend on the runtime context.
-
- ### Add-In runtime services
-
- These services are available only when code runs inside a TIA Portal Add-In:
-
- | Service type | Namespace | Purpose |
- |---|---|---|
- | `MessageBoxProvider` | `Siemens.Engineering.AddIn` | Display notification/confirmation dialogs in TIA Portal |
- | `ProgressProvider` | `Siemens.Engineering.AddIn` | Display a progress bar; check `IsCancelRequested` for cancellation |
- | `FeedbackProvider` | `Siemens.Engineering.AddIn` | Log messages to TIA Portal's Inspector window (General Info tab) |
-
- ```csharp
- // MessageBoxProvider — notification and confirmation dialogs
- var msgBox = tiaPortal.GetService<MessageBoxProvider>();
- msgBox?.ShowNotification(NotificationIcon.Information, "Title", "Message text");
- msgBox?.ShowNotification(NotificationIcon.Warning, "Title", "Message", "Detail text");
-
- // Confirmation dialog — defaultChoice is the pre-selected button; return value is user's choice
- ConfirmationResult result = msgBox.ShowConfirmation(
- ConfirmationIcon.General, "Title", "Proceed?",
- ConfirmationChoices.Yes | ConfirmationChoices.No, ConfirmationResult.Yes);
- if (result == ConfirmationResult.No) return;
-
- // ProgressProvider — progress bar with cancellation
- var progress = tiaPortal.GetService<ProgressProvider>();
- progress?.Update("Step description", "Detail text");
- if (progress?.IsCancelRequested == true)
- {
- // User requested cancellation — clean up and return
- }
-
- // FeedbackProvider — log to Inspector window (General Info tab)
- var feedback = tiaPortal.GetService<FeedbackProvider>();
- feedback?.Log(NotificationIcon.Information, "Operation completed successfully.");
- feedback?.Log(NotificationIcon.Warning, "Skipped 3 items due to missing data.");
- ```
-
- **Important:** In Add-In status callbacks (the `OnUpdateStatus` delegate passed to
- `AddActionItem`), `GetService<T>()` returns `null`. Only use services in action
- callbacks where COM access is fully available.
-
- ---
-
- ## Event handlers
-
- Four events are available on `TiaPortal`. Always unsubscribe in a `finally` block.
-
- ### Disposed
-
- Fires when TIA Portal closes while the Openness client is still connected.
-
- ```csharp
- tiaPortal.Disposed += OnDisposed;
- try
- {
- // ... work ...
- }
- finally
- {
- tiaPortal.Disposed -= OnDisposed;
- }
-
- private static void OnDisposed(object sender, EventArgs e)
- {
- // TIA Portal was closed externally — clean up and exit
- }
- ```
-
- ### Notification
-
- Fires for informational messages that require only an acknowledgment (OK).
-
- ```csharp
- tiaPortal.Notification += OnNotification;
- try { /* ... */ }
- finally { tiaPortal.Notification -= OnNotification; }
-
- private static void OnNotification(object sender, NotificationEventArgs e)
- {
- Console.WriteLine($"[TIA Notification] {e.Text}");
- // No response required — notification is auto-acknowledged
- }
- ```
-
- ### Confirmation
-
- Fires for dialogs that require a decision. **Must** set `e.Result` to one of:
- `"Yes"`, `"YesToAll"`, or `"No"`. Any other value throws an exception.
-
- Auto-confirm pattern (most common in automation):
-
- ```csharp
- tiaPortal.Confirmation += OnConfirmation;
- try { /* ... */ }
- finally { tiaPortal.Confirmation -= OnConfirmation; }
-
- private static void OnConfirmation(object sender, ConfirmationEventArgs e)
- {
- e.Result = "Yes";
- }
- ```
-
- ### Authentication (V17+)
-
- Fires when opening a UMAC-protected project. Use to specify the authentication method
- instead of passing credentials via `UmacDelegate`. Only fires for protected projects.
+ ## Service providers and event handlers
- ```csharp
- tiaPortal.Authentication += OnAuthentication;
- try { /* open project ... */ }
- finally { tiaPortal.Authentication -= OnAuthentication; }
+ Full patterns: `references/portal-services-and-events.md` (Add-In `MessageBoxProvider`,
+ `ProgressProvider`, `FeedbackProvider` via `GetService<T>()`, and the `Disposed`,
+ `Notification`, `Confirmation`, `Authentication` events on `TiaPortal`).
- private static void OnAuthentication(object sender, AuthenticationEventArgs e)
- {
- // Choose one of: DesktopSso, Anonymous, Interactive, Credentials
- e.AuthenticationTypeProvider = AuthenticationTypeProvider.DesktopSso;
- // For Credentials mode, credentials are supplied separately via UmacDelegate
- }
- ```
+ Rules that always apply:
- | `AuthenticationTypeProvider` | Behaviour |
- |---|---|
- | `DesktopSso` | Signs in with the current Windows user — no password prompt |
- | `Anonymous` | Uses the anonymous user account — no password prompt |
- | `Interactive` | Shows TIA Portal login dialog to the user |
- | `Credentials` | Reads credentials from the supplied `UmacDelegate` |
+ - `GetService<T>()` returns `null` in Add-In status callbacks (`OnUpdateStatus`);
+ use services only in action callbacks.
+ - Unsubscribe every `TiaPortal` event handler in a `finally` block.
+ - In `Confirmation`, `e.Result` **must** be `"Yes"`, `"YesToAll"` or `"No"`; any other
+ value throws.
+ - `Authentication` (V17+) fires only for UMAC-protected projects; set
+ `e.AuthenticationTypeProvider` (`DesktopSso`, `Anonymous`, `Interactive`, `Credentials`).
---
## Exclusive access
Highly recommended for all non-trivial operations. Signals TIA Portal that a controlled
activity is in progress and displays a dialog to the user.
```csharp
using (ExclusiveAccess exclusiveAccess = tiaPortal.ExclusiveAccess("Generating project"))
{
// All Openness operations here run under exclusive access
// Optionally update the displayed message during long operations
exclusiveAccess.Text = "Compiling software";
// ...
exclusiveAccess.Text = "Downloading to PLC";
}
// Exclusive access is released on dispose
```
**Rules:**
- Only one `ExclusiveAccess` can exist at a time — a second attempt throws a recoverable
exception while the first is still active.
- Setting `exclusiveAccess.Text = string.Empty` or `null` clears the displayed message.
---
## Transactions
Group multiple modifications into a single undo unit. Requires an active `ExclusiveAccess`.
```csharp
using (ExclusiveAccess exclusiveAccess = tiaPortal.ExclusiveAccess("Bulk edit"))
{
using (Transaction transaction = exclusiveAccess.Transaction(project, "Create blocks"))
{
// Perform all modifications
project.DeviceGroups.Create("Line_1");
project.DeviceGroups.Create("Line_2");
// MANDATORY: call CommitOnDispose() to persist changes
// If this line is never reached, the transaction is rolled back
transaction.CommitOnDispose();
}
}
```
**Rollback rules — critical:**
- If `CommitOnDispose()` is never called → rolled back on dispose.
- If an exception occurs at any point before the transaction is disposed → rolled back,
including when `CommitOnDispose()` was already requested or the exception was caught.
- After an `EngineeringTargetInvocationException`, a later `CommitOnDispose()` request can
itself raise a recoverable exception; check `CanCommit` and do not assume persistence.
**Not allowed inside a transaction:**
Compile, go online/offline, ProjectText import/export, opening or closing a global library,
project create/open/open-with-upgrade/save/save-as/close, and changing a TIA Portal setting.
These calls raise a recoverable exception while a transaction is active.
---
## Destructive-operation safety
These rules are mandatory for generated C# Openness code:
- **Never bare `.Delete()`**. Any destructive operation must be inside an active
`ExclusiveAccess` scope and a `Transaction` when the API permits transactions.
- Use `using` blocks for `ExclusiveAccess`, `Transaction`, `TiaPortal`, and other disposable
Openness objects so rollback and release behavior is explicit. Close projects explicitly
according to lifecycle ownership; `Project` is not an `IDisposable` object.
- Call `transaction.CommitOnDispose()` only after every validation and mutation in
the transaction has succeeded.
- Run or request a compile check after generated block, tag, hardware, or HMI
changes. Do not present generated project changes as deployable until that check
passes.
- If an API refuses to run inside a transaction, state that limitation explicitly,
use the narrowest possible `ExclusiveAccess` scope, and require a preview or
user confirmation before the destructive call.
---
## Exception handling
Two top-level categories:
| Category | Base type | Behaviour |
|---|---|---|
| Recoverable | `Siemens.Engineering.EngineeringException` | API call failed; session remains valid |
| Non-recoverable | `Siemens.Engineering.NonRecoverableException` | TIA Portal closed; restart required |
**Recoverable subtypes:**
| Type | Typical cause |
|---|---|
| `EngineeringSecurityException` | Missing access rights |
| `EngineeringObjectDisposedException` | Object no longer exists (e.g. deleted) |
| `EngineeringNotSupportedException` | Attribute or operation not available on this object |
| `EngineeringTargetInvocationException` | General failure despite a valid API call |
| `EngineeringRuntimeException` | Runtime error, e.g. invalid cast |
| `EngineeringOutOfMemoryException` | Insufficient resources in TIA Portal instance |
| `EngineeringUserAbortException` | Operation cancelled by the user (e.g. import dialog) |
| `EngineeringDelegateInvocationException` | Exception thrown inside a caller-supplied delegate |
| `MissingProductsException` | Project requires TIA Portal products/packages not installed (V18+) |
**Recommended catch order:**
```csharp
try
{
// Openness work
}
catch (EngineeringSecurityException ex)
{
Console.WriteLine($"Access denied: {ex.Message}");
}
catch (EngineeringObjectDisposedException ex)
{
Console.WriteLine($"Object disposed: {ex.Message}");
}
catch (EngineeringNotSupportedException ex)
{
Console.WriteLine($"Not supported: {ex.MessageData.Text}");
foreach (ExceptionMessageData detail in ex.DetailMessageData)
Console.WriteLine($" Detail: {detail.Text}");
}
catch (MissingProductsException ex)
{
Console.WriteLine($"Missing products: {ex.Message}");
}
catch (EngineeringTargetInvocationException)
{
throw; // Do not swallow — rethrow for caller to handle
}
catch (EngineeringException)
{
throw; // Do not swallow general engineering exceptions
}
catch (NonRecoverableException ex)
{
Console.WriteLine($"Fatal — TIA Portal closed: {ex.Message}");
// Must restart TIA Portal; the session is gone
}
```
---
## Project open mode
When opening multiple projects or working in multiuser environments:
```csharp
// Primary — visible in project navigation, full read-write access
Project primary = tiaPortal.Projects.Open(
new FileInfo(path), umacDelegate, ProjectOpenMode.Primary);
// Secondary — hidden from UI, always read-only regardless of user rights
Project secondary = tiaPortal.Projects.Open(
new FileInfo(path), null, ProjectOpenMode.Secondary);
bool isPrimary = secondary.IsPrimary; // false
```
**Rules:**
- Only one primary project per TIA Portal instance.
- Secondary projects do not need a primary project to be open first.
- A user with write rights to a UMAC-protected project still gets read-only access when
opening it as secondary.
---
## Object model contracts
### Composition methods
Most engineering objects are accessed through compositions. Available methods:
| Method | Behaviour |
|---|---|
| `Create(id, ...)` | Creates and adds a new instance. Signature varies per composition. |
| `Find(id)` | Finds by identifier. **Non-recursive** — searches current level only. |
| `GetEnumerator()` | Used implicitly in `foreach` loops. |
| `Contains(obj)` | Returns `bool` — checks if object is in the composition. |
| `IndexOf(obj)` | Returns `int` index of the object in the composition. |
| `Import(path, ImportOptions)` | Available on compositions that support import. `ImportOptions`: `None` or `Overwrite`. |
| `Delete(obj)` | Removes and deletes the instance. |
Not all methods are available on every composition — check domain skills.
### Object equality
```csharp
// Reference equality (same .NET object reference)
bool sameRef = (objA == objB);
// TIA Portal identity equality (same engineering object, possibly different references)
bool sameObj = System.Object.Equals(objA, objB);
```
Use `System.Object.Equals()` when comparing objects retrieved through different navigation
paths that may represent the same underlying engineering object.
### DirectoryInfo / FileInfo — absolute paths only
All `DirectoryInfo` and `FileInfo` instances passed to Openness **must** use absolute paths.
Relative paths cause an exception at runtime.
```csharp
// Correct
var dir = new DirectoryInfo(@"C:\Projects\MyProject\Export");
// Wrong — will throw
var dir = new DirectoryInfo(@"Export");
```
---
## Bulk attribute access
Useful for dynamic tooling or when setting multiple attributes efficiently on HW objects.
### Read all attributes
```csharp
// Get names of all available attributes
IList<EngineeringAttributeInfo> infos =
((IEngineeringObject)deviceItem).GetAttributeInfos();
// Read all readable attributes in one call
IList<string> names = infos.Select(i => i.Name).ToList();
IList<object> values = ((IEngineeringObject)deviceItem).GetAttributes(names);
```
### Filtered read by access mode
```csharp
// AttributeAccessOptions: None, ReadOnly, WriteOnly, ReadWrite
IReadOnlyList<KeyValuePair<string, object>> readableAttrs =
deviceItem.GetAttributes(AttributeAccessOptions.ReadOnly);
```
### Bulk write (HW objects only — provides callback)
```csharp
var pairs = new List<KeyValuePair<string, object>>
{
new KeyValuePair<string, object>("Name", "NewModuleName"),
new KeyValuePair<string, object>("Comment", "Auto-generated")
};
// Overload with callback fires after each attribute is set
deviceItem.SetAttributes(pairs, (name, val, ex) =>
{
if (ex != null)
Console.WriteLine($"Failed to set {name}: {ex.Message}");
});
// Overload without callback — for non-HW objects
((IEngineeringObject)someObject).SetAttributes(pairs);
```