OFFICIUM INSERVIO
Your reliable partner for your business software...
Sage 100 Sage 100 - Optionen für eigene UX-Elemente / Options for own UX elements
9.0
Dienstag, 4. November 2025
Clientseitigen .net-Code vollständig "Smart Client"-kompatibel ausführen
Aufruf der Klasse über AppDesigner-Definition
Definition der Klasse , die ausschließlich im Client laufen soll
Definition einer Client-Klasse auf der Basis von "ClientLibraryExecuteBase"
Von Sage bereitgestellte Klassen und Funktionen für den Client (sehr eingeschränkt!)
Applikationsserver-Daten und Aktionen
Interaktion mit dem Frontend , z.B. Client-Shell beim neuen "Smart Client"
Funktionen aus Client im Sage Applikationsserver auslösen
Die wichtigsten "Stolperfallen" in der Übersicht
Eigenes Mandantenobjekt initialisieren?
Komplexere UX/UI-Belegoperationen der laufenden Sage 100-Belegerfassung
AppDesigner-Einstiegspunkt für Aufruf-Typ "DataServiceExecute.Sage.System"
Definition DCM-Handler / Registrierung DCM-Handler
Handhabung des Servercodes im Sage Applikationsserver
(Optionale) Rückgabe von geänderten Belegdaten an die UX/UI der Sage 100-Belegerfassung
Deutsch
Hintergrund
In der Sage 100-Praxis stellt sich immer wieder die Frage, wie man vollständig mit neuer Sage AppDesigner- und Sage Applikationsserver-Technik auch komplexere Anforderungen umsetzen kann.
Dazu gehören sehr oft die Anforderungen, dass man in der Sage 100-Belegerfassung entweder gespeicherte Belege nachbearbeiten/weiterverarbeiten möchte.
Oder sogar einen Beleg, den der Anwender "live" bearbeitet/verändert, dann über Kontextmenü mit .net-Code weiterzuverarbeiten.
In großen Projekten kommt es z.B. auch vor, dass man komplexere eigene UX/UI-Elemente mit Drittanbieter-Komponenten designen möchte bzw. muss, weil sich Kundenanforderungen nicht mit dem Sage AppDesigner vollständig realisieren lassen.
Es stellt sich bei allen diesen Punkten die Frage, wie dabei vollständig kompatibel zum neuen "Smart Client" vorgegangen werden kann.
⚠ Die folgenden Wege fokusieren sich auch auf Beispiele , die auch mit dem neuen "Smart Client" der Sage 100 kompatibel und somit zukunftsfähig sind (sofern sinnvoll).
D.h. sämtliche Wege , MS-Access-Legacy-Code aufzurufen , seien an dieser Stelle nicht näher erläutert.
Derartige Legacy-Technik ("gbAddInRun" aus AppDesigner z.B.) erklärt sich weitestgehend von selbst und sollte ohnehin den meisten Techpartnern und Fachhandelspartnern bereits bekannt sein.
Clientseitigen .net-Code vollständig "Smart Client"-kompatibel ausführen
Aufruf der Klasse über AppDesigner-Definition
Der Aufruf erfolgt in der Sage 100 über den Funktionstyp "AppLibraryCall.Sage.System" wie folgt in einem Kontextmenü-Eintrag.



