OFFICIUM INSERVIO IT
Your reliable partner for your business software...
9.0.x - Komplex-Beispiel Daten-Service mit Daten-Edit-Element (EDI) mit Multi-Data-Edit-Element (MDEE) als Unterelement / Complex example of a Data Service with a Data-Edit-Element (EDI) with Multi-Data-Edit-Element (MDEE) as a sub element
9.0.x
Donnerstag, 20. August 2026
Deutsch
Hintergrund
Es gibt Anforderungen für AddOns in der Sage 100-Entwicklung , die sich nicht mit einfachen schreibenden Listen oder simplen Master-Detail-Elementen etc. lösen lassen.
Für komplexere Bearbeitungsmasken führt meist an folgenden Elementen kein Weg dran vorbei:
-Daten-Edit-Element (EDI) mit...
-Multi-Data-Edit-Element (MDEE) als Unterelement... , um eine 1:n Beziehung abzubilden ...
-gebunden an einen Daten-Service (Data Service) für das Bereitstellen der Daten.
-Listen-Elemente für Anzeigen / Suchen bzw. Filtern der Daten (nicht für die Hauptfunktionalität wichtig).
Das folgende Komplexbeispiel zeigt die Umsetzung Schritt für Schritt.
Besonderes Augenmerk wird dabei auf das Bereitstellen von Benutzerfeldern (USER-Felder) gelegt , da der Anwender in der Lage sein möchte , diese nach Bedarf in der Ansicht als reine Information (read-only) hinzuzufügen.
Beispiel-Szenario
Im vorliegenden Beispiel-Szenario geht es um eine Darstellung von Terminverschiebungen im Verkauf , mit der Möglichkeit , diverse Daten eingeben und automatische Prozesse anstoßen zu können.

Daten
Datenquellen
Für die eigentliche Bearbeitungsmaske sind keine klassischen Datenquellen nötig.
Lediglich die Anzeige/Suche/Filterung mittels Listen-Elementen führt kein Weg an einer Datenquelle vorbei , z.B. mit Datensuch-Elementen (sea).
Tipp: Listen binden wir i.d.R. an SQL-Views und nicht direkt an Tabellen.
Dies hat sich in Praxis massiv(!) bewährt , da es die Komplexität des Designs im AppDesigner massiv reduziert , Erweiterungen bei der Datenermittlung sehr flexibel hält und es außerdem dem SQL-Server besser ermöglicht , die Performance beim Lesen der Daten zu optimieren.
Da es im vorliegenden Fall hauptsächlich um die eigentliche Erfassung mit dem Daten-Service gehen soll , sei dies nur am Rande erwähnt.
Datenstrukturen
Diese sind die entscheidenden Daten-Bindeglieder in diesem Anwendungsfall!
D.h. , beim Umgang mit komplexeren Erfassungselementen und Daten-Services (Data Services) führt an Datenstrukturen im Sage 100 AppDesigner kein Weg dran vorbei!
Die Haupt-Ebene der Termin-Details wird im vorliegenden Fall die Datenstruktur "strTerminverschKriterien" benutzt.
Hier ist es entscheidend , dass auf dieser Ebene der eigentliche Geschäftsprozess (.net-Assembly) mit der Geschäftslogik für den Daten-Service hinterlegt wird.

⚠️Vergessen wird bei der Haupt-Ebene oft , dass ein Primärschlüssel-Feld angegeben werden muss - hier das Feld "SelectionId".
Tipp: Wir empfehlen grundsätzlich keine(!) zusammengesetzten Primärschlüssel.
KISS , Keep It Simple , Stupid: Verwenden Sie als Primärschlüssel grundsätzlich INT-IDENTITY Spalten - auf die Erklärung der massiven Vorteile verzichten wir an dieser Stelle.
⚠️Außerdem werden auf der Kopf-Ebene , dem Element "strTerminverschKriterien" , natürlich auch zwingend alle Detail-Elemente angegeben (siehe nächster Punkt).

Die Detail-Liste der Termin-Details ist in diesem Beispiel an die Datenstruktur "strTerminverschDetails" gebunden , für die kein eigener Daten-Service nötig ist , da dies der Daten-Service des übergeordneten Elements erledigt.
Dies zeigt die Detail-Daten zu einem Kopf-Datensatz.
⚠️Jedes Detailelement benötigt zwingend einen virtuellen Feldnamen (Virtual Field Name).
Im vorliegenden Fall "DetailPos".
Dies ist ein Begriff , der nicht nach Belieben jederzeit umbenannt werden kann , sondern ein Begriff , der auch im Quellcode des Daten-Service zwingend benötigt wird.
Es empfiehlt sich daher , hier mit Bedacht die virtuellen Feldnamen zu wählen und sich alles genau zu notieren.
⚠️In der Praxis haben wir festgestellt , dass dieser Punkt mit den virtuellen Feldnamen oft falsch verstanden und vernachlässigt wird , was dann zu massiven Problemen zur Laufzeit führt , da Sage solche Probleme nicht immer mit aussagekräftigen Fehlermeldungen quittiert , was wiederum zu langatmigen Fehlersuchen führen kann.

