Wie das Automation Interface per DTE verbunden wird
Das Automation Interface wird über das DTE-Objekt von Visual Studio oder TcXaeShell angesprochen. DTE steht für Development Tools Environment. Das DTE ist das oberste Automatisierungsobjekt der IDE: Über dte.Solution.Projects gelangen Sie an die einzelnen Projekte, und das TwinCAT-Projekt liefert über seine Object-Eigenschaft den Einstiegspunkt ITcSysManager. Dieser Artikel zeigt, wie Sie eine Verbindung zu einer bereits laufenden Instanz aufbauen und das ITcSysManager-Interface bekommen.
Eine laufende IDE-Instanz wird über die Running Object Table, kurz ROT, des Betriebssystems angesprochen. Die ROT ist eine systemweite Tabelle, in der laufende COM-Server unter einer eindeutigen ProgID registriert sind. Welche ProgID die richtige ist, hängt vom Host wie Visual Studio oder TcXaeShell und dessen Version ab. Damit das Anhängen funktioniert, müssen anfragender Prozess und IDE im selben Benutzerkontext und mit demselben Elevation-Level laufen, da die ROT pro Session getrennt geführt wird.
flowchart LR
A[DTE object] --> B[Solution]
B --> C[Projects.Item 1]
C --> D[Object as ITcSysManager]
Voraussetzungen
- TwinCAT XAE mit Visual Studio oder TcXaeShell ist installiert.
- Eine Visual Studio- oder TcXaeShell-Instanz läuft bereits, wenn
GetActiveObjectverwendet wird.
C#-Beispiel
Für C# ab .NET 5 müssen die entsprechenden Ole32-Funktionen manuell importiert werden. Die Methode Marshal.GetActiveObject ist im klassischen .NET Framework enthalten, wurde aber in .NET 5+ aus dem System.Runtime.InteropServices.Marshal-Typ entfernt. Rufen Sie die zugrunde liegenden Win32-Funktionen CLSIDFromProgID und GetActiveObject per P/Invoke auf. CLSIDFromProgID übersetzt die lesbare ProgID, etwa TcXaeShell.DTE.17.0, in die binäre CLSID, mit der GetActiveObject den passenden Eintrag in der ROT nachschlägt. Es ist sinnvoll, diese Aufrufe in einer wiederverwendbaren Hilfsklasse zu kapseln:
using System.Runtime.InteropServices;
internal static class ComHelper
{
[DllImport("ole32.dll")]
private static extern int CLSIDFromProgID(
[MarshalAs(UnmanagedType.LPWStr)] string lpszProgID,
out Guid pclsid);
[DllImport("oleaut32.dll")]
private static extern int GetActiveObject(
ref Guid rclsid,
IntPtr pvReserved,
[MarshalAs(UnmanagedType.IUnknown)] out object ppunk);
public static object GetActiveObject(string progId)
{
CLSIDFromProgID(progId, out Guid clsid);
int hr = GetActiveObject(ref clsid, IntPtr.Zero, out object obj);
Marshal.ThrowExceptionForHR(hr);
return obj;
}
}
Mit dieser Hilfsklasse lässt sich die laufende TwinCAT-Instanz erreichen. Der Aufruf liefert das DTE-Objekt. Über dte.Solution.Projects.Item(1).Object erhalten Sie das ITcSysManager-Interface des ersten Projekts der geöffneten Solution. GetActiveObject setzt voraus, dass bereits eine Instanz mit der angegebenen ProgID läuft. Andernfalls schlägt der Aufruf mit einem MK_E_UNAVAILABLE-HRESULT fehl. Der Index Item(1) ist 1-basiert. Bei Solutions mit mehreren Projekten müssen Sie das gewünschte TwinCAT-Projekt gezielt auswählen.
DTE dte = (DTE)ComHelper.GetActiveObject("TcXaeShell.DTE.17.0");
ITcSysManager sysManager = dte.Solution.Projects.Item(1).Object as ITcSysManager;
![]() | PowerShell und die TwinCAT-Instanz müssen dasselbe "Elevation Level" besitzen. Läuft TwinCAT als Administrator und PowerShell nicht, oder umgekehrt, befinden sich die ROT-Einträge in getrennten Sessions und sind nicht sichtbar. |
PowerShell-5-Beispiel
In PowerShell 5 ist das Verbinden mit einer laufenden Instanz unkompliziert, da Windows PowerShell auf dem klassischen .NET Framework basiert und Marshal.GetActiveObject dort zur Verfügung steht. Es ist kein manueller Import von OLE-Funktionen nötig. Der folgende Aufruf hängt sich an die laufende IDE an, öffnet die angegebene Solution und greift auf das erste Projekt zu. Da dte.Solution.Open() keinen Rückgabewert liefert, wird das Projekt anschließend über Projects.Item(1) geholt. Dessen Object-Eigenschaft ist das ITcSysManager-Interface.
$dte = [System.Runtime.InteropServices.Marshal]::GetActiveObject("TcXaeShell.DTE.17.0")
$dte.Solution.Open("C:\Projects\MyProject\MyProject.sln")
$project = $dte.Solution.Projects.Item(1)
$sysManager = $project.Object
![]() | PowerShell und die TwinCAT-Instanz müssen dasselbe "Elevation Level" besitzen. Läuft TwinCAT als Administrator und PowerShell nicht, oder umgekehrt, befinden sich die ROT-Einträge in getrennten Sessions und sind nicht sichtbar. |
PowerShell-7-Beispiel
PowerShell 7 basiert auf .NET 5+. Dort ist Marshal.GetActiveObject nicht mehr standardmäßig vorhanden. Daher müssen die benötigten Typen über Add-Type selbst hinzugefügt werden. Die folgende Inline-Typdefinition kapselt dieselben P/Invoke-Aufrufe wie das C#-Beispiel, also CLSIDFromProgID + GetActiveObject, in einer Klasse ROT. Nach dem Kompilieren der Typdefinition steht die statische Methode [ROT]::GetActiveObject(...) zur Verfügung. Der weitere Ablauf ist identisch zu PowerShell 5: Solution öffnen, Projekt holen und Object auswerten.
Add-Type -TypeDefinition @"
using System;
using System.Runtime.InteropServices;
public class ROT {
[DllImport("ole32.dll")]
private static extern int CLSIDFromProgID(
[MarshalAs(UnmanagedType.LPWStr)] string lpszProgID,
out Guid pclsid);
[DllImport("oleaut32.dll")]
private static extern int GetActiveObject(
ref Guid rclsid,
IntPtr pvReserved,
[MarshalAs(UnmanagedType.IUnknown)] out object ppunk);
public static object GetActiveObject(string progId) {
Guid clsid;
CLSIDFromProgID(progId, out clsid);
object obj;
int hr = GetActiveObject(ref clsid, IntPtr.Zero, out obj);
if (hr != 0) Marshal.ThrowExceptionForHR(hr);
return obj;
}
} "@
$dte =[System.Runtime.InteropServices.Marshal]::GetActiveObject("TcXaeShell.DTE.17.0")
$dte.Solution.Open("C:\Projects\MyProject\MyProject.sln")
$project = $dte.Solution.Projects.Item(1)
$sysManager = $project.Object
![]() | PowerShell und die TwinCAT-Instanz müssen dasselbe "Elevation Level" besitzen. Läuft TwinCAT als Administrator und PowerShell nicht, oder umgekehrt, befinden sich die ROT-Einträge in getrennten Sessions und sind nicht sichtbar. |
An eine laufende VS-Instanz über die Running Object Table anhängen
Wenn mehrere VS-Instanzen laufen und eine bestimmte Solution-Datei gesucht wird, kann die Running Object Table, kurz ROT, verwendet werden. Die Klasse DteHelper implementiert diesen Ansatz vollständig in C#. Im Unterschied zum einfachen GetActiveObject-Aufruf, der nur die zuerst gefundene Instanz einer ProgID liefert, durchläuft dieser Ansatz alle Einträge der ROT und prüft jeden DTE-Eintrag auf seinen Solution-Pfad. Das ist nowendig, wenn mehrere identische Hosts, etwa mehrere TcXaeShell.DTE.17.0, gleichzeitig registriert sind und eindeutig die richtige Instanz angesprochen werden soll.
Die drei Methoden hängen voneinander ab:
Methode | Beschreibung |
|---|---|
| Fragt die Running Object Table, kurz ROT, nach einem Snapshot aller laufenden Prozesse ab. Gibt alle gefundenen Prozesse in einer |
| Sucht nach DTE-Instanzen im ROT-Snapshot. Die Abfrage nach |
| Verwendet |
using EnvDTE;
using System.Collections;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
internal static class DteHelper
{
[DllImport("ole32.dll")]
private static extern int GetRunningObjectTable(uint reserved, out IRunningObjectTable pprot);
[DllImport("ole32.dll")]
private static extern int CreateBindCtx(uint reserved, out IBindCtx ppbc);
public static Hashtable GetRunningObjectTable()
{
Hashtable result = new Hashtable();
GetRunningObjectTable(0, out IRunningObjectTable rot);
rot.EnumRunning(out IEnumMoniker enumMoniker);
IMoniker[] monikers = new IMoniker[1];
while (enumMoniker.Next(1, monikers, IntPtr.Zero) == 0)
{
CreateBindCtx(0, out IBindCtx ctx);
monikers[0].GetDisplayName(ctx, null, out string name);
rot.GetObject(monikers[0], out object obj);
result[name] = obj;
}
return result;
}
public static Hashtable GetIDEInstances(bool openSolutionsOnly, string progId)
{
Hashtable result = new Hashtable();
Hashtable runningObjects = GetRunningObjectTable();
IDictionaryEnumerator enumerator = runningObjects.GetEnumerator();
while (enumerator.MoveNext())
{
string candidateName = (string)enumerator.Key;
if (!candidateName.StartsWith("!" + progId))
continue;
DTE ide = enumerator.Value as DTE;
if (ide == null)
continue;
if (openSolutionsOnly)
{
try
{
string solutionFile = ide.Solution.FullName;
if (solutionFile != string.Empty)
result[candidateName] = ide;
}
catch { }
}
else
{
result[candidateName] = ide;
}
}
return result;
}
public static DTE AttachToExistingDte(string solutionPath, string progId)
{
Hashtable dteInstances = GetIDEInstances(false, progId);
IDictionaryEnumerator enumerator = dteInstances.GetEnumerator();
while (enumerator.MoveNext())
{
DTE dte = enumerator.Value as DTE;
if (dte?.Solution.FullName == solutionPath)
{
Console.WriteLine($"Found solution: {dte.Name}");
return dte;
}
}
return null;
}
}
Um die Klasse zu nutzen und die Verbindung zu einer bestimmten TwinCAT-Instanz aufzubauen, gibt es verschiedene Möglichkeiten. Grundsätzlich liefert GetRunningObjectTable() zunächst einen Snapshot aller registrierten COM-Objekte. GetIDEInstances() filtert daraus die DTE-Instanzen anhand der ProgID heraus, und AttachToExistingDte() wählt schließlich diejenige Instanz aus, deren geöffnete Solution dem gesuchten Pfad entspricht. So lässt sich gezielt eine von mehreren parallellaufenden IDE-Instanzen ansprechen, etwa wenn auf einem Engineering-Rechner mehrere TwinCAT-Projekte gleichzeitig geöffnet sind.
Beispiel Verbinden über Solutionpfad:
//Reading all running dte objects and displaying them in te console
System.Collections.Hashtable runningObjects = DteHelper.GetRunningObjectTable();
Console.WriteLine("Running COM objects:");
foreach (System.Collections.DictionaryEntry entry in runningObjects)
{
Console.WriteLine($"Name: {entry.Key}, Type: {entry.Value.GetType().FullName}");
}
//Attach to specific dte with matching solution path
DTE dte = DteHelper.AttachToExistingDte(
@"C:\Users\Admin\Documents\TcXaeShell\TwinCAT Project1\TwinCAT Project1.sln",
"TcXaeShell.DTE.17.0"
);
Auslesen von allen TwinCAT-Instanzen und Auswählen
// Reading all runnning TcXaeShells
Hashtable instances = DteHelper.GetIDEInstances(true, "TcXaeShell.DTE.17.0");
IDictionaryEnumerator enumerator = instances.GetEnumerator();
//Enumerating through all found shells
while (enumerator.MoveNext())
{
//Get DTE and check wether its the correct DTE or not
DTE dte = enumerator.Value as DTE;
Console.WriteLine(dte?.Solution.FullName); // e.g. C:\Projects\MyProject\MyProject.sln
}