Definition der Klasse , die ausschließlich im Client laufen soll
Um "Smart Client"-kompatibel ausschließlich clientseitigen Code auszuführen , steht die relativ neue "Sagede.OfficeLine.Shared.ClientActions.ClientLibraryExecuteBase"-Klasse bereit.
Dafür muss eine Referenz auf die Assembly "Sagede.OfficeLine.Shared.ClientActions.dll" dem Projekt hinzugefügt sein.
Es empfiehlt sich unbedingt , derartigen Code in eine separate "*Client*"-Assembly oder "*ClientFrm*"-Assembly (sinngemäß) zu integrieren , so dass man sofort sieht , dass hier "client-only" Code enthalten ist.
Anmerkung: Bei der Verwendung der bisherigen "AppLibraryCall"-Technik mit der alten Sage-Klasse "Sagede.OfficeLine.Engine.AppLibraryExecuteBase" generiert Sage inzwischen konsequenterweise eine Obsolete-Compiler-Warnung:
Warning CS0618: 'AppLibraryExecuteBase' is obsolete: 'Use Sagede.OfficeLine.Shared.ClientActions.ClientLibraryExecuteBase' (19, 40)
Mittels der neuen Basisklasse "ClientLibraryExecuteBase" kann eine eigene .net-Klasse zur Laufzeit über die AppDesigner-Metadaten für eine kontextabhängige Ausführung definiert werden.
Sage initialisiert dann die entsprechende definierte Klasse , und diese wird ausschließlich auf dem Client ausgeführt.
Dadurch wird das Einbinden eigener , quellcodeseitig erzeugter Oberflächen (WPF , Meldungsfenster usw.) ermöglicht.
Definition einer Client-Klasse auf der Basis von "ClientLibraryExecuteBase"
Der folgende Code zeigt , die auf den o.g. Funktionsnamen "SAGExampleFunctionNameIdSetMatchcode" reagiert werden kann.
public class MyTest1ClientLibraryExecute : Sagede.OfficeLine.Shared.ClientActions.ClientLibraryExecuteBase
{
if (string.Equals(base.FunctionName, "SAGExampleFunctionNameIdSetMatchcode", StringComparison.OrdinalIgnoreCase))
{
...
}
}Die eigene Instanz der "ClientLibraryExecuteBase" wird – siehe Punkt zuvor – im AppDesigner in einem bestimmten Kontext definiert.
Beispielsweise im Kontextmenü eines Beleges.
Wenn die Sage 100 dann den clientseitigen Quellcode in der eigenen Klasse "MyTest1ClientLibraryExecute" über deren "Execute"-Methode auslöst , dann werden grundsätzlich in der "Data"-Property (Typ "Sagede.Core.Data.DataContainer") die Felder des Kontexts bereitgestellt.
In der Belegerfassung der Sage 100 sind dies z.B. die Felder der jeweiligen Datenstrukturen im AppDesigner.
Für das Verkaufsbeleg-Objekt sind dies "strVKBeleg.Sage.Wawi" und "strVKBelegPosition.Sage.Wawi" etc.
Somit sind sehr einfache Feld-Änderungen der Datenstrukturfelder sofort im eigenen Client-Code möglich.
Beispiel zum Abfragen eines Feldinhalts:
var documentType = this.Data.GetValue<string>("Belegart");Beispiel zum Setzen/Aktualisieren eines Feldinhalts:
this.Data.Fill("Matchcode", "My document matchcode".Left(50));⚠ Vorsicht bei Datenfeld-Änderungen!
Bei der Verwendung der "Fill"-Funktion wird Makro-Logik von Sage (oder eigene) weder auf Datenstruktur- noch Oberfläche-`edi`-Ebene ausgeführt!
⚠ Auch wenn das Ereignis z.B. über die Verkaufsbelegpositionsebene (Position!) ausgelöst wird , so steht in der "Data"-Property immer das gesamte Belegobjekt zur Verfügung , nicht das aktuell gewählte Positionsobjekt.
Dies ist teils anderes Verhalten als z.B. die "NamedParameters" in den Proxy-Klassen (siehe weiter unten).
Grundsätzlich werden auch die Belegpositionen (u.a. untergeordnete Elemente) von Sage in der "Data"-Property des Typs "DataContainer" in der Unter-Auflistung "Data.Children" übergeben.
Dahinter stecken ebenfalls die Felder der jeweiligen Datenstrukturen im AppDesigner.
Für das Verkaufsbeleg-Objekt sind "strVKBelegPosition.Sage.Wawi" für die Positionen etc.
Der Umgang mit diesen "Children"-Objekten ist folglich recht aufwändig , und natürlich ist die Veränderung von Daten an dieser Stelle entsprechend aufwändig.
Für komplexere Aktionen , insbesondere für das Hinzufügen neuer Artikelpositionen oder dergleichen , sollten daher unbedingt die Business Objekte (Geschäftsobjekte) verwendet werden!
Dazu weiter unten mehr.
Wo läuft der Code überhaupt?
Wird das MS-Access-basierte Frontend der Sage 100 gestartet , dann läuft der Code stets im Prozess der "MSAccess.exe".
⚠Der "MSAccess.exe"-Prozess stößt selbst mit aktiviertem 3 GB-Speicherpatch unglaublich schnell an Grenzen! Man sollte daher auf keinen Fall sehr komplexe UX/UI-Elemente in der Client-DLL direkt aufrufen, sondern stattdessen über eigene externe Programme nachdenken , die man aufruft.
Wird das neue "Smart Client"-Frontend der Sage 100 gestartet , dann läuft der Code stets im Prozess "Sagede.OfficeLine.Client.exe" des neuen "Smart Client".
Von Sage bereitgestellte Klassen und Funktionen für den Client (sehr eingeschränkt!)
Mandanten-Informationen
Seitens Sage wird über die Property "MandantBase" eine Instanz vom Typ "Sagede.OfficeLine.Engine.MandantBase" bereitgestellt.
Diese implementiert "IDisposable" und folglich wird von Sage entsprechend automatisch freigegeben , sobald sinnvoll/notwendig.
⚠ Der Typ "Sagede.OfficeLine.Engine.MandantBase" ist ein sehr "abgespecktes" Mandanten-Objekt , welches lediglich diverse Grundinformationen bietet.
⚠Es sind keinerlei direkte Datenzugriffe mehr über dieses Objekt möglich (siehe "Stolperfallen" nachfolgend).
Applikationsserver-Daten und Aktionen
Über die Property "AppContext" stellt Sage eine Instanz des (oft recht bekannten) Typs "Sagede.Shared.RealTimeData.Common.ApplicationContext" bereit.
Darüber lassen sich zahlreiche serverrelevante Daten auslesen , d.h. diese Instanz ist wichtig für den Zugriff auf den Sage Applikationsserver.
Erwähnenswert sind folgende Sub-Properties , die an der Property "AppContext" hängen:
"ApplicationId" , "CurrentUser" , "Country" , "Token" , "ServerInfo" , "IsTerminalServerClient" , "IsLegacyClient" , "DatabaseName" , "MandantId" , "DmsCredential" uvm.
Es ist zu beachten , dass es natürlich vom Kontext abhängt , welche Informationen wie von Sage überhaupt in den Properties bereitgestellt werden!
Interaktion mit dem Frontend , z.B. Client-Shell beim neuen "Smart Client"
Äußerst interessant und wichtig ist nun die neue Property "ClientActions" , die vom Typ "Sagede.Shared.ControlCenter.Controller.ClientAction.ICustomClientActionCallback" ist.
Darüber erlaubt es Sage , beispielsweise im Frontend mit "ShowMessageBox" eine einfache Meldung anzuzeigen.
Oder , wenn tatsächlich der Aufruf aus dem MS-Access-basierten Legacy-Client erfolgte , kann mit "Eval" im MS-Access-Frontend die VBA.Eval-Funktion ausgelöst werden.
Dieses "Eval" ist sicherlich sehr vielen Fachhändlern und Techpartnern sehr gut geläufig.
Um vollständig eigene UX/UI-Elemente direkt in die Shell des neuen "Smart Clients" zu integrieren und innerhalb der Container-Anwendung der Sage 100 "Smart Clients" anzuzeigen , stehen in der "ClientAction"-Property die folgenden Methoden bereit:
"TryShowCustomDocument"
"IsCustomDocumentOpen"
"TryCloseCustomDocument"
Der Ablauf ist dabei so , dass man eine neue .net-Klasse als eigenen UX/UI-Handler erstellen muss , die zwingend von "Sagede.Shared.ControlCenter.Controller.ClientAction.CustomUiFactoryBase" abgeleitet wird.
⚠ Sage setzt als Technik die Windows Presentation Foundation (WPF) und ein MVVM-Pattern (Model-View-ViewModel) für die Umsetzung voraus.
Dies ist ein gängiges Pattern im WPF-Umfeld.
Vereinfacht ausgedrückt bedeutet dies:
Das "Model" repräsentiert die Daten- und Geschäftslogik und wird in "normalen" Klassen abgebildet , die einem speziellen Pattern folgen müssen.
Die "View" ist die eigentliche UX/UI-Anzeige und wird mit "*.xaml"-Dateien definiert (deklarativer Ansatz).
Das "ViewModel" handhabt ausschließlich die Logik der UX/UI und exponiert die Daten zur View.
Für die vollständige Interaktion mit der Shell des "Smart Client" müssen zudem , wie schon erwähnt , zwingend die speziellen Ableitungen von Sage-Klassen erfolgen , und darüber hinaus sind diverse Sage-Besonderheiten einzuhalten!
⚠Technik-begründet steigt dadurch extrem der Implementierungsaufwand an (gegenüber alter MS-Access-Legacy-Technik).
Die Lernkurve von WPF ist – gegenüber anderen Techniken – sehr steil.
Dies muss einem bewusst sein!
Funktionen aus Client im Sage Applikationsserver auslösen
Mittels "base.CustomBackend.ExecuteCustomOperation" (asynchrone Methode "ExecuteAsyncCustomOperation") in der eigenen Klasse vom Typ "Sagede.OfficeLine.Shared.ClientActions.ClientLibraryExecuteBase" können aus dem Client gezielt einzelne Funktionen im Sage Applikationsserver getriggert werden.
Dazu muss in einer .net-DLL für die Ausführung im Sage Applikationsserver , die dann dynamisch zur Laufzeit vom Client angesprochen wird , eine eigene Klasse erstellt werden , die zwingend von der Sage-Klasse "Sagede.OfficeLine.Shared.RealTimeData.CustomOperationBase" erbt , z.B:
public class MyOperation1 : Sagede.OfficeLine.Shared.RealTimeData.CustomOperationBase
{
...
}Diese Klasse gehört in eine separate DLL ("*RealTimeData*"-DLL für das Projekt) , die auf allen Sage Applikationsservern in den dortigen "\Shared"-Ordner der Sage 100-Installation ausgerollt sein muss.
Code auf Clientseite:
var responseParameters = base.CustomBackend.ExecuteCustomOperation( "MyProject.RealTimeData.dll" , "MyProject.RealTimeData.CustomOperations.MyOperation1" , requestParameters , base.AppContext );Der Response ist dann eine Sage-typische "NamedParameters"-Auflistung.
⚠ Diese Technik erlaubt es nicht ohne Weiteres , komplexe Objekte "out-of-the-box" zu übertragen.
Da jedoch Strings übertragen werden können , kann eine eigene Datenklasse definiert werden , die man dann mit einem gängigen Serializer , z.B. JSON , auf Server-Seite in einen String serialisiert.
Der String wird dann vom Server zurück an den Client übergeben und muss dort mit demselben Serializer wieder deserialisiert und in dieselbe Klasse gecasted werden.
Bei diesem ganzen Pattern profitiert man davon , wenn man "vernünftig" eine Assembly-Aufteilung für das Projekt vorgenommen hat , d.h. es gibt eine einfache separate "*Contracts*"-Assembly , in der diese zu serialisierenden Datenklassen , POCOs , DTOs etc. enthalten sind (und kein komplexer Logik-Code!).
Diese "*Contracts*"-Assembly muss dann zusammen mit der DLL , die die "ClientLibraryExecute" beinhaltet , natürlich auch auf alle Clients verteilt werden.
Die Logik der erwähnten "base.CustomBackend.ExecuteCustomOperation" wird dann jedoch immer im Sage Applikationsserver ausgeführt.3
Für komplexere Anwendungsfälle: Wie kommt man aus dem neuen Client an die SQL-Daten?
Ein typischer Anwendungsfall wäre , wenn man , wie weiter oben erklärt , komplexe eigene WPF UX/UI-Komponenten in der Shell des "Smart Clients" hosten möchte.
Mittels der zuvor vorgeschlagenen "base.CustomBackend.ExecuteCustomOperation"-Methode kann man sich auf Sage Applikationsserver-Seite eine Implementierung aufrufen , die dort die gewünschten SQL-Anmeldedaten ermittelt und initial an den Client-Code zurücktransferiert.
Im Client-Code macht man sich dann einfach eine eigene SQL-Datenverbindung auf , um z.B. komplexere WPF-Controls direkt per SQL-Client-Server-Verbindung an die SQL-Daten binden zu können etc.
⚠ Ein eigenes neues "klassisches" Sage-Mandantenobjekt aufzubauen , ist ausdrücklich nicht empfehlenswert , dazu später mehr!
Will man stattdessen irgendeine Funktionalität implementieren , die mit einem vom Anwender nicht veränderten Beleg arbeitet und den Beleg vollständig mit Sage-Belegobjekten verändern , dann ist dies möglich.
Eine mögliche Umsetzung wäre:
Makro , welches auf das Speichern eines Beleges in der Oberfläche reagiert.
Das Makro ruft dann einen vorher angelegten unsichtbaren(!) Kontextmenü-Eintrag vom Typ "AppLibraryCall.Sage.System" auf.
In dem unsichtbaren Makro wird der clientseitige "ClientLibraryExecute"-Code ausgelöst , und "Vollstaendig : Ersetzen ; Felder und Kinder : Felder und Kinder" wird als Aufrufbearbeitung für Sage instruiert.
Da die Sage-Makro-Ausführung grundsätzlich synchron wartet und per Default nicht asynchron , kann der eigene Client-Code natürlich dann z.B. auf diverse Wege etwas mit dem Beleg machen – einfache Sachen funktionieren durch simples Verändern der Datenstruktur-Felder des Beleges (siehe Hinweise zuvor).
Komplexere Aktionen wären über z.B. weitere AppServer-Calls denkbar , um den Beleg mit den Business Objekten (Geschäftsobjekten) von Sage , der BelegEngine , dort serverseitig zu laden , zu verändern und abzuspeichern.
Am Ende des Makros , nach dem Aufrufen des unsichtbaren Kontextmenüs , macht man in dem Makro dann einfach ein "DatensatzLaden" bzw. "LoadDataRecord" und als ersten Parameter übergibt das vorher in einer Makro-Variable zwischengespeicherte Beleg-Handle (Feld "[Handle]") , um den durch div. Code veränderten Beleg komplett neu zu laden.