Wie schon erwähnt: Auf Positionsebene möchte der Anwender gerne in der Lage sein , die Benutzerfelder in dem Fall der Belegpositionen einblenden zu können.
Diese sollen jedoch schreibgeschützt sein.
Entscheidend ist , dass in der Detail-Datenstruktur dann die richtige benutzerdefinierte Feldklasse "VKBelegPosition" in der Eigenschaft "User Defined Field Source" hinterlegt wird.
Darüber hinaus muss man natürlich auch für die Detail-Datenstruktur einen Primärschlüssel angeben.
Oberfläche - Elemente
Die Kopfdaten gehören in ein Daten-Edit-Element (EDI) , in dem Fall mit dem Namen "ediTerminverschVK".

Es sollte klar sein , dass sich an dieser Stelle der Kreis zu der Datenstruktur "strTerminverschKriterien" schließen muss.
D.h. , diese Datenstruktur ist konsequenterweise als Haupt-Datenquelle des Daten-Edit-Elements zu hinterlegen.
Vergessen wird in der Praxis oft , im EDI-Element den Modus auf "Erfassungs-Modus basierend auf Datenstrukturen" umzustellen.
Dies muss jedoch zwingend geschehen.
⚠️Extrem häufig wird in der Praxis übersehen , die IDs der Primärschlüssel-Felder (Primary Key) zu setzen.
Hier muss man verstehen , dass man die Nummer/ID als numerische Referenz auf das Feld mit derselben Nummerierung in dem Fall der Datenstruktur setzen muss.
Da wir in der Datenstruktur das erste(!) Feld , also das Feld mit der Nummer 1 , ausgefüllt haben , ist im EDI die Nummer 1 im Feld "Primary Key 1" einzutragen.
Tut man dies nicht , braucht man sich bei Primärschlüssel-Feldfunktionen u.a. nicht zu wundern , wenn "abstruse" Fehlermeldungen von Sage geworfen werden und die AppDesigner-Maske crasht.
Ein Kontextmenü ist fast immer zwingend notwendig , um dem Anwender ausreichend Funktionalität zu bieten.
In der Praxis machen wir davon massiv Gebrauch , um in den Kontextmenüs die eigentlichen Haupt-Funktionen der Maske unterzubringen.
D.h. , teilweise handelt es sich um interne Menüpunkte , die ausgeblendet sind und über Aufrufen-Kontextmenü (CallContextMenu) dann getriggert werden.
Selbstverständlich finden sich im Kontextmenü auch Anwenderfunktionen , z.B. um die eigentlichen Hauptaktionen für die Terminverschiebung zu triggern.
Darauf wollen wir an dieser Stelle nicht näher eingehen.
Erwähnt werden sollte an dieser Stelle noch , dass man dem EDI-Element vernünftige Mindest-Fenstergrößen mitgeben sollte.
Außerdem sind die Makro-Ereignisse "On Open Part" bzw. "Beim Öffnen" und "On Current" bzw. "Beim Anzeigen" ideal geeignet , um Makrobausteine oder eben versteckte Kontextmenü-Einträge zu öffnen , in denen man wiederverwendbare UX/UI-Feldlogik packt.
Der "Klassiker" ist natürlich das Ein- und Ausblenden gewisser Felder , die in Abhängigkeit der Daten separat behandelt werden müssen.
⚠️Einbinden des Unterelements
Dieser Punkt ist von enormer Bedeutung und wird in der Praxis ebenfalls oft falsch verstanden und falsch umgesetzt!
Es sollte klar sein , dass man ja "irgendwie" das Multi-Data-Edit-Element (MDEE) für die Anzeige der Detaildaten dem Haupt-EDI mitgeben muss.
Dieses MDEE-Unterelement wird an "strTerminverschDetails" gebunden und muss aber im EDI-Haupt-Element hinterlegt werden.
Da hier in der Praxis mit die häufigsten Fehler passieren , erwähnen wir den Punkt bereits an dieser Stelle.

