Connecting to the Automation Interface via DTE
The Automation Interface is accessed via the DTE object in Visual Studio or TcXaeShell. DTE stands for Development Tools Environment. The DTE is the top-level automation object in the IDE: You can access the individual projects via dte.Solution.Projects, and the TwinCAT project provides the entry point ITcSysManager through its Object property. This article shows you how to set up a connection to an instance that is already running and obtain the ITcSysManager interface.
A running IDE instance is accessed via the operating system's Running Object Table (ROT). ROT is a system-wide table in which running COM servers are registered under a unique ProgID. Which ProgID is the correct one depends on the host – such as Visual Studio or TcXaeShell – and its version. For attachment to work, the requesting process and the IDE must run in the same user context and with the same elevation level, since the ROT is managed separately for each session.
flowchart LR
A[DTE object] --> B[Solution]
B --> C[Projects.Item 1]
C --> D[Object as ITcSysManager]
Requirements
- TwinCAT XAE with Visual Studio or TcXaeShell is installed.
- A Visual Studio or TcXaeShell instance is already running when
GetActiveObjectis used.
C# sample
For C# in .NET 5 and later, the corresponding Ole32 functions must be imported manually. The Marshal.GetActiveObject method is included in the classic .NET Framework, but was removed from the System.Runtime.InteropServices.Marshal type in .NET 5+. Call the underlying Win32 functions CLSIDFromProgID and GetActiveObject using P/Invoke. CLSIDFromProgID converts the human-readable ProgID – such as TcXaeShell.DTE.17.0 – into the binary CLSID, which GetActiveObject uses to look up the corresponding entry in the ROT. It makes sense to encapsulate these calls in a reusable helper class:
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;
}
}
This helper class can be used to access the currently running TwinCAT instance. The call returns the DTE object. Via dte.Solution.Projects.Item(1).Object, you can obtain the ITcSysManager interface of the first project in the open solution. GetActiveObject requires that an instance with the specified ProgID is already running. Otherwise, the call fails with an MK_E_UNAVAILABLE HRESULT. The index Item(1) is 1-based. For solutions with multiple projects, you must specifically select the desired TwinCAT project.
DTE dte = (DTE)ComHelper.GetActiveObject("TcXaeShell.DTE.17.0");
ITcSysManager sysManager = dte.Solution.Projects.Item(1).Object as ITcSysManager;
![]() | PowerShell and the TwinCAT instance must have the same “Elevation Level”. If TwinCAT is running as an administrator but PowerShell is not, or vice versa, the ROT entries are located in separate sessions and are not visible. |
PowerShell 5 sample
In PowerShell 5, establishing a connection to a running instance is straightforward because Windows PowerShell is based on the classic .NET Framework, where Marshal.GetActiveObject is available. There is no need to manually import OLE functions. The following command attaches to the running IDE, opens the specified solution, and accesses the first project. Since dte.Solution.Open() does not return a value, the project is then retrieved using Projects.Item(1). Its Object property is the 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 and the TwinCAT instance must have the same “Elevation Level”. If TwinCAT is running as an administrator but PowerShell is not, or vice versa, the ROT entries are located in separate sessions and are not visible. |
PowerShell 7 sample
PowerShell 7 is based on .NET 5+. Marshal.GetActiveObject is no longer included by default there. Therefore, you must add the required types yourself via Add-Type. The following inline type definition encapsulates the same P/Invoke calls as the C# sample – namely, CLSIDFromProgID and GetActiveObject – in a class named ROT. After compiling the type definition, the static method [ROT]::GetActiveObject(...) is available. The rest of the process is the same as in PowerShell 5: Open the solution, load the project, and evaluate Object.
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 and the TwinCAT instance must have the same “Elevation Level”. If TwinCAT is running as an administrator but PowerShell is not, or vice versa, the ROT entries are located in separate sessions and are not visible. |
Attaching to a running VS instance via the Running Object Table
If multiple VS instances are running and you are searching for a specific solution file, you can use the Running Object Table (ROT). The DteHelper class fully implements this approach in C#. Unlike a simple call to GetActiveObject, which returns only the first instance of a ProgID found, this approach iterates through all entries in the ROT and checks each DTE entry for its solution path. This is necessary when multiple identical hosts – such as several TcXaeShell.DTE.17.0 – are registered at the same time and you need to ensure that the correct instance is accessed.
The three methods are interdependent:
Method | Description |
|---|---|
| Query the Running Object Table (ROT) for a snapshot of all running processes. Returns all found processes in a |
| Search for DTE instances in the ROT snapshot. The query for |
| Uses |
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;
}
}
There are several ways to use the class and establish a connection to a specific TwinCAT instance. Generally, GetRunningObjectTable() first returns a snapshot of all registered COM objects. GetIDEInstances() filters the DTE instances based on the ProgID, and AttachToExistingDte() then selects the instance whose open solution matches the path being searched for. This allows you to specifically address one of several concurrently running IDE instances, for example, when multiple TwinCAT projects are open at the same time on an engineering computer.
Example: Connection via the solution path:
//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"
);
Reading all TwinCAT instances and selecting one
// 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
}