⚠ Das Neuladen der Daten aus der Belegerfassung ist von Sage nicht wirklich so vorgesehen und macht in der Belegerfassung Probleme. Die Verwendung von "LoadDataRecord" bzw. "DatensatzLaden" macht in Makros , die in der Belegerfassung ausgeführt werden , somit keinen Sinn! Das Beispiel im Screenshot oben ist nur ein Grundbeispiel für ein mögliches Ablaufkonzept zu verstehen und dann erst möglich , wenn Sage aus AppDesigner-Makros heraus das vollständige Neuladen eines bereits geöffneten Beleges erlaubt.
⚠ Wesentlich komplexer wird es immer dann , wenn der Anwender den Beleg schon geändert hat bzw. verändern können soll und man mit diesem Beleg , der "live" vom Anwender in der Sage 100-Belegerfassung bearbeitet wird , dann vollständig mit den Sage Business Objekten (der BelegEngine) weiterarbeiten will.
Dann führt i.d.R. kein Weg an den Proxy-DCMs vorbei , z.B. "VKBelegProxyServiceCall"-DCMs für Verkaufsbelege (siehe nachfolgende Infos).
Die wichtigsten "Stolperfallen" in der Übersicht
Alle folgenden Punkte sollte man genau prüfen und beachten!
⚠ Es ist zu beachten , dass in dem zuvor dargestellten gesamten Umfeld von Implementierungen des Typs "Sagede.OfficeLine.Shared.ClientActions.ClientLibraryExecuteBase" generell keinerlei direkte Datenzugriffe weder über SQL noch die Sage-Geschäftsobjekte möglich sind. Ein SQL-Datenzugriff wäre ausschließlich über eine eigene SQL-Verbindung möglich. Workarounds: siehe weiter unten.
⚠ Vorsicht bei Datenfeld-Änderungen!
Bei der Verwendung der "Fill"-Funktion wird Makro-Logik von Sage (oder eigene) weder auf Datenstruktur- noch Oberfläche-"edi"-Ebene ausgeführt! Der nachfolgende Code zum Ändern des Liefertermins ist daher als äußerst kritisch zu bewerten, da keinerlei Geschäftslogik durch diese direkte Änderung des Feldes berücksichtigt wird.
this.Data.Fill( "Liefertermin" , DateTime.Today.AddDays( 5 ) );⚠ Ferner stehen seitens Sage an dieser Stelle auch ausschließlich beschränkte Möglichkeiten zur Verfügung , vom Client aus über die "Data"-Property (Typ "Sagede.Core.Data.DataContainer") serialisierte Infos anzurufen oder über Serialisierung zurück an Sage zu geben.
Beispiel für Aufrufe in der Belegerfassung:
Es steht kein "bequemes" Sage Business-Belegobjekt (BelegEngine , Sage-Geschäftsobjekte) zur Verfügung , mit dem Belege sehr umfangreich geändert werden könnten. D.h. es ist nicht ohne Weiteres möglich , über den "DataContainer" Positionen zu verändern oder neu hinzuzufügen etc.
Komplexe Aktionen dieser Art gehören einfach nicht in den Client-Code!
⚠ Falls man den Legacy-Client startet:
Der "MSAccess.exe"-Prozess stößt selbst mit aktiviertem 3 GB-Speicherpatch unglaublich schnell an Grenzen! Man sollte daher auf keinen Fall sehr komplexe UX/UI-Elemente in der Client-DLL direkt aufrufen, sondern stattdessen über eigene externe Programme nachdenken , die man aufruft. Auch der neue "Smart Client" ist derzeit (2025) noch ein 32-Bit .net-Prozess! Sage denkt über eine zukünftige Umstellung auf .net core und 64-Bit nach!
Eigenes Mandantenobjekt initialisieren?
⚠ Generell Vorsicht beim Gedanken , ein eigenes "klassisches" Mandantenobjekt im Client neu zu initialisieren:
Dabei gibt es gleich mehrere gravierende Probleme!
Natürlich könnte man über ein eigenes "klassisches" Mandantenobjekt zwar dann auch wieder komplexere Sage-Geschäftsobjekte (z.B. Belegobjekt , BelegEngine) verwenden und diese im Sage 100-Client auslösen. Das Aufmachen eines eigenen Mandantenobjekts hat aber diverse Nachteile , teils gravierende.
Durch das Initialisieren eines eigenen Mandantenobjekt wird i.d.R. immer die Named-User-Lizenz zusätzlich belegt!
Außerdem kostet der Aufbau einer eigenen Datenverbindung Performance (beim Mandantenobjekt mehr als bei einer eigenen SQL-Verbindung).
Und natürlich widerspricht dieses Prinzip dem vom Sage anvisierten Weg , dass komplexere Sage-Business-Objekt-Aktionen auf den Sage Applikationsserver verlagert gehören.
⚠ Das Gravierendste ist jedoch , dass das "klassische" Mandantenobjekt eben sehr "mächtig" ist und "IDisposable" erfordert (ebenso wie das Sage Session-Objekt in diesem Zusammenhang).
Es gibt aber in dem neuen Pattern von Sage für die Implementierung eigener UX/UI-Komponenten in die Shell des "Smart Clients" (per heute) keine Berücksichtigung/Abwicklung von "IDisposable", die an die "Lebensdauer" des geöffneten Fensters (der View) gekoppelt wäre! Ein eigen initialisiertes Mandantenobjekt muss aber nunmal zwingend am Ende wieder disposed werden , wenn der Anwender das Fenster schließt , ansonsten blieben die ganze Zeit unnötig Ressourcen blockiert - Das kann gravierende negative Konsequenzen haben!
Man muss sich auch vor Augen führen , dass Sage mit der Anwendungs-Shell des "Smart Clients" ein "Multi-Window-Pattern" vorsieht. D.h. im Grunde sind auch mehrere Instanzen des eigenen UX/UI-Controls über mehrere Fenster denkbar. Mangels Callback-Events aus der Anwendungs-Shell des "Smart Clients" im Falle des Schließen des Fensters/Containers kann an dieser Stelle also (per heute) nicht wirklich 100% sicher ein Event-basiertes/getriggertes Disposing des Mandantenobjekts ermöglicht werden. Somit kann das selbst eröffnete Mandantenobjekt nicht "sauber" freigegeben werden.
Weitere potenzielle Gefahren in diesem Zusammenhang:
Es ist nicht klar , was Sage alles genau im "klassischen" , sehr "heavy impact" , Mandantenobjekt so alles speichertechnisch , Thread-technisch usw. durchführt.
Unter Umständen wird durch das (u.U. mehrfache) Neuinitialisieren eines eigenen Mandantenobjekts innerhalb der Anwendungs-Shell des "Smart Clients" die Stabilität der gesamten laufenden "Smart Client"-Instanz negativ beeinflusst.
Daher ist von diesen Ideen eines eigenen Mandantenobjekts innerhalb der Anwendungs-Shell des "Smart Clients" eher deutlichst abzuraten!
Innerhalb der Anwendungs-Shell des "Smart Clients" sollte man sich eher strikt an die von Sage vorgeschlagenen Wege halten , um Daten zu lesen und zu verändern (z.B. über "base.CustomBackend.ExecuteCustomOperation").
Alternativ konzentriert man sich auf das Öffnen einer eigenen , nativen SQL-Datenverbindung über den üblichen SQL-Verbindungspool.
Dann muss man lediglich bei diversen Aktionen der SQL-Objekte , die lokal deterministisch mit "IDisposable" sehr gut benutzbar sind , bei der Verwendung an ein Kapseln in "using" bzw. den "Dispose"-Aufruf in den eigenen Methoden denken.
Reine lesende SQL-Bindungen an WPF-Controls sollten in diesem Zusammenhang unkritisch sein , weil diese kein "IDisposable" voraussetzen.
⚠ Natürlich sind allzu "mächtige" Controls , die massiv komplexen "Anwenderluxus" wie diverse komplexe SQL-Filterung erlauben , unter Umständen sehr kritisch in Bezug auf den Impact auf die SQL-Performance zu sehen.
Dieses Problem hat Sage ja selber im Zusammenhang mit den zahlreichen möglichen dynamisch konfigurierbaren WPF-Filterelementen des Sage 100-Standards , die SQL-serverseitig sehr oft nicht optimierte SQL-Queries triggern und bei Massendaten dann die Gesamtperformance des Systems negativ beeinflussen können.
Z.B. nur weil ein Anwender (vermutlich unbewusst) häufig auf großen Tabellen sehr unglücklich "LIKE"-Suchen oder dergleichen mit Sage 100-Filter-"Bordmitteln" auslöst.
Komplexere UX/UI-Belegoperationen der laufenden Sage 100-Belegerfassung
Komplexere Operationen in der laufenden Sage 100-Belegerfassung gehören grundsätzlich in Code platziert , der auf der Sage Applikationsserver-Seite aktiv ist.
D.h. für laufende , bereits vom Anwender bearbeitete Belege führt i.d.R. kein Weg um die Verwendung der folgenden DCMs vorbei:
"VKBelegProxyServiceCall" für Verkaufsbelege.
"EKBelegProxyServiceCall" für Einkaufsbelege.
Insbesondere ist dieser Weg zwingend notwendig , wenn mit Belegen der Belegerfassung gearbeitet wird , die der Anwender "live" bearbeitet hat oder noch weiterbearbeiten soll!
Die Verwendung ist auf den ersten Blick recht einfach in der Grunddefinition , aber dann doch (gegenüber Legacy-Code-Programmierung) deutlich aufwändiger in der inhaltlichen Umsetzung.
AppDesigner-Einstiegspunkt für Aufruf-Typ "DataServiceExecute.Sage.System"
Für die Grunddefinition legt man sich einen entsprechenden Kontextmenü-Eintrag vom Typ "DataServiceExecute.Sage.System" für den "Funktionsaufruf" bzw. "Function Call" an.