Wie man im Screenshot sehr gut erkennen kann , wird das MDEE-Unter-Element ganz einfach als Typ Unterelement in einen Reiter des EDI-Haupt-Elements gepackt.
Das klingt logisch.
Man wählt es einfach über die Sage-Suchfunktion im AppDesigner aus.
⚠️Man muss allerdings zwingend für den Namen des Unterelements im Haupt-Element-Feld den weiter oben erwähnten virtuellen Feldnamen hinterlegen - in dem Fall "DetailPos".
Vergisst man dies und trägt irgendeinen anderen Namen ein , crasht die AppDesigner-Maske zur Laufzeit (nicht unbedingt mit aussagekräftigen Fehlermeldungen!).
Durch die vorhergehenden Hinweise haben wir nun den Bogen geschlagen zu dem eigentlichen Unter-Element , dem Multi-Data-Edit-Element (MDEE) für die Detaildaten.
Dieses ist selbstverständlich an die Datenstruktur "strTerminverschDetails" gebunden und kann selbstverständlich ein eigenes Kontextmenü haben.
Auch beim MDEE-Element macht es u.U. Sinn , über Event-Makros einzusteigen , um Felder nach Bedarf zu aktivieren/deaktivieren etc. , falls komplexere UX/UI-Eingriffe in Abhängigkeit der Eingabedaten des Anwenders notwendig sind.

