Reading the VS output and the error list
The contents of the build output window and the error list can be read programmatically using the DTE2 interface. This is useful for automated build reports.
Requirements
- A DTE connection is active.
C# sample
using EnvDTE;
using EnvDTE80;
DTE2 dte2 = (DTE2)dte;
// Read build output window
OutputWindow outputWindow = dte2.ToolWindows.OutputWindow;
foreach (OutputWindowPane pane in outputWindow.OutputWindowPanes)
{
if (pane.Name == "Build")
{
TextDocument textDoc = pane.TextDocument;
TextSelection sel = textDoc.Selection;
sel.StartOfDocument(false);
sel.EndOfDocument(true);
string buildOutput = sel.Text;
Console.WriteLine(buildOutput);
break;
}
}
// Read error list
ErrorList errorList = dte2.ToolWindows.ErrorList;
ErrorItems errors = errorList.ErrorItems;
for (int i = 1; i <= errors.Count; i++)
{
ErrorItem item = errors.Item(i);
Console.WriteLine($"[{item.ErrorLevel}] {item.Description} ({item.FileName}:{item.Line})");
}
The error list window displays status messages for various functions of the Visual Studio development environment and, consequently, the TwinCAT development environment as well. Every ErrorItem object has the following properties:
Property | Description |
|---|---|
| Describes the error message. |
| The name of the file in which the error occurred. |
| The line in which the error occurred. |
| The column in which the error occurred. |
| Project in which the error occurred. |
![]() |
|
Reading the error list in PowerShell
Since PowerShell accesses COM objects via late binding, the direct cast from DTE to DTE2 used in C# cannot be applied here. This results in the loss of type information, which prevents reliable casting. The recommended solution compiles the C# logic at runtime using Add-Type directly in PowerShell and executes it. This means that early binding will continue to be used with the Interop assemblies.
Requirements
PowerShell 5.x or 7.x and TwinCAT XAE must be installed.
Problem and solution
The C# approach in PowerShell fails because PowerShell does not correctly resolve the ErrorItems collection during DTE2 late binding:
# FUNKTIONIERT NICHT (Late Binding Fehler):
# $errors = $dte2.ToolWindows.ErrorList.ErrorItems
PowerShell 5 solution:
# Locate the Visual Studio installation path using vswhere.exe,
# which ships with every VS installation since 2017
# only useable when VisualStudio is installed
$vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe"
$installPath = & $vswhere -latest -property installationPath
# For TcXaeShell version 4026+
$installPath = 'C:\Program Files\Beckhoff\TcXaeShell'
# Locate the Microsoft.VisualStudio.Interop assembly dynamically,
# since its exact path varies depending on the VS version and edition
$interopDll = Get-ChildItem "$installPath" -Recurse -Filter "Microsoft.VisualStudio.Interop.dll" | Select-Object -First 1
# Load assemblies via Reflection instead of Add-Type -Path,
# because LoadFrom is more tolerant of missing transitive dependencies
$asmEnvDTE = [System.Reflection.Assembly]::LoadFrom("$installPath\Common7\IDE\PublicAssemblies\EnvDTE.dll")
$asmEnvDTE80 = [System.Reflection.Assembly]::LoadFrom("$installPath\Common7\IDE\PublicAssemblies\EnvDTE80.dll")
$asmInterop = [System.Reflection.Assembly]::LoadFrom($interopDll.FullName)
# Compile a C# helper class at runtime using the loaded assemblies as references.
# This is necessary because PowerShell uses COM late binding, which loses type information
# and prevents a direct cast from DTE to DTE2. C# uses early binding and resolves
# the cast correctly via the 'as' operator and QueryInterface under the hood.
Add-Type -ReferencedAssemblies @(
$asmEnvDTE.Location,
$asmEnvDTE80.Location,
$asmInterop.Location
) -TypeDefinition @"
using System;
using System.Collections.Generic;
using EnvDTE;
using EnvDTE80;
public class ErrorListReader
{
public static List<string[]> GetErrors(object dteObject)
{
var results = new List<string[]>();
// Cast the generic COM object to DTE2 using early binding.
// This is the key step that fails in PowerShell but works correctly in C#.
DTE2 dte2 = dteObject as DTE2;
if (dte2 == null) return results;
// Access the ErrorList via ToolWindows, which is only available on DTE2
ErrorList errorList = dte2.ToolWindows.ErrorList;
ErrorItems items = errorList.ErrorItems;
// Note: COM collections are 1-indexed, not 0-indexed
for (int i = 1; i <= items.Count; i++)
{
ErrorItem item = items.Item(i);
results.Add(new string[] {
item.ErrorLevel.ToString(), // 1 = Error, 2 = Warning, 3 = Message
item.Description,
item.FileName,
item.Line.ToString(),
item.Project
});
}
return results;
}
}
"@
# Get a reference to the running TcXaeShell instance via the Running Object Table (ROT)
$dte = [System.Runtime.InteropServices.Marshal]::GetActiveObject("TcXaeShell.DTE.17.0")
# Call the C# method, passing the raw COM object
$errors = [ErrorListReader]::GetErrors($dte)
# Format and display the results as a PowerShell table
if ($errors.Count -eq 0) {
Write-Host "No errors found."
} else {
$errors | ForEach-Object {
[PSCustomObject]@{
Level = $_[0]
Description = $_[1]
File = $_[2]
Line = $_[3]
Project = $_[4]
}
} | Format-Table -AutoSize
}
![]() | The late-binding issue only affects direct access to the |
Clearing the error list
The error list can be automatically cleared using the DTE ExecuteCommand mechanism:
C#:
dte.ExecuteCommand("OtherContextMenus.ErrorList.Clear");PowerShell:
$dte.ExecuteCommand("OtherContextMenus.ErrorList.Clear")