SapGui.Wrapper
1.1.0
dotnet add package SapGui.Wrapper --version 1.1.0
NuGet\Install-Package SapGui.Wrapper -Version 1.1.0
<PackageReference Include="SapGui.Wrapper" Version="1.1.0" />
<PackageVersion Include="SapGui.Wrapper" Version="1.1.0" />
<PackageReference Include="SapGui.Wrapper" />
paket add SapGui.Wrapper --version 1.1.0
#r "nuget: SapGui.Wrapper, 1.1.0"
#:package SapGui.Wrapper@1.1.0
#addin nuget:?package=SapGui.Wrapper&version=1.1.0
#tool nuget:?package=SapGui.Wrapper&version=1.1.0
SapGui.Wrapper

A strongly-typed .NET wrapper for the SAP GUI Scripting API Purpose-built for UiPath Coded Workflows - giving RPA developers IntelliSense, compile-time safety, and clean, readable code instead of raw COM calls.
Why this wrapper?
Native SAP GUI automation in UiPath forces you to choose between:
| Approach | Problem |
|---|---|
| Classic Activities (click/type) | Fragile, image-based, slow |
Invoke Code + raw COM (SapROTWr) |
Verbose, no IntelliSense, runtime errors only |
| Script Recorder → VBScript | Not reusable, no structure |
SapGui.Wrapper gives you another option: paste the recorder IDs you already have, use C# or VB.NET objects with full IntelliSense, and get exceptions with meaningful messages the moment anything goes wrong.
UiPath Coded Workflow (C# / VB.NET)
│
▼
SapGui.Wrapper ← this NuGet package
│
▼
sapfewse.ocx ← SAP GUI Scripting Engine (installed with SAP GUI)
│
▼
SAP GUI (running)
Zero build-time dependency on the OCX - the package uses late-binding COM interop, so it works on any machine where SAP GUI is installed.
Prerequisites
| Requirement | Details |
|---|---|
| SAP GUI | 7.40 or later, installed on the robot machine |
| Scripting enabled | SAP GUI Options → Accessibility & Scripting → Enable scripting |
| .NET | net461 (legacy UiPath) or net6.0-windows (UiPath Studio 23+) |
| Platform | x64 Windows only (SAP GUI constraint) |
Installation
In UiPath Studio:
Manage Packages → NuGet.org → search SapGui.Wrapper → Install
CLI:
dotnet add package SapGui.Wrapper
UiPath – Coded Workflow
This is the primary use case. A CodedWorkflow file in UiPath Studio is a plain C# class - use it exactly like any other C# code.
using System.Data;
using SapGui.Wrapper;
using UiPath.CodedWorkflows;
namespace MyUiPathProject
{
public class ProcessMaterialList : CodedWorkflow
{
[Workflow]
public DataTable Execute(string plant)
{
using var sap = SapGuiClient.Attach();
var session = sap.Session;
// ── Log who we're running as ──────────────────────────────────────────
Log($"System: {session.Info.SystemName} | User: {session.Info.User} | TCode: {session.Info.Transaction}");
// ── Navigate ──────────────────────────────────────────────────────────
session.MainWindow().Maximize();
session.StartTransaction("MM60");
// ── Fill selection screen ─────────────────────────────────────────────
session.TextField("wnd[0]/usr/txtS_WERKS-LOW").Text = plant;
session.PressExecute(); // F8
// ── Handle "no results" popup ─────────────────────────────────────────
var popup = session.GetActivePopup();
if (popup != null)
{
Log($"Popup: {popup.Text}", LogLevel.Warn);
popup.ClickOk();
return new DataTable();
}
// ── Read ALV grid ─────────────────────────────────────────────────────
var grid = session.GridView("wnd[0]/usr/cntlGRID/shellcont/shell");
var rows = grid.GetRows(new[] { "MATNR", "MAKTX", "MEINS", "LABST" });
// ── Convert to DataTable for UiPath data activities ───────────────────
var dt = new DataTable();
dt.Columns.Add("Material"); dt.Columns.Add("Description");
dt.Columns.Add("Unit"); dt.Columns.Add("UnrestrictedStock");
foreach (var row in rows)
dt.Rows.Add(row["MATNR"], row["MAKTX"], row["MEINS"], row["LABST"]);
session.ExitTransaction();
return dt;
}
}
}
What makes this clean for RPA
StartTransactionreplaces typing/nXX+ Enter every timeGetActivePopup()handles bothGuiMessageWindowandGuiModalWindowuniformlygrid.GetRows(columns)returnsList<Dictionary<string, string>>- maps directly to DataTable columnsExitTransaction()navigates back to Easy Access without hardcoding/n- Every method throws a descriptive exception on failure - no silent
nullreturns from COM
UiPath – Invoke Code activity (VB.NET)
Works in classic UiPath projects without Coded Workflows:
Dim sap As SapGuiClient = SapGuiClient.Attach()
Dim session As GuiSession = sap.Session
session.StartTransaction("VA01")
session.TextField("wnd[0]/usr/ctxtVBAK-AUART").Text = "OR"
session.ComboBox("wnd[0]/usr/cmbVBAK-VKORG").Key = "1000"
session.PressEnter()
Dim status As String = session.Statusbar().Text
Dim isError As Boolean = session.Statusbar().IsError
UiPath – static one-liners (SapGuiHelper)
For the simplest workflows where you just need to set a field or press a button, SapGuiHelper removes even the Attach() call:
' VB.NET in UiPath Invoke Code - no variable management needed
SapGuiHelper.StartTransaction("MM01")
SapGuiHelper.SetText("wnd[0]/usr/txtMM01-MATNR", "MAT-0042")
SapGuiHelper.PressEnter()
Dim msg As String = SapGuiHelper.GetStatusMessage()
If SapGuiHelper.HasError() Then Throw New Exception("SAP error: " & msg)
Getting component IDs from the SAP Script Recorder
Every component ID ("wnd[0]/usr/txtRSYST-BNAME") comes directly from the SAP GUI Script Recorder:
- In SAP GUI: Alt+F12 → Script Recording and Playback → Start Recording
- Perform the actions manually
- Stop recording - the VBScript output contains every ID you need
- Paste those IDs into this wrapper; only change is adding
()to method calls
Recorder output:
session.findById("wnd[0]/usr/txtRSYST-BNAME").text = "MYUSER"
session.findById("wnd[0]/tbar[1]/btn[8]").press
C# equivalent - paste the IDs, one mechanical change:
session.findById("wnd[0]/usr/txtRSYST-BNAME").Text = "MYUSER";
session.findById("wnd[0]/tbar[1]/btn[8]").press();
findById returns dynamic, so all SAP properties and methods resolve at runtime - identical to VBScript. For compile-time IntelliSense, use the typed overload:
session.FindById<GuiTextField>("wnd[0]/usr/txtRSYST-BNAME").Text = "MYUSER";
Common patterns
Reading a classic ABAP table
var table = session.Table("wnd[0]/usr/tblSAPLXXX");
// Scroll-aware read: loop in visible-row increments
int totalRows = table.RowCount;
int pageSize = table.VisibleRowCount;
for (int start = 0; start < totalRows; start += pageSize)
{
table.ScrollToRow(start);
for (int r = start; r < Math.Min(start + pageSize, totalRows); r++)
Console.WriteLine(table.GetCellValue(r, 0));
}
Reading an ALV grid (scroll-aware)
var grid = session.GridView("wnd[0]/usr/cntlGRID/shellcont/shell");
Log($"Total rows: {grid.RowCount}, visible: {grid.VisibleRowCount}, first: {grid.FirstVisibleRow}");
// Navigate to a specific cell before reading a tooltip or checkbox
grid.SetCurrentCell(3, "STATUS");
string tooltip = grid.GetCellTooltip(3, "STATUS");
bool flagged = grid.GetCellCheckBoxValue(3, "CRITICAL");
// Get currently selected rows (e.g. after SelectAll)
grid.SelectAll();
var selected = grid.SelectedRows; // IReadOnlyList<int>
Handling popups
// Works for both GuiMessageWindow (pure dialogs) and GuiModalWindow (form dialogs)
var popup = session.GetActivePopup();
if (popup != null)
{
Log($"[{popup.MessageType}] {popup.Title}: {popup.Text}");
// Option 1 – standard buttons
popup.ClickOk(); // or popup.ClickCancel()
// Option 2 – custom button by partial text
popup.ClickButton("Continue");
// Option 3 – inspect all buttons
foreach (var btn in popup.GetButtons())
Log(btn.Text);
}
Tabs, toolbars, and menus
// Select a tab
session.TabStrip("wnd[0]/usr/tabsTABSTRIP").SelectTab(1);
// Press a toolbar button by SAP function code
session.Toolbar().PressButtonByFunctionCode("BACK");
// Navigate a menu
session.Menu("wnd[0]/mbar/menu[4]/menu[2]").Select();
Tree controls
var tree = session.Tree("wnd[0]/usr/cntlTREE/shellcont/shell");
tree.ExpandNode("ROOT");
foreach (var key in tree.GetChildNodeKeys("ROOT"))
Log($"{key}: {tree.GetNodeText(key)}");
tree.SelectNode("CHILD01");
tree.DoubleClickNode("CHILD01");
Multi-session workflows
var sap = SapGuiClient.Attach();
// Get existing sessions
var session0 = sap.GetSession(connectionIndex: 0, sessionIndex: 0);
var session1 = sap.GetSession(connectionIndex: 0, sessionIndex: 1);
// Open a new parallel session on demand
session0.CreateSession();
// Then retrieve it:
var newSession = sap.Application
.GetFirstConnection()
.GetSessions()
.Last();
Reactive automation with session events
StartMonitoring() starts a lightweight background thread that raises .NET events when the session state changes - useful for logging round-trips or detecting abends without polling in your workflow loop.
var session = SapGuiClient.Attach().Session;
session.StartRequest += (_, e) =>
Log($"Round-trip starting");
session.EndRequest += (_, e) =>
Log($"Round-trip done - FunctionCode={e.FunctionCode}");
session.Change += (_, e) =>
Log($"Round-trip done - [{e.MessageType}] {e.Text} FunctionCode={e.FunctionCode}");
session.AbapRuntimeError += (_, e) =>
throw new Exception($"ABAP abend: {e.Message}");
session.Destroy += (_, _) =>
Log("SAP session closed", LogLevel.Warn);
// StartMonitoring tries a COM event sink first (no polling thread).
// Falls back to polling (500 ms) if the COM sink cannot connect.
session.StartMonitoring(pollMs: 500);
// ... run your automation ...
session.StopMonitoring();
Connect to a new SAP system
var app = GuiApplication.Attach();
var connection = app.OpenConnection("PRD - Production"); // SAP Logon Pad entry name
var session = connection.GetFirstSession();
// Or quickly get whichever session has focus
var active = app.ActiveSession;
SSO / Unattended login
Use LaunchWithSso when the robot must start SAP itself (e.g. unattended UiPath jobs).
It starts saplogon.exe if it isn't already running, opens the connection via SSO
(no credential dialog), and blocks until the session is ready.
// SapGuiClient and GuiSession both implement IDisposable -
// their COM references are released deterministically when the block exits.
using var sap = SapGuiClient.LaunchWithSso("PRD - Production");
// sap.Session creates a new GuiSession wrapper each time it is called.
// Assign it to a local variable (with 'using' if you want deterministic COM release).
// Disposing 'session' releases only the .NET RCW - the SAP session itself
// remains open inside SAP GUI. Calling sap.Session again returns a fresh
// wrapper for the same live SAP session.
using var session = sap.Session;
// Clear any post-login popups (system messages, license notices, etc.)
int dismissed = session.DismissPostLoginPopups();
// Now automate normally
session.StartTransaction("MM60");
Existing session already open
When the same user is already logged on to the same SAP system, SAP Logon shows a
"License information for multiple logons" dialog that is owned by saplogon.exe
itself — outside the SAP scripting API — and blocks the new connection from completing.
Use the reuseExistingSession parameter to control this:
// true → skip opening a new connection; return a client wrapping the existing session
using var sap = SapGuiClient.LaunchWithSso("PRD - Production", reuseExistingSession: true);
// false (default) → throw InvalidOperationException if a session is already open,
// so the caller can decide what to do (attach, close, or abort)
using var sap = SapGuiClient.LaunchWithSso("PRD - Production"); // throws if already logged on
DismissPostLoginPopups handles (in order):
- Multiple Logon / User already logged on — clicks Continue (keeps both sessions alive)
- License expiration warnings — clicks OK
- System message / Message of the Day banners — clicks OK
- Any single-button info dialog — presses that button
- Unrecognised multi-button dialogs are left untouched
Waiting for SAP to finish
After navigation steps that trigger a server round-trip, call WaitReady() to block until SAP is done:
session.PressExecute();
session.WaitReady(timeoutMs: 30_000); // default: 30 seconds, polls every 200 ms
if (session.Statusbar().IsError)
throw new Exception($"SAP error: {session.Statusbar().Text}");
Resilient automation: retry and waiting
SAP GUI is timing-sensitive. Network latency, slow ABAP reports, and post-navigation busy pulses all cause flaky automation when you interact with the screen too early. The wrapper provides five methods to handle this:
| Method | Use when |
|---|---|
WaitReady(timeoutMs) |
Simple busy-flag poll after navigation |
WaitForReadyState(timeoutMs, pollMs, settleMs) |
Stricter: also waits for a settle period and verifies the main window is reachable |
ElementExists(id, timeoutMs) |
Poll until a specific component appears before touching it |
WaitUntilHidden(id, timeoutMs) |
Poll until a loading spinner or progress dialog disappears |
WithRetry(maxAttempts, delayMs).Run(…) |
Re-execute a block if it raises SapComponentNotFoundException or TimeoutException |
WaitForReadyState – stricter wait
Use WaitForReadyState instead of WaitReady after screen transitions that trigger a brief second busy pulse:
session.StartTransaction("/nMM60");
// Verifies IsBusy is false, adds a 300 ms settle, then confirms the main window is reachable.
session.WaitForReadyState(timeoutMs: 15_000, settleMs: 300);
ElementExists – wait for a component to appear
// Don't call TextField() until the field is actually on screen.
const string reportField = "wnd[0]/usr/ctxtSELWERKS-LOW";
if (!session.ElementExists(reportField, timeoutMs: 8_000))
throw new Exception("Selection screen did not load in time.");
session.TextField(reportField).Text = plant;
WaitUntilHidden – wait for a spinner or dialog to close
// Wait out a 'Please wait…' processing dialog before reading the result.
const string spinner = "wnd[1]/usr/txtMESSAGE";
session.PressExecute();
session.WaitUntilHidden(spinner, timeoutMs: 60_000);
session.WaitForReadyState(timeoutMs: 10_000);
WithRetry – resilient execution block
WithRetry retries on SapComponentNotFoundException (slow screen loads) and TimeoutException (session still busy). It never retries on SapGuiNotFoundException - that is a fatal setup error and should propagate immediately.
// Wrap any navigation + field-access block that may race on slow networks.
session.WithRetry(maxAttempts: 3, delayMs: 500).Run(() =>
{
session.StartTransaction("/nMM60");
session.WaitForReadyState(timeoutMs: 15_000);
session.TextField("wnd[0]/usr/ctxtSELWERKS-LOW").Text = plant;
session.PressExecute();
session.WaitForReadyState(timeoutMs: 30_000);
});
Return a value from the protected block with Run<T>:
string status = session.WithRetry(maxAttempts: 3, delayMs: 400).Run(() =>
{
session.WaitReady(timeoutMs: 10_000);
return session.Statusbar().Text;
});
Combined pattern - navigate, wait, read with retry
using var sap = SapGuiClient.Attach();
var session = sap.Session;
const string tableField = "wnd[0]/usr/ctxtDATABROWSE-TABLENAME";
// 1. Navigate with retry in case the first attempt races
session.WithRetry(maxAttempts: 3, delayMs: 400).Run(() =>
{
session.StartTransaction("/nSE16");
session.WaitForReadyState(timeoutMs: 10_000);
// Confirm the expected field is present before continuing
if (!session.ElementExists(tableField, timeoutMs: 5_000))
throw new SapComponentNotFoundException(tableField);
});
// 2. Interact only after the page is confirmed ready
session.TextField(tableField).Text = "MARA";
session.PressExecute();
session.WaitForReadyState(timeoutMs: 30_000);
// 3. Wait for a progress dialog to disappear if one appears
bool hadSpinner = session.WaitUntilHidden("wnd[1]", timeoutMs: 60_000);
if (hadSpinner) Log("Progress dialog closed.");
var grid = session.GridView("wnd[0]/usr/cntlGRID/shellcont/shell");
Log($"Rows returned: {grid.RowCount}");
Logging
The wrapper is silent by default - zero overhead when no logger is configured.
Three overloads of Attach and LaunchWithSso accept a logger:
| Overload | When to use |
|---|---|
Attach() / LaunchWithSso(system) |
No logging needed |
Attach(ILogger) / LaunchWithSso(system, ILogger) |
ASP.NET Core DI, Serilog/NLog via MEL adapter |
Attach(SapLogAction) / LaunchWithSso(system, SapLogAction) |
UiPath Log(), Serilog static Log, console — no MEL dependency |
Log levels emitted
| Level | Events |
|---|---|
| Debug | Every FindById / FindByIdDynamic call (path logged) |
| Information | StartTransaction, ExitTransaction, session open/close, LaunchWithSso progress, popup dismissed |
| Warning | Popup detected, retry attempt, WaitReady/WaitForReadyState near timeout |
| Error | All thrown exceptions before they propagate |
UiPath – route SAP logs to UiPath's Log() action
No NuGet dependency on Microsoft.Extensions.Logging needed in your workflow:
// All levels — each SAP level maps to the matching UiPath LogLevel
using var sap = SapGuiClient.Attach(
logAction: (level, msg, ex) => Log(
$"[SAP/{level}] {msg}",
level switch
{
SapLogLevel.Error => LogLevel.Error,
SapLogLevel.Warning => LogLevel.Warn,
SapLogLevel.Debug => LogLevel.Trace,
_ => LogLevel.Info,
}));
// Warning and Error only — less noise in production runs
using var sap = SapGuiClient.Attach(
logAction: (level, msg, ex) => Log($"[SAP/{level}] {msg}"),
minLevel: SapLogLevel.Warning);
// With LaunchWithSso (unattended jobs):
using var sap = SapGuiClient.LaunchWithSso(
"PRD - Production",
reuseExistingSession: true,
logAction: (level, msg, ex) => Log(
$"[SAP/{level}] {msg}",
level switch
{
SapLogLevel.Error => LogLevel.Error,
SapLogLevel.Warning => LogLevel.Warn,
SapLogLevel.Debug => LogLevel.Trace,
_ => LogLevel.Info,
}),
minLevel: SapLogLevel.Warning);
ASP.NET Core / Serilog / NLog (via MEL adapter)
// ILogger injected by DI, or created via LoggerFactory
using var sap = SapGuiClient.Attach(logger);
// Or with LaunchWithSso:
using var sap = SapGuiClient.LaunchWithSso("PRD - Production", logger);
Serilog static Log class (no MEL adapter)
using var sap = SapGuiClient.Attach(logAction: (level, msg, ex) =>
Serilog.Log.Write(level switch
{
SapLogLevel.Debug => Serilog.Events.LogEventLevel.Debug,
SapLogLevel.Warning => Serilog.Events.LogEventLevel.Warning,
SapLogLevel.Error => Serilog.Events.LogEventLevel.Error,
_ => Serilog.Events.LogEventLevel.Information,
}, ex, "{SapMessage}", msg));
Console (quick debugging)
using var sap = SapGuiClient.Attach(
logAction: (level, msg, ex) =>
Console.WriteLine($"[{level}] {msg}{(ex is null ? "" : " – " + ex.Message)}"));
Pre-flight health check
Before running automation in a robot, call HealthCheck() to produce a structured report - or EnsureHealthy() for a single fail-fast call.
// Non-throwing: inspect findings yourself
var result = SapGuiClient.HealthCheck();
if (!result.IsHealthy)
throw new InvalidOperationException(result.FailureSummary);
foreach (var line in result.Findings)
Log(line); // each line is prefixed OK: / WARN: / FAIL:
// Throwing shorthand - equivalent to the above
SapGuiClient.EnsureHealthy();
// Then proceed normally
using var sap = SapGuiClient.Attach();
sap.Session.StartTransaction("SE16");
Checks performed (in order):
| # | Check | FAIL condition |
|---|---|---|
| 1 | saplogon.exe is running |
Process not found - SAP GUI not installed / not started |
| 2 | Scripting API accessible via Windows ROT | Scripting disabled or ROT registration failed |
| 3 | At least one active connection | No SAP system logged on |
| 4 | At least one active session | Connection exists but no window open |
| 5 | Session info readable (user / system / client) | Session is mid-logon or in an error state |
Exception types
| Exception | When thrown |
|---|---|
SapGuiNotFoundException |
SAP GUI is not running or scripting is disabled |
SapComponentNotFoundException |
FindById path does not match any component |
InvalidCastException |
FindById<T> found a component but it's a different type |
InvalidOperationException |
LaunchWithSso could not open an SSO connection, or a session for the system is already open and reuseExistingSession is false |
TimeoutException |
WaitReady timed out while session was busy, or LaunchWithSso found no ready session in time |
VKey reference
| VKey | Key | Action |
|---|---|---|
| 0 | Enter | Confirm / navigate forward |
| 3 | F3 | Back - one screen back within the transaction |
| 4 | F4 | Input Help / Possible Values |
| 8 | F8 | Execute - runs the current report / selection screen |
| 11 | Ctrl+S | Save |
| 12 | F12 | Cancel - discards changes and closes the current screen |
| 15 | Shift+F3 | Exit - steps back to the previous menu level |
| 71 | Ctrl+Home | Scroll to top |
| 82 | Ctrl+End | Scroll to bottom |
Shortcuts: PressEnter() · PressBack() · PressExecute() · ExitTransaction()
API coverage
Component wrappers
| SAP COM Class | Wrapper | Key members |
|---|---|---|
SapGuiClient |
✅ | Attach(), Attach(logAction:, minLevel:, logger:), LaunchWithSso(system, reuseExistingSession, …, logAction:, minLevel:, logger:), HealthCheck(), EnsureHealthy(), Session, GetSession(), GetConnections(), Dispose() |
GuiApplication |
✅ | Version, ActiveSession, GetConnections(), OpenConnection() |
GuiConnection |
✅ | Description, GetSessions() |
GuiSession |
✅ | ActiveWindow, FindById, StartTransaction, ExitTransaction, CreateSession, PressEnter/Back/Execute, SendVKey, GetActivePopup, WaitReady, WaitForReadyState, ElementExists, WaitUntilHidden, WithRetry, DismissPostLoginPopups, StartMonitoring (COM sink → polling fallback), StartRequest, EndRequest, Change, Destroy, AbapRuntimeError, UserArea, ScrollContainer, Calendar, HtmlViewer, Shell, Dispose |
GuiMainWindow |
✅ | Title, IsMaximized, SendVKey, HardCopy, Maximize, Iconify, Restore, Close |
GuiTextField |
✅ | Text, DisplayedText, MaxLength, IsReadOnly, IsRequired, IsOField, CaretPosition |
GuiPasswordField |
✅* | Text set (write-only) - falls back to GuiTextField |
GuiButton |
✅ | Text, Press() |
GuiLabel |
✅ | Text (read-only) |
GuiCheckBox |
✅ | Selected |
GuiRadioButton |
✅ | Selected |
GuiComboBox |
✅ | Key, Value, ShowKey, Entries |
GuiStatusbar |
✅ | Text, MessageType, IsError / IsWarning / IsSuccess |
GuiTable |
✅ | RowCount, ColumnCount, FirstVisibleRow, VisibleRowCount, ScrollToRow, CurrentCellRow/Column, GetCellValue, SetCellValue, GetVisibleRows, SelectRow |
GuiGridView |
✅ | RowCount, FirstVisibleRow, VisibleRowCount, ColumnNames, CurrentCellRow/Column, SetCurrentCell, SelectedRows, GetCellValue, GetCellTooltip, GetCellCheckBoxValue, GetSymbolsForCell, GetRows, PressEnter, ClickCell, PressToolbarButton, SelectAll |
GuiTree |
✅ | ExpandNode, CollapseNode, SelectNode, DoubleClickNode, GetNodeText, GetItemText, GetNodeType, GetChildNodeKeys, GetAllNodeKeys, NodeContextMenu, SelectedNode |
GuiTabStrip / GuiTab |
✅ | TabCount, GetTabs(), SelectTab(int), GetTabByName(), Tab.Select() |
GuiToolbar |
✅ | ButtonCount, PressButton(int), PressButtonByFunctionCode(string), GetButtonTooltip(int) |
GuiMenubar / GuiMenu |
✅ | Count, SelectItem(GuiSession, string), GetChildren() |
GuiContextMenu |
✅ | SelectByFunctionCode(), GetItemTexts(), Close() |
GuiMessageWindow |
✅ | Text, Title, MessageType, ClickOk(), ClickCancel(), ClickButton(), GetButtons() |
GuiModalWindow |
🔶 | Via session.GetActivePopup() - children accessible with findById |
GuiUserArea |
✅ | FindById(relativeId), FindById<T>(relativeId) - address children with relative IDs |
GuiScrollContainer |
✅ | VerticalScrollbar, HorizontalScrollbar (Position, Minimum, Maximum, PageSize), ScrollToTop() |
GuiShell (generic) |
✅ | SubType - typed fallback for unrecognised shell variants |
GuiCalendar |
✅ | FocusedDate, SetDate(DateTime), GetSelectedDate() |
GuiHTMLViewer |
✅ | BrowserHandle, FireSapEvent(event, param1, param2) |
Legend: ✅ typed wrapper · 🔶 accessible via dynamic / base class · ❌ not yet implemented
Events
GuiSession exposes five .NET events, activated by calling StartMonitoring():
| Event | When it fires |
|---|---|
StartRequest |
At the start of a server round-trip (session transitions to busy) |
EndRequest |
At the end of a server round-trip, before Change (includes FunctionCode when COM sink is active) |
Change |
After every server round-trip (IsBusy → idle transition) |
Destroy |
When the session becomes unreachable (window closed / connection lost) |
AbapRuntimeError |
When a status bar message type A (abend) is detected after a round-trip |
Since v0.8.0,
StartMonitoring()connects a true COM event sink when the SAP version supports it, with automatic fallback to polling. No extra configuration needed.
AI Agent & MCP Server
SapGui.Wrapper.Mcp is a dotnet tool that exposes SAP GUI scripting as 18 MCP tools consumable by any MCP-capable host (Claude Desktop, VS Code Copilot Agent, Cursor, etc.). It ships with a built-in screen observation layer and semantic action façade — no separate package needed.
Install
# Global dotnet tool — resolves to sapgui-mcp on PATH
dotnet tool install --global SapGui.Wrapper.Mcp
# Or as a local project tool (dotnet tool restore will activate it)
dotnet tool install SapGui.Wrapper.Mcp
Claude Desktop
Add to claude_desktop_config.json (%APPDATA%\Claude\claude_desktop_config.json on Windows):
{
"mcpServers": {
"sapgui": {
"command": "dotnet",
"args": ["tool", "run", "sapgui-mcp", "--", "--connection", "COR\\PRD\\100"]
}
}
}
The --connection argument format is <SapLogonPadEntry>\\<SystemId>\\<Client>.
Omit it to start the server without a session; the agent can then call sap_connect to connect at runtime.
VS Code Copilot (Agent Mode)
Create .vscode/mcp.json in your workspace:
{
"inputs": [
{
"id": "sap-connection",
"type": "promptString",
"description": "SAP connection string (e.g. COR\\PRD\\100)",
"default": "COR\\PRD\\100"
}
],
"servers": {
"sapgui": {
"type": "stdio",
"command": "dotnet",
"args": ["tool", "run", "sapgui-mcp", "--", "--connection", "${input:sap-connection}"]
}
}
}
Available tools (18)
| Tool | Description |
|---|---|
sap_connect |
Connect to a running SAP session by connection string and client |
sap_disconnect |
Release the session |
sap_scan_screen |
Full JSON snapshot of the current screen (fields, buttons, grids, tabs, trees, statusbar) |
sap_take_screenshot |
Base-64 PNG of the current screen |
sap_set_field |
Set a field value by label or component ID |
sap_get_field |
Read a field value by label or component ID |
sap_clear_field |
Clear a field by label or component ID |
sap_click_button |
Click a button by text, tooltip, or SAP function code |
sap_press_key |
Send a VKey (Enter, F3, F8, Ctrl+S, …) |
sap_start_transaction |
Navigate to a transaction code (subject to guardrails) |
sap_select_menu |
Click a menu item by path |
sap_select_tab |
Select a tab by index or label |
sap_read_grid |
Read all rows from an ALV grid |
sap_select_grid_row |
Select a row in an ALV grid by index |
sap_open_grid_row |
Double-click a grid row to open its detail screen |
sap_handle_popup |
Dismiss or click a button in a popup dialog |
sap_expand_tree_node |
Expand a tree node by key |
sap_select_tree_node |
Select a tree node by key |
sap_wait_and_scan |
Wait for SAP to become ready then return a fresh screen snapshot |
Security: read-only mode and blocked transactions
The server ships with 12 dangerous transaction codes blocked by default (SE38, SE37, SM49, RZ10, SCC4, SU01, PFCG, SCC5, SCC1, SM59, SM50, RZ04).
Override via appsettings.json placed next to the tool binary, or via environment variables prefixed SAP__:
{
"Sap": {
"ReadOnlyMode": true,
"BlockedTransactions": ["SE38", "SE37", "SM49", "RZ10"]
}
}
ReadOnlyMode: true restricts to observation-only tools (sap_scan_screen, sap_take_screenshot, sap_read_grid, sap_wait_and_scan). All write/navigation tools are rejected with a clear error message that the agent sees. The BlockedTransactions list is enforced regardless of ReadOnlyMode.
Building the NuGet package
cd SapGui.Wrapper
dotnet pack -c Release -o ../nupkg
After packing, an SBOM (SapGui.Wrapper-{version}-sbom.cdx.json) is generated automatically alongside the .nupkg via dotnet CycloneDX. Restore the tool once if it is not yet installed:
dotnet tool restore # reads .config/dotnet-tools.json
To sign the packages with a self-signed certificate:
.\scripts\New-SigningCert.ps1
# follow the on-screen instructions and record the printed thumbprint
Verify a signed package:
dotnet nuget verify nupkg\SapGui.Wrapper.1.0.0.nupkg --certificate-fingerprint <thumbprint>
Project structure
SapGui.Wrapper/
├── SapGuiClient.cs ← main entry point (Attach / LaunchWithSso / HealthCheck / GetSession)
├── SapGuiHelper.cs ← static one-liner helpers for UiPath Invoke Code
├── SapLogging.cs ← SapLogLevel enum, SapLogAction delegate, SapLogger bridge
├── HealthCheckResult.cs ← HealthCheckResult record (IsHealthy, Findings, FailureSummary)
├── Exceptions.cs
├── GlobalUsings.cs
├── Polyfills.cs ← IsExternalInit shim for net461 record support
│
├── Com/
│ ├── SapRot.cs ← ROT access (SapROTWr + P/Invoke fallback)
│ ├── ComConnectionPoint.cs ← IConnectionPoint / IConnectionPointContainer COM interfaces
│ └── GuiSessionComSink.cs ← COM-visible dispatch sink for SAP session events
│
├── RetryPolicy.cs ← configurable retry (WithRetry, Run, Run<T>)
│
├── Core/
│ ├── GuiComponent.cs ← base wrapper (late-binding helpers)
│ ├── GuiApplication.cs ← GetConnections, OpenConnection, ActiveSession
│ ├── GuiConnection.cs ← GetSessions
│ ├── GuiSession.cs ← FindById, typed finders, events, WaitForReadyState, ElementExists, WaitUntilHidden, WithRetry, DismissPostLoginPopups, Dispose
│ ├── GuiMainWindow.cs ← SendVKey, HardCopy, Maximize
│ └── SessionEvents.cs ← SessionChangeEventArgs, SessionEventMonitor
│
├── Elements/
│ ├── GuiTextField.cs
│ ├── GuiButton.cs
│ ├── GuiComboBox.cs
│ ├── GuiCheckBox.cs / GuiRadioButton.cs
│ ├── GuiLabel.cs
│ ├── GuiStatusbar.cs
│ ├── GuiTable.cs ← FirstVisibleRow, ScrollToRow, CurrentCell
│ ├── GuiGridView.cs ← SetCurrentCell, SelectedRows, GetCellTooltip, …
│ ├── GuiTree.cs
│ ├── GuiTabStrip.cs
│ ├── GuiToolbar.cs
│ ├── GuiMenu.cs ← GuiMenubar, GuiMenu, GuiContextMenu
│ └── GuiMessageWindow.cs
│
└── Enums/
└── SapComponentType.cs
License
MIT
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0-windows7.0 is compatible. net9.0-windows was computed. net10.0-windows was computed. |
| .NET Framework | net461 is compatible. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
-
.NETFramework 4.6.1
- Microsoft.Extensions.Logging.Abstractions (>= 8.0.0)
-
net8.0-windows7.0
- Microsoft.Extensions.Logging.Abstractions (>= 8.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/) and this project adheres to [Semantic Versioning](https://semver.org/).
## [1.1.0] – 2026-04-03 — `SapGui.Wrapper.Mcp` (new package)
### New package
- **`SapGui.Wrapper.Mcp` 1.1.0** — MCP stdio server (`sapgui-mcp` dotnet tool) that exposes SAP GUI automation as 18 typed MCP tools consumable by any MCP-capable host (Claude Desktop, VS Code Copilot Agent, Cursor, and others). Includes a screen observation layer (typed snapshots, JSON context) and semantic action façade (fuzzy field/button resolution via label) bundled as an internal dependency — no separate package needed. All SAP COM calls are marshalled onto a dedicated STA thread. Includes built-in guardrails: 12 dangerous transactions blocked by default and a `ReadOnlyMode` flag that restricts the server to observation-only tools.
### Infrastructure
- `Directory.Build.props` — single source of truth for `SharedVersion`, shared NuGet metadata (Authors, License, URLs), and compiler settings (`LangVersion latest`, `Nullable enable`, `ImplicitUsings enable`, `Deterministic true`) across all projects.
- CI (`ci.yml`) — dry-run `dotnet pack` for both packages on every PR.
- Publish (`publish.yml`) — reads version from `Directory.Build.props`, builds, packs, and pushes both packages on every push to `main`.
## [1.0.0] – 2026-03-11
### First stable release
SapGui.Wrapper is now considered production-ready for general use. All core APIs, element wrappers, events, retry/wait mechanisms, logging, health checks, and SSO support have been validated through extensive integration testing and iterative refinement across the 0.x series.
### Breaking changes
- **`GuiTable.VerticalScrollbarPosition` removed** — this property was deprecated in 0.5.0 in favour of `FirstVisibleRow`. Use `FirstVisibleRow` instead.
- **`GuiComboBox.SetKeyAndFireEvent` removed** (since 0.8.4) — use `cb.Key = value` instead.
### Summary of capabilities (since 0.0.0)
- **20+ typed element wrappers** — `GuiTextField`, `GuiButton`, `GuiGridView`, `GuiTable`, `GuiTree`, `GuiComboBox`, `GuiCheckBox`, `GuiRadioButton`, `GuiTabStrip`, `GuiToolbar`, `GuiMenu`, `GuiStatusbar`, `GuiMessageWindow`, `GuiCalendar`, `GuiHTMLViewer`, `GuiScrollContainer`, `GuiUserArea`, `GuiShell`, `GuiLabel`, and more.
- **Session management** — `SapGuiClient.Attach()`, `LaunchWithSso()`, `HealthCheck()`, `EnsureHealthy()`, `GetSession()`, multi-connection/multi-session support.
- **Resilient automation** — `WaitReady`, `WaitForReadyState`, `ElementExists`, `WaitUntilHidden`, `WithRetry` (retries on `SapComponentNotFoundException` and `TimeoutException`).
- **COM event sink** — `StartMonitoring()` connects a true COM event sink (with polling fallback): `StartRequest`, `EndRequest`, `Change`, `Destroy`, `AbapRuntimeError`.
- **Logging** — `ILogger` and `SapLogAction` delegate overloads, zero overhead when unconfigured.
- **Post-login popup handling** — `DismissPostLoginPopups` handles multiple-logon dialogs, license warnings, system messages.
- **NuGet hardening** — deterministic builds, SourceLink, SBOM generation, package signing script.
- **COM lifecycle safety** — all intermediate COM objects released via `try/finally` + `Marshal.ReleaseComObject`; `SapGuiClient` and `GuiSession` are `IDisposable`.
- **GitHub Pages documentation** — full API reference (auto-generated from XML docs via DocFX), Getting Started, Common Patterns, Logging, Resilient Automation, Health Check, Session Events, Best Practices, Troubleshooting, and FAQ articles published at `https://alexrivax.github.io/SapGui.Wrapper`.
## [0.9.2] – 2026-03-10
### Changed
- **`SapGuiClient.LaunchWithSso` — new `reuseExistingSession` parameter** — Instead of attempting to dismiss the SAP Logon "License information for multiple logons" dialog programmatically (which proved unreliable because the dialog is a native Win32 window owned by `saplogon.exe`, outside the SAP scripting API), `LaunchWithSso` now detects an existing open session for the requested system **before** calling `OpenConnection`, preventing the dialog from ever appearing. Pass `reuseExistingSession: true` to reuse the existing session; omit it (default `false`) to receive a clear `InvalidOperationException` with instructions.
### Removed
- All Win32 dialog-dismissal code (`EnumWindows`, `keybd_event`, `SetForegroundWindow`, background watcher task) introduced in 0.9.1 — replaced by the pre-flight session check above.
## [0.9.1] – 2026-03-10
### Bug fix
- **`DismissPostLoginPopups` now handles the "License information for multiple logons" dialog** — When a user is already logged on to the same SAP system, SAP shows a radio-button dialog titled _"License information for multiple logons"_ before the standard session-management popup. The dialog was not matched by the existing heuristics and was left untouched, blocking automation. A dedicated check is now inserted before the generic `MULTIPLE LOGON` branch: when the title contains `"License information for multiple logons"`, `DismissPostLoginPopups` presses Enter (VKey 0), accepting the default option _"Continue without ending other logon"_ — the non-destructive, license-safe choice.
## [0.9.0] – 2026-03-09
### Added
- **`SapGuiClient.HealthCheck()`** — static, never-throws pre-flight method that returns a `HealthCheckResult` record (`IsHealthy`, `Findings`, `FailureSummary`). Runs five ordered checks: SAP GUI process is running → scripting API accessible via ROT → at least one connection exists → at least one session exists → session info (user / system / client) is readable. Each finding is prefixed `OK:`, `WARN:`, or `FAIL:` for easy filtering. The temporary COM reference obtained during the check is released in a `finally` block.
- **`SapGuiClient.EnsureHealthy()`** — convenience throwing variant: calls `HealthCheck()` and raises `InvalidOperationException` with all `FAIL:` lines when any check fails. Designed for fail-fast workflows.
- **`HealthCheckResult`** record — `IsHealthy` (`bool`), `Findings` (`IReadOnlyList<string>`), `FailureSummary` (FAIL lines joined by newline), `ToString()` (all findings).
- **`Polyfills.cs`** — internal `System.Runtime.CompilerServices.IsExternalInit` shim (guarded by `#if !NET5_0_OR_GREATER`) enabling C# 9 `record` types on net461 with zero runtime cost.
### NuGet / build hardening
- **Transitive dependencies pinned** — `Microsoft.Build.Tasks.Git` and `Microsoft.SourceLink.Common` are now explicitly listed at `8.0.0` in the `.csproj`, matching the already-present `Microsoft.SourceLink.GitHub 8.0.0`. This produces a fully deterministic, auditable NuGet dependency graph required by enterprise artifact repositories (Artifactory, Azure Artifacts with policy enforcement).
- **SBOM on every pack** — `GenerateSbom` AfterPack MSBuild target invokes `dotnet CycloneDX` and writes `SapGui.Wrapper-{version}-sbom.cdx.json` alongside the `.nupkg`. The tool is pinned to `cyclonedx 3.0.8` in `.config/dotnet-tools.json`. Run `dotnet tool restore` once before the first pack.
- **Package signing script** — `scripts/New-SigningCert.ps1` creates a self-signed code-signing certificate (RSA-3072, SHA-256, 5-year validity), exports it as a PFX, and signs all `.nupkg` files in `nupkg/` via `dotnet nuget sign` with a DigiCert timestamp. `*.pfx` and `*.p12` are now excluded by `.gitignore`.
## [0.8.7] – 2026-03-09
### Added
- **`RetryPolicy`** — new class returned by `session.WithRetry(maxAttempts, delayMs)`. Call `.Run(action)` or `.Run<T>(func)` to execute an operation with automatic retries on `SapComponentNotFoundException` (slow screen loads) and `TimeoutException` (session still busy). `SapGuiNotFoundException` is never retried — it is a fatal setup error.
- **`GuiSession.WithRetry(maxAttempts, delayMs)`** — fluent entry point that creates a `RetryPolicy` scoped to the current session.
- **`GuiSession.WaitForReadyState(timeoutMs, pollMs, settleMs)`** — more reliable alternative to `WaitReady`. After the busy flag clears it waits an additional settle period, then verifies the main window COM object is still accessible. Catches the brief second busy pulse that `WaitReady` can miss during screen transitions.
- **`GuiSession.ElementExists(id, timeoutMs, pollMs)`** — polls until a component ID is accessible or the timeout elapses. Returns `bool`. Use instead of a try/catch around `FindById` when you need an explicit wait.
- **`GuiSession.WaitUntilHidden(id, timeoutMs, pollMs)`** — polls until a component ID is no longer accessible. Returns `bool`. Use to wait out loading spinners or processing dialogs.
## [0.8.6] – 2026-03-09
### Bug fixes
- **`GuiTable.ColumnCount` always returned 0** — `GuiTableControl` exposes columns via a `Columns` collection, not a flat `ColumnCount` integer property. `GetInt("ColumnCount")` silently caught `DISP_E_UNKNOWNNAME` and returned 0. Fixed to read `Columns.Count` via the collection object.
- **`GuiTable.FirstVisibleRow` always returned 0 / `ScrollToRow` threw `DISP_E_UNKNOWNNAME`** — Both used dotted property paths (`"VerticalScrollbar.Position"`) passed to `InvokeMember`, which does not resolve nested COM objects. Fixed to retrieve the `VerticalScrollbar` object first, then get/set `Position` on it.
- **`GuiTable.GetCellValue` always returned empty string** — `GetCellRaw` was navigating `Rows.Item(row).Item(col)`, which does not work on `GuiTableControl`. Fixed to call `GetCell(row, col)` directly on the table COM object.
- **`GuiTable.GetVisibleRows` returned all rows instead of visible rows** — Loop iterated `RowCount` (total) starting at index 0. SAP only populates COM cells for the currently visible viewport; off-screen rows always return empty values. Fixed to iterate `VisibleRowCount` rows beginning at `FirstVisibleRow`.
## [0.8.5] – 2026-03-09
### Bug fix
- **`session.Table()` failed with `GuiTableControl` type** — Some SAP systems return `"GuiTableControl"` from the COM `Type` property instead of `"GuiTable"` for the same classic ABAP table control. `WrapComponent` now maps both `GuiTable` and `GuiTableControl` to `GuiTable`, so `session.Table(id)` works regardless of which type string the system reports.
## [0.8.4] – 2026-03-09
### Breaking change
- **`GuiComboBox.SetKeyAndFireEvent` removed** — SAP GUI does not expose `FireSelectEvent` on `GuiComboBox`; the method was equivalent to setting `Key` directly. Use `cb.Key = value` followed by `session.WaitReady()` or `session.SendVKey(0)` if field-level PAI validation is needed.
## [0.8.3] – 2026-03-09
### Bug fix
- **`GuiMainWindow.IsMaximized` always returned `false`** — The SAP COM `IsMaximized` property is not consistently updated after `Maximize()` / `Restore()` across SAP GUI versions. Replaced the COM property read with a Win32 `IsZoomed(HWND)` P/Invoke call, using the HWND already exposed by SAP's `GuiFrameWindow.Handle`. Also added `GuiMainWindow.Handle` (`IntPtr`) as a new public property.
## [0.8.2] – 2026-03-09
### Bug fixes
- **`GetBool` silent VARIANT_BOOL cast failure** — `GuiComponent.GetBool` was casting the COM return value with `(bool)` directly. SAP's COM layer returns `VARIANT_BOOL` as a boxed `short` (-1 = true, 0 = false); the cast threw `InvalidCastException`, which the `catch` swallowed and returned `false`. Changed to `Convert.ToBoolean(val)`, which handles `bool`, `short`, and `int` correctly. This was a silent bug affecting every boolean property across the wrapper: `IsMaximized`, `IsBusy`, `IsReadOnly`, `IsRequired`, `IsOField`, `IsModified`, `Changeable`, `DisabledByServer`, `ShowKey`, `IsDialog`, and all `GetBool` call sites.
### GuiSession — `StartTransaction` documentation
- Expanded XML doc on `StartTransaction(tCode)` to document the `/n` prefix requirement when calling from inside an existing transaction (`"/nSE16"`), the `/o` prefix to open in a new session, and the difference from bare code (only reliable from Easy Access menu).
### UiPath Studio project (UiPathTests)
- `SapGui.Wrapper.Tests/UiPathTests/` is now a standalone **UiPath Studio 2023.10+ project**: added `project.json` (NuGet dependencies, net6.0-windows target) and `Main.xaml` (sequence that calls all 14 tests via `InvokeWorkflowFile`).
- All 14 coded workflow files updated: unified namespace `SapGuiWrapperTests`, added all missing `using` directives (`System`, `System.Collections.Generic`, `System.Linq`, `System.IO`, `System.Threading`, `UiPath.Core`), removed unused UiPath package imports.
- **Test 01** — `GuiConnection.Description` → `GuiConnection.Host` (property was renamed); `application.ActiveSession` wrapped in try/catch with `session.MainWindow().SetFocus()` before the call to handle OS-focus dependency.
- **Test 03** — `StartTransaction("SE16")` → `StartTransaction("/nSE16")` so navigation works when already inside another transaction.
- **Test 11** — `GuiMenubar.GetChildren()` does not exist; replaced with index-based loop using `session.Menu("wnd[0]/mbar/menu[{i}]")` to retrieve top-level `GuiMenu` items.
- `SapGui.Wrapper.Tests.csproj` — added `<Compile Remove="UiPathTests\**\*.cs" />` to exclude UiPath files from the C# build.
## [0.8.1] – 2026-03-07
### Security — COM lifecycle hardening
- `SapRot.GetGuiApplication()` now releases all intermediate COM objects (`rotWrapper`, `sapGuiRaw`, `IRunningObjectTable`, per-iteration `IBindCtx`) via `try/finally` + `Marshal.ReleaseComObject`. No COM references leak after `Attach()` completes.
- `SapGuiClient.Dispose()` now explicitly calls `Marshal.ReleaseComObject(Application.RawObject)` instead of relying on the GC finalizer, returning the COM reference to SAP GUI immediately.
- `GuiSession` now implements `IDisposable`. `Dispose()` calls `StopMonitoring()` first (disconnects COM event sinks and stops any polling thread) then releases the session COM RCW. Safe to use in `using` blocks.
### SapGuiClient — new `LaunchWithSso` factory
- `SapGuiClient.LaunchWithSso(systemDescription, connectionTimeoutMs)` — starts `saplogon.exe` if it is not already running, waits for SAP GUI to register in the Windows ROT, opens the connection by SAP Logon Pad entry name (SSO-configured systems complete without a credential dialog), and polls until a non-busy session is available before returning. Throws `SapGuiNotFoundException`, `InvalidOperationException`, or `TimeoutException` on failure — never silently succeeds with an unusable session.
### GuiSession — new `DismissPostLoginPopups`
- `DismissPostLoginPopups(maxPopups, timeoutMs)` — automatically dismisses post-SSO / post-login dialogs: "User already logged on / Multiple Logon" (clicks **Continue** to keep both sessions alive), license expiration warnings, system message banners, and any single-button information dialog. Unrecognised multi-button dialogs are left untouched. Returns the count of dismissed popups.
## [0.8.0]
### GuiSession — COM event sink (architectural improvement)
- `StartMonitoring()` now first tries to connect a true COM event sink to the SAP session via `IConnectionPointContainer::EnumConnectionPoints` / `IConnectionPoint::Advise`. If the COM sink connects successfully, all five SAP session events are driven by the native SAP COM source — no polling thread is started.
- Falls back automatically to the previous polling-based monitor when the COM object does not support connection points (e.g. older SAP GUI versions).
- `SessionChangeEventArgs.FunctionCode` is now populated (e.g. `"BACK"`, `"EXEC"`) when the COM event sink is active. It remains empty in the polling fallback.
### GuiSession — new `StartRequest` / `EndRequest` events
- Added `StartRequest` event (`StartRequestEventArgs`: `Text`) — fires at the beginning of a server round-trip. When using the COM sink this is a true SAP event; in the polling fallback it fires when `IsBusy` first becomes `true`.
- Added `EndRequest` event (`EndRequestEventArgs`: `Text`, `FunctionCode`, `MessageType`) — fires at the end of a server round-trip, before `Change`. Provides a precise `WaitReady` alternative when combined with `StartMonitoring()`.
### Internal
- Added `Com/ComConnectionPoint.cs` — declares `IConnectionPoint`, `IConnectionPointContainer`, `IEnumConnectionPoints` COM interfaces (standard OLE GUIDs; not present in .NET 6+ BCL).
- Added `Com/GuiSessionComSink.cs` — `[ComVisible]` `IReflect`-based dispatch sink. Routes both name-based and DISPID-based SAP event calls to the correct .NET event raisers.
## [0.7.0]
### GuiScrollContainer (new wrapper)
- Added `GuiScrollContainer` typed wrapper for SAP GUI scroll container controls.
- `VerticalScrollbar` and `HorizontalScrollbar` properties return a `GuiScrollbar` object exposing `Position` (read/write), `Minimum`, `Maximum`, and `PageSize`.
- `ScrollToTop()` convenience method sets the vertical scroll position to its minimum.
- `session.ScrollContainer(id)` typed finder added to `GuiSession`.
### GuiUserArea (new wrapper)
- Added `GuiUserArea` typed wrapper for the dynpro content area (typically `wnd[0]/usr`).
- `FindById(relativeId)` and `FindById<T>(relativeId)` allow child lookup with short relative IDs instead of fully qualified paths.
- `session.UserArea(id)` typed finder added to `GuiSession` (defaults to `"wnd[0]/usr"`).
### GuiCalendar (new wrapper)
- Added `GuiCalendar` typed wrapper for SAP GUI calendar controls.
- `FocusedDate` property returns the focused date as a nullable `DateTime`.
- `SetDate(DateTime)` — sets the focused/selected date.
- `GetSelectedDate()` — returns the currently selected date.
- `session.Calendar(id)` typed finder added to `GuiSession`.
### GuiHTMLViewer (new wrapper)
- Added `GuiHTMLViewer` typed wrapper for embedded HTML viewer controls.
- `BrowserHandle` — Win32 handle of the embedded browser control.
- `FireSapEvent(eventName, param1, param2)` — fires a named SAP event defined inside the embedded HTML page.
- `session.HtmlViewer(id)` typed finder added to `GuiSession`.
### GuiShell (new wrapper)
- Added `GuiShell` as a typed generic fallback wrapper for SAP shell controls whose specific sub-type is not individually wrapped.
- `SubType` property exposes the shell sub-type string (e.g. `"GridView"`, `"TreeView"`, `"Chart"`).
- `session.Shell(id)` typed finder added to `GuiSession`.
- `WrapComponent` now routes `GuiShell` type strings to `GuiShell` instead of falling through to the bare `GuiComponent` base.
## [0.6.0]
### GuiTextField
- Added `DisplayedText` property — returns the formatted/displayed value as shown in SAP GUI (may differ from `Text` on amount or date fields where SAP applies locale-specific formatting).
- Added `IsRequired` property — returns `true` if the field is mandatory (marked with `?`).
- Added `IsOField` property — returns `true` if the field is an output-only screen field.
### GuiMainWindow
- Added `Iconify()` — minimizes the window to the taskbar.
- Added `IsMaximized` property — returns `true` when the window is currently maximised.
### GuiTree
- Added `GetItemText(nodeKey, columnName)` — reads a cell value in a multi-column tree.
- Added `GetAllNodeKeys()` — returns a flat `IReadOnlyList<string>` of every node key in the tree.
- Added `NodeContextMenu(nodeKey)` — opens the right-click context menu for a node.
- Added `GetNodeType(nodeKey)` — returns the node type string (e.g. `"LEAF"`, `"FOLDER"`).
### GuiComboBox
- Added `ShowKey` property — returns `true` when the combo box shows the technical key rather than the description.
- ~~`SetKeyAndFireEvent(key)`~~ — removed in 0.8.4; use `cb.Key = value` instead.
## [0.5.0]
### GuiSession
- Added `ActiveWindow` property — returns the currently active (focused) window without guessing the `wnd[x]` index.
- Added `CreateSession()` — opens a new parallel SAP session on the same connection.
- Added polling-based .NET events: `Change`, `Destroy`, `AbapRuntimeError`. Call `StartMonitoring(pollMs)` / `StopMonitoring()` to activate. `SessionChangeEventArgs` carries `Text`, `FunctionCode`, and `MessageType`; `AbapRuntimeErrorEventArgs` carries `Message`.
### GuiApplication
- Added `ActiveSession` property — shortcut to the currently focused session.
- Added `OpenConnection(description, sync)` — opens a new connection by SAP Logon Pad entry name and returns the resulting `GuiConnection`.
### GuiGridView
- Added `FirstVisibleRow` and `VisibleRowCount` — viewport scroll-awareness properties.
- Added `CurrentCellRow` and `CurrentCellColumn` — focused cell coordinates.
- Added `SetCurrentCell(row, column)` — navigates focus to a cell before reading tooltips, checkboxes, or invoking context menus.
- Added `SelectedRows` — returns currently selected row indices as `IReadOnlyList<int>`.
- Added `GetCellTooltip(row, column)` — reads the tooltip text on a cell.
- Added `GetCellCheckBoxValue(row, column)` — reads boolean checkbox-type cells.
- Added `GetSymbolsForCell(row, column)` — reads icon/symbol column values.
- Added `PressEnter()` — confirms cell input.
### GuiTable
- Added `FirstVisibleRow` — index of the first visible row (replaces `VerticalScrollbarPosition`, removed in 1.0.0).
- Added `VisibleRowCount` — number of rows visible in the current viewport.
- Added `ScrollToRow(row)` — scrolls the table to put the given row at the top.
- Added `CurrentCellRow` and `CurrentCellColumn` — row index and column key of the focused cell.
### README
- Rewritten with an RPA / UiPath Coded Workflow focus: new "Why this wrapper?" section, lead example as a full `CodedWorkflow`, consolidated "Common patterns" section covering all major automation scenarios.
## [0.4.6]
- Added `GuiSession.ExitTransaction()`: exits the current transaction and returns to the SAP Easy Access menu (writes `/n` to the command field and presses Enter).
- Added `GuiSession.PressExecute()`: sends F8 (VKey 8) to the main window, equivalent to clicking the Execute button on a selection screen.
- Fixed `GuiSession.SendVKey()` XML doc comment: corrected VKey-to-action mapping and expanded it to a reference table covering VKey 0/3/4/8/11/12/15.
## [0.4.5]
- Bug fix: `GuiMessageWindow.Text` now returns the actual message body text by reading the inner SAP text fields (`usr/txtMESSTXT1`…`usr/txtMESSTXT4`), joining non-empty lines with a space. Falls back to the window title if no inner fields exist.
- Bug fix: `GuiMessageWindow.Title` and `.Text` were previously identical (both returned the window title bar text).
## [0.4.4]
- Bug fix: `GetActivePopup()` now correctly returns a `GuiMessageWindow` when the popup is a SAP `GuiModalWindow` (e.g. dialog boxes with form fields). Previously it returned `null` because `WrapComponent` maps `GuiModalWindow` to `GuiMainWindow`, causing the typed cast to fail silently.
## [0.4.0]
- Bug fix: SapComponentType.FromString now correctly resolves GuiMenu, GuiContextMenu, GuiApoGrid, GuiCalendar and GuiOfficeIntegration (previously returned Unknown).
- Added GuiSession.RadioButton() typed convenience finder.
- Fixed SapGuiHelper.GetSession COM handle leak (now properly disposed).
- Full XML documentation on all public members (zero CS1591 warnings).
## [0.3.0]
- Added typed wrappers: GuiTabStrip, GuiTab, GuiToolbar, GuiMenubar, GuiMenu, GuiContextMenu, GuiMessageWindow.
- Added GuiSession convenience finders: TabStrip(), Tab(), Toolbar(), Menubar(), Menu(), Tree(), GetActivePopup().
- FindById() now returns typed instances for all newly added types.
- SapComponentType enum extended with GuiMenu, GuiContextMenu, GuiCalendar, GuiOfficeIntegration.
## [0.2.0]
- All public types moved into a single SapGui.Wrapper namespace. Only one import is now needed: Imports SapGui.Wrapper (VB.NET) or using SapGui.Wrapper; (C#).
## [0.1.0]
- Added findById(id) camelCase alias on GuiSession returning dynamic; recorder VBScript can be used in C# with only one change: add () to method calls.
- Added FindByIdDynamic(id) returning dynamic for full IDispatch late-binding.
- Promoted Text, Press() and SetFocus() to GuiComponent base class; FindById(id).Text / .Press() / .SetFocus() now work without casting.
## [0.0.0]
- Initial release.