Mit diesen Elementen sind die wesentlichen Elemente für die Erfassung abgeschlossen.
Auf Hilfselemente wie Such-Elemente (mit den o.g. Listen-Elementen).
Quellcode
Kommen wir nun zum Geschäftsprozess.
Sage hat ein Template für den Aufbau eines Geschäftsprozess für einen Daten-Service (Data Service).
Wir haben dieses vor Jahren adaptiert und nach Bedarf umgebaut.
Die eigene Klasse für den Daten-Service muss von "Sagede.OfficeLine.Shared.RealTimeData.DataServiceBase" erben:
public class TerminverschiebungVkUxDataService : Sagede.OfficeLine.Shared.RealTimeData.DataServiceBaseDadurch ergeben sich direkt diverse Zwänge , welche Funktionen , Properties usw. zur Verfügung stehen.
Als Tipp an dieser Stelle empfiehlt es sich , sofort über Konstanten deutlich die Verbindungsglieder zu wichtigen Feldern im Quellcode herzustellen.
Dabei geht es z.B. um Primärschlüssel-Felder und vor allem die ID der virtuellen Feldnamen der Unterelemente.
/// The virtual field name for children selection steps.
private const string VirtualFieldNameForChildrenDetailPos = "DetailPos";
/// The primary key field.
private const string KeyField1 = "SelectionId";Dies soll sofort anhand eines praktischen Beispiels erläutert werden.
Die Haupt-Ebene des Daten-Service orientiert sich logischerweise an der Datenstruktur "strTerminverschKriterien" - wie oben erklärt.
Die Unter-Ebene hängt wiederum an der Datenstruktur "strTerminverschDetails" mit genau dem virtuellen Feldnamen "DetailPos" , siehe Konstante "VirtualFieldNameForChildrenDetailPos".
Für die Implementierung des Daten-Service verlangt nun Sage zwingend das Befüllen diverser Methoden , z.B. folgender:
public override DataActionResult GetItem( DataActionRequest request )In dieser Methode finden Boilerplate-Arbeiten statt (wie das Dekodieren von Base64-Schlüsseln u.a.) , auf die an dieser Stelle nicht näher eingegangen werden soll.
Es geht um den virtuellen Feldnamen , wie zuvor erläutert.
In der "GetItem" verlangt Sage Quellcode , der "irgendwie" die Daten zu einem konkreten Terminverschiebung-Eintrag liest.
Dafür schreibt man sich i.d.R. irgendeine leicht wiederverwendbare Funktion , die mit allgemein gehaltenen SDATA-DTO-Instanzen des Typs "DataContainer" und einem eigenen DTO-POCO-Wrapper umgehen kann.
Beispiel:
private static void FillDto( DataContainer dto , TerminverschiebungVk bo )D.h. nichts anderes:
"TerminverschiebungVk" ist ein simpler POCO DTO-Wrapper , welcher die Felder der o.g. Datenstruktur "strTerminverschKriterien" in CSharp-Code kapselt (einem speziellen Sage Code-Muster folgend).
In dieser "FillDto"-Implementierung macht man dann nichts anderes als dem SDATA-Hilfsobjekt die eigenen Inhalte aus einem POCO-Business Objekt zuzuweisen. Soweit so gut.
Nun kommen wir zurück zu dem virtuellen Feld "DetailPos" , siehe Konstante "VirtualFieldNameForChildrenDetailPos".
Hier schließt sich der Kreislauf zur Definition im AppDesigner und dem dortigen Begriff bzw. Schlüssel "DetailPos" , der den virtuellen Feldnamen bildet.
⚠️Derselbe Schlüssel / virtuelle Feldnamen muss zwingend verwendet werden , wenn man sich um das Befüllen der Detailsätze (Datenstruktur "strTerminverschDetails") kümmert und diese als Unterelement dem Haupt-Element mit der Methode "SetChild" mitgibt.
Dabei muss nämlich absolut zwingend der exakt identische Schlüsselname ("DetailPos" in dem Fall) als Key übergeben werden!
Macht man dies nicht und übergibt andere Schlüssel , kann Sage die Feldzuordnung nicht mappen und im schlimmsten Fall kommt nicht einmal ein Fehler - die Daten "verschwinden" einfach im Nirvana!
// KeyValue und UuidValue des DTOs müssen zwingend gefüllt werden.
dto.KeyValue = bo.KeyValue;
if ( string.IsNullOrWhiteSpace( bo.UuidValue ) )
{
bo.UuidValue = Guid.NewGuid().ToString();
}
dto.UuidValue = bo.UuidValue;
dto.VersionStamp = bo.VersionStamp;
dto.Fill( "Mandant" , bo.Mandant );
...
var childrenSet = new RowSet();
if ( bo.DetailDataPos != null )
{
foreach ( var child in bo.DetailDataPos )
{
var childItem = new Row();
this.FillDtoSelectionDetail( childItem , child );
childrenSet.Add( childItem );
}
}
// The following part is most important - make sure to use the proper key name of the sub element here:
dto.SetChild( childrenSet , VirtualFieldNameForChildrenDetailPos );Wie schon weiter oben erwähnt , sind die üblichen Methoden-Implementierungen eines Sage Daten-Service sehr straight forward und boilerplate stuff.
Daher soll an dieser Stelle nur auf Besonderheiten eingegangen werden.
Zu diesen Besonderheiten gehört genau der o.g. Umgang mit der Sonderanforderung der Anwender , dass diese sehr gerne Benutzerfelder auf Detail-Ebene ("strTerminverschDetails") zur Verfügung haben möchten.
Diese sollen allerdings schreibgeschützt sein.
Wie oben angedeutet , ist das Grundprinzip des Mappings zwischen der nativen SDATA-Welt und den eigenen Geschäftsklassen (POCO-DTO) immer derselbe Ablauf.
Da die Benutzerfelder auf der Detail-Ebene gefordert sind , muss man somit beim Beschreiben der Detail-Felder eingreifen und sich irgendeine Methode aufrufen , die sich um die Benutzerfelder kümmert.
Beispiel:
Man baut sich in der POCO-Klasse für die Details eine eigene Hilfsklasse für den Abgriff der Benutzerfelder:
public Sagede.OfficeLine.Engine.UserDefinedFieldCollection VkPosUserFields { get; set; }"UserDefinedFieldCollection" von Sage nimmt dabei eine Menge Arbeit ab. Mit dieser Klasse sollte man sich einfach mal näher beschäftigen , was nicht Gegenstand dieses Beitrags sein soll.
Empfehlenswerterweise packt man die Handhabung der Benutzerfelder in eine eigene Methode. Dabei kommt erneut Sage zur Hilfe.
Sage hat mit der Klasse "Sagede.Shared.SData.Syndication.RepresentationInformationBuilder" eine Hilfsklasse geschaffen , um sehr leicht Felder bezüglich ihrer Oberflächen-Wirkung zu verwalten. Das ist wirklich extrem hilfreich.
Mit "representationInfoBuilder.AddReadOnlyFields(userFieldName);" kann man dieser Klasse sehr leicht mitteilen , dass ein Feld z.B. schreibgeschützt sein soll (read-only).
Nachdem man dies für alle Felder gemacht hat , übergibt man die gesamte Instanz des Hilfsobjekts ganz einfach dem zuständigen SDATA-Handler mit.
Dafür ist die Funktion "SetRepresentationInformation" am SDATA-DTO zuständig.
Fertig! Das hat Sage sehr gut gelöst 👍🏻.
Beispiel für eine solche Funktion (das Wichtigste als Extrakt):
private static void FillDtoSelectionDetailUserFields(DataContainer dto, TerminverschiebungVkDetailItem dataObject)
{
if (dataObject.VkPosUserFields != null)
{
var representationInfoBuilder = new RepresentationInformationBuilder();
foreach (var field in dataObject.VkPosUserFields)
{
var userFieldName = TerminverschiebungVkManager.UserFielsClassName1Table + field.Name;
dto.Fill(userFieldName, field.Value);
representationInfoBuilder.AddReadOnlyFields(userFieldName);
}
}
dto.SetRepresentationInformation(representationInfoBuilder.BuildOnlyIfRequired());
...