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

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

Description

Describes the error message.

FileName

The name of the file in which the error occurred.

Line

The line in which the error occurred.

Column

The column in which the error occurred.

Project

Project in which the error occurred.

Reading the VS output and the error list 1:

DTE2 (from EnvDTE80) is the more modern version compared to DTE. The build output pane is called exactly "Build". The index in errors.Item(i) is 1-based. The last four properties are not always populated – an error message such as “No license found” is not tied to a specific file or line.

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
}
Reading the VS output and the error list 2:

The late-binding issue only affects direct access to the ErrorItems collection via PowerShell. In C#, dte2.ToolWindows.ErrorList.ErrorItems works without any problems.

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")