Der erste Parameter für den Funktionsaufruf ist dabei ein freier Text , der jedoch prägnant und vor allem im gesamten Projekt übergreifend eindeutig sein sollte.
Dieser Text fungiert als ID , um später in dem serverseitig laufenden Quellcode im Sage Applikationsserver den Ausführungskontext zu bestimmen.
Der zweite Parameter gibt an , wie Sage bei der Rückkehr aus dem Servercode mit einem dort eventuell veränderten Belegobjekt umgehen soll. Diese Einstellung ist Performance-relevant.
Eine Option , die z.B. vollständig den Beleg in der UX/UI mit den Daten aus dem Servercode aufbaut , lautet:
Vollstaendig : Ersetzen ; Felder und Kinder : Felder und KinderDie folgende Option sorgt dagegen dafür , dass Rückgaben aus dem Servercode bei der Deserialisierung durch Sage ignoriert werden , was natürlich bedeutet , dass die UX/UI-Elemente nicht komplett neu aufgebaut werden müssen:
Vollstaendig : IgnorierenDefinition DCM-Handler / Registrierung DCM-Handler
Um nun .net-Code schreiben zu können , der im Sage Applikationsserver getriggert wird und um dort entsprechend mit den Sage Business Objekten (Geschäftsobjekten) arbeiten zu können , muss man noch einen entsprechenden DCM-Handler über den AppDesigner in "Erweiterungen" -> "Ereignisse" registrieren.
Dort ist entsprechend ein neuer Eintrag zu hinterlegen , der auf das Ereignis bzw. den Event-Namen "VKBelegProxyServiceCall" reagiert (für Verkaufsbelege!).
Und natürlich muss der Name der Assembly und der Klassenname (Class Name) angegeben werden , in dem sich der entsprechende Code befindet.
Der Sage AppDesigner schaut für die Auswahl der Klasse per Reflection in der angegebenen DLL nach , welche Klassen "IDcmCallback" implementieren.
D.h. der Code muss zwingend das Interface "IDcmCallback" und daraus die Methode "Entry" sinnvoll implementiert haben.
Dies ist der Einstiegspunkt zur Laufzeit aus Sage Sicht.
⚠ DCMs werden nicht immer 100% zuverlässig ausgelöst:
https://www.officium-inservio.com/sage-100-1/dcmnotalwaystriggered
In besonders "kritischen" Anpassungen (die für einen Kunden besonders wichtig sind) sollte man darüber nachdenken , entsprechende Logik direkt in die passenden Sage Business Objekte (Geschäftsobjekte) einzubauen. Wenn es eine reine clientseitige Logik ist, könnte man z.B. die entsprechenden Proxy-Assemblies von Sage anpassen!
Handhabung des Servercodes im Sage Applikationsserver
In der "Entry"-Funktion der eigenen Klasse (die in eine separate DLL gehört! z.B. "*RealTimeData*") reagiert man entsprechend auf den DCM-Enumerationstyp "DcmDefinitionManager.DcmListId.VKBelegProxyServiceCall".
Den grundlegenden Context der "IDcmCallback"-Implementierung muss man sich dann in den Typ "Sagede.OfficeLine.Wawi.BelegProxyEngine.DcmContextBelegProxyServiceCall" casten.
Z.B. mit folgender Hilfsmethode:
private TContext GetContext<TContext>(IDcmContext context) where TContext : class, IDcmContext
{
if (context == null) throw new ArgumentException("SAG: 27137111: Context not provided!");
var specificContext = context as TContext;
if (specificContext is null) throw new InvalidCastException($"SAG: 27137112: Could not get context of type '{typeof(TContext)}'");
return specificContext;
}In der Behandlung für den DCM-Enumerationstyp "DcmDefinitionManager.DcmListId.VKBelegProxyServiceCall" kann man dann entsprechend mit der vorgeschlagenen "GetContext"-Methode ein Casting in "DcmContextBelegProxyServiceCall" vornehmen.
case DcmDefinitionManager.DcmListId.VKBelegProxyServiceCall:
var serviceCallCtx = GetContext<DcmContextBelegProxyServiceCall>(context);Damit sich der Kreis zu der o.g. AppDesigner-Einstiegsdefinition im Kontextmenü schließt, muss man natürlich nun noch wissen, wie man die o.g. selbst definierte ID für den Kontext abfragt (im o.g. Beispiel ist dies die ID "SAGAddWerkstoff", siehe Screenshot zuvor).
Dies geschieht über die String-Property "ServiceCall" des Kontext-Objekts.
if (string.Equals(serviceCallCtx.ServiceCall, "SAGAddWerkstoff", StringComparison.OrdinalIgnoreCase))
{
// ... Hauptlogik / Main logic!
}Der gesamte Code für die Hauptlogik sollte aus diversen Gründen eines guten Programmierstils ebenfalls in eigene Klassen und sogar eine eigene Logik-DLL gekapselt werden.
Dadurch lässt sich die Logik wiederverwenden (neben anderen Vorteilen).
In "serviceCallCtx.Beleg" würde man das "echte" Geschäftsobjekt von Sage für den Verkaufsbeleg finden.
⚠ In "serviceCallCtx.NamedParameters" hat man übergebene Parameter zur Verfügung. Diese Parameter sind kontextabhängig!
Beispiel:
Wenn der "VKBelegProxyServiceCall" z.B. zu einer Belegposition ausgelöst wird , dann stehen die Felder der Datenstruktur der Belegposition an dieser Stelle zur Verfügung.
Darüber kann man sich dann den genauen Kontext der ausgewählten Artikelposition ermitteln.
Komplexere Veränderungen führt man dann über die Beleg-Instanz in "serviceCallCtx.Beleg" durch , die man als Parameter an die Logik-Assembly/Logik-Klasse übergibt.
Über das vollständige Belegobjekt als Geschäftsobjekt lassen sich z.B. auch neue Positionen hinzufügen etc.
(Optionale) Rückgabe von geänderten Belegdaten an die UX/UI der Sage 100-Belegerfassung
Abschließend muss man entscheiden , ob eine vollständige Rückgabe aller Änderungen aus dem laufenden Code auf Sage Applikationsserverseite zurück an den Sage 100-Client notwendig ist.
Die Belegerfassung wird natürlich ausschließlich dann mit den veränderten Daten in der UX/UI aktualisiert , wenn wie oben erklärt o.g. (im zweiten Parameter) für den Funktionsaufruf die Anweisung enthalten ist , die UX/UI-Daten vollständig durch die deserialisierten Rückgaben vom Sage Applikationsserver zu ersetzen:
Vollstaendig : Ersetzen ; Felder und Kinder : Felder und KinderSiehe Hinweise zuvor.
