7.2 Dynamischer Code — C#-Skripte für Geschäftslogik

7.2 Dynamischer Code — C#-Skripte für Geschäftslogik

Anleitung für Kundenadministratoren · UDM Admin Client

1

Überblick — Was ist dynamischer Code?

Das UDM-System ermöglicht es Ihnen als Administrator, an vielen Stellen der Anwendung eigene C#-Code-Snippets zu hinterlegen. Diese Skripte werden zur Laufzeit ausgeführt und erlauben es, Geschäftslogik direkt im Admin Client zu definieren — ganz ohne Deployment oder Programmierprojekt.

Typische Anwendungsfälle:

  • Validierung: Pflichtfelder prüfen, bevor ein Datensatz gespeichert wird
  • Berechnungen: Felder automatisch berechnen, z. B. Gesamtpreis = Menge × Einzelpreis
  • Sichtbarkeit steuern: Formularfelder oder Unterdialoge ein-/ausblenden je nach Datenlage
  • Daten nachladen: Beim Speichern automatisch Folgedatensätze erzeugen oder externe Daten abrufen
  • Berechtigungen: Dynamisch steuern, wer lesen, schreiben oder löschen darf
  • Formatierung: Tabellenzeilen farblich hervorheben, wenn bestimmte Bedingungen gelten
Grundkonzept Jeder Code-Abschnitt ist einem Hook (Ereignis) zugeordnet — z. B. „nach dem Laden der Daten“ oder „vor dem Speichern“. Je nach Hook stehen Ihnen unterschiedliche API-Objekte als Variablen zur Verfügung, über die Sie auf Formulardaten, Benutzerinformationen, Filter und vieles mehr zugreifen können.
2

Code-Container-Typen

Code wird immer in einem Container gespeichert. Der Container bestimmt, wo und wann Ihr Code ausgeführt wird. Es gibt Container auf Client-Seite (Formular, Ansichtselemente) und auf Server-Seite (Entitäten, Datenquellen).

FormCode — Formular-Code Client

Wird auf einen Dialog (Formular) gelegt. Hier können Sie auf alle Formularfelder zugreifen, Werte setzen, Felder ein-/ausblenden und das Speichern steuern.

HookBeschreibung
OnInitializedBeim Initialisieren des Formulars
OnDataLoadedNach dem Laden der Formulardaten
OnFieldValueChangedBei Änderung eines Feldwertes
OnBeforeSavingVor dem Speichern — hier können Sie mit CancelSaving = true abbrechen
OnAfterSavingNach dem Speichern
OnAfterFormClosedAsyncNach dem Schließen des Formulars

SubFormCode — Unterformular-Code Client

Wie FormCode, aber für Unterdialoge. Zusätzlich stehen ParentForm und ParentDataItem zur Verfügung. Über den Hook GetSubformCondition und IsActive = true/false steuern Sie, ob der Unterdialog angezeigt wird.

EntityCode — Entitäts-Code Server

Läuft serverseitig bei Datenbank-Operationen. Ideal für Logik, die unabhängig vom Client ausgeführt werden muss (z. B. Folgedatensätze anlegen, Daten validieren bevor sie in der Datenbank landen).

HookBeschreibung
OnBeforeSaveEntityVor dem Speichern — IsNewRecord zeigt an, ob es ein neuer Datensatz ist
OnAfterSaveEntityNach dem Speichern — z. B. Folgedatensätze erzeugen
OnBeforeDeleteEntityVor dem Löschen — z. B. Abhängigkeiten prüfen
OnAfterDeleteEntityNach dem Löschen — z. B. Aufräumarbeiten
OnAfterGetDataNach dem Laden von Datensätzen

DatasourceCode — Datenquellen-Code Server

Wird auf Datenquellen angewendet. Damit können Sie vor dem Laden Filter und Sortierung setzen, nach dem Laden die Ergebnisse manipulieren oder sogar eigene Ergebnis-Datensätze erzeugen.

HookBeschreibung
OnBeforeGetDataVor dem Laden — Filter und Sortierung setzen
OnCreateDataEigene Ergebnis-Datensätze erzeugen
OnAfterGetDataNach dem Laden — Ergebnisse nachbearbeiten
OnProcessRowWird für jede einzelne Ergebniszeile aufgerufen

Weitere Container (Spezialzwecke)

ContainerZweckOrt
FormElementCodeCode für einzelne Formularfelder — Validierung, Parameter, FilterClient
FormActionCodeCode für Aktionen im Dialog — Sichtbarkeit, AusführungClient
ViewElementCodeCode für Ansichtselemente — Filter, Bedingungen, InfokartenClient
ViewElementActionCodeCode für Aktionen in AnsichtselementenClient
PermissionCodeDynamische Berechtigungen — Lesen, Schreiben, Löschen steuernClient
ItemAppearanceBedingte Formatierung — Zeilen/Zellen farblich hervorhebenClient
FkConfigurationCodeFilter für Lookup-Felder (Fremdschlüssel)Server
UdmBaseCodeCode für NachrichtenvorlagenServer
ElementMappingCodeFilter für Element-MappingsClient
PredefinedFilterCodeVordefinierte Filter-BedingungenClient
3

Schritt für Schritt — Code erstellen

Das folgende Beispiel zeigt, wie Sie einen FormCode auf einem Dialog anlegen. Die Vorgehensweise ist bei allen Container-Typen ähnlich — nur der Ort der Konfiguration unterscheidet sich.

  1. Dialog-Konfiguration öffnen Navigieren Sie im Admin Client zum gewünschten Dialog und öffnen Sie dessen Konfiguration (über das Kontextmenü oder die Admin-Verwaltung).
  2. Code-Container zuordnen Im Konfigurationsdialog finden Sie den Bereich „Dynamischer Code“. Klicken Sie auf „Code hinzufügen“ und wählen Sie den passenden Container-Typ (z. B. „FormCode“).
  3. Hook wählen Nach dem Anlegen sehen Sie die verfügbaren Hooks (Ereignisse). Klicken Sie auf den Hook, für den Sie Code schreiben möchten — z. B. OnFieldValueChanged.
  4. Code im Editor schreiben Der Monaco-Editor öffnet sich mit Syntax-Highlighting. Schreiben Sie Ihren C#-Code direkt in das Editorfenster. Nutzen Sie den Funktionsbaum auf der linken Seite, um verfügbare APIs einzufügen.
  5. Speichern und testen Klicken Sie auf „Speichern“. Im Admin Client wird der Code sofort bei der nächsten Auslösung des Hooks ausgeführt — dank Live-Reload müssen Sie die Anwendung nicht neu starten.
Tipp: Live-Reload im Admin Client Wenn Sie im Admin Client arbeiten, werden Code-Änderungen sofort wirksam — ohne Neustart. So können Sie Ihren Code schnell iterativ entwickeln und testen. Reguläre Clients laden den Code einmalig beim Start.
4

Verfügbare APIs

Je nach Container und Hook stehen Ihnen unterschiedliche API-Objekte als direkte Variablen zur Verfügung. Sie können diese sofort im Code verwenden, ohne sie zu instanziieren.

DataItem — Datensatz-Zugriff

Zugriff auf den aktuellen Datensatz. Verfügbar in fast allen Containern.

MethodeBeschreibung
DataItem.GetDataValue("feld")Feldwert lesen
DataItem.SetValue("feld", wert)Feldwert setzen
DataItem.IsNullOrEmpty("feld")Prüfen ob ein Feld leer ist
DataItem.IsEqual("feld", wert)Feldwert vergleichen
DataItem.IsToday("datumsFeld")Ist das Datum heute?
DataItem.GetDayDiff("datumsFeld")Differenz in Tagen zu heute
DataItem.CopyDataValue("quell", "ziel")Feldwert in anderes Feld kopieren

Form — Formular-Manipulation

Zugriff auf das Formular selbst. Verfügbar in FormCode, SubFormCode, FormElementCode, FormActionCode.

Methode / PropertyBeschreibung
Form.SetValue("feld", wert)Feldwert im Formular setzen
Form.SetHidden("feld", true/false)Feld ein-/ausblenden
Form.SetReadOnly("feld", true/false)Feld schreibschützen
Form.ShowSubdialog("name", true/false)Unterdialog ein-/ausblenden
Form.IsNullOrEmpty("feld")Ist das Formularfeld leer?
Form.ValueChangedFieldNameName des geänderten Feldes (in OnFieldValueChanged)
Form.SetInfo("text")Info-Text im Formular anzeigen
Form.SaveDataAsync()Formulardaten speichern
Form.RefreshDataAsync()Daten neu laden

Filter — Filter-Operationen

Zum Setzen von Filtern auf Datenquellen und Ansichtselemente. Nutzt den Compare-Alias.

MethodeBeschreibung
Filter.SetFilter("feld", Compare.Equals, wert)Filter setzen
Filter.AndFilter("feld", Compare.X, wert)UND-Filter hinzufügen
Filter.OrFilter("feld", Compare.X, wert)ODER-Filter hinzufügen

Häufig verwendete Compare-Werte:

WertBedeutung
Compare.EqualsGleich
Compare.NotEqualsUngleich
Compare.ContainsEnthält
Compare.GreaterThanGrößer als
Compare.LessThanKleiner als
Compare.IsNullIst leer (null)
Compare.IsNotNullIst nicht leer
Compare.IsTrue / Compare.IsFalseBoolean-Prüfung

User — Benutzer-Informationen

Zugriff auf den angemeldeten Benutzer.

Property / MethodeBeschreibung
User.UsernameBenutzername
User.UserIdBenutzer-ID (Guid)
User.RoleIdRollen-ID
User.PositionIdPositions-ID
User.GetUserFieldValue("feld")Beliebigen Feldwert des Benutzers lesen

Display — UI-Helfer (Dialoge und Navigation)

Für Meldungsdialoge, Bestätigungsabfragen und Navigation.

MethodeBeschreibung
Display.ShowDialogAndWaitAsync("titel", "text")Hinweisdialog anzeigen (wartet auf Schließen)
Display.ShowConfirmationDialogAsync("titel", "text")Ja/Nein-Bestätigungsdialog
Display.OpenModalDialog("dialogName", null)Popup-Dialog öffnen (gibt Datensatz zurück)
Display.OpenDialogNewPage("dialogName", pkWert)Dialog als neue Seite öffnen
Display.ShowFileUploadDialogAsync("titel", "text")Datei-Upload-Dialog

Weitere API-Objekte

APIBeschreibungVerfügbar in
DataAccessDirekter Datenbankzugriff (CRUD) — nur serverseitigEntityCode
DatasourceDatenquellen-Operationen (Lesen, Speichern, Löschen)FormCode, FormActionCode
NumberRangeNummernkreise — neue Nummern generierenEntityCode, FormCode
PermissionBerechtigungs-Flags setzen (Read, ReadWrite, Delete, ...)PermissionCode
SortSortierung setzenFormCode, DatasourceCode
ParameterParameter lesen und setzenFormElementCode, ViewElementCode
ReportingBerichte erzeugen und anzeigenFormCode, FormActionCode
ImportDataCSV-Import durchführenFormActionCode
FormElementZugriff auf das einzelne FormularfeldFormElementCode
Code-Container im Dialog: Hook-Auswahl und Editor
Container & Hook
Container-Typ
FormCode
Trigger / Hook
OnFieldValueChanged
Aktiv
Live-Reload
✓ (Admin Client)
Asynchrone Methoden Methoden mit dem Suffix Async müssen mit await aufgerufen werden, z. B. await Display.ShowDialogAndWaitAsync("Titel", "Text").
5

Code-Editor Features

Der integrierte Monaco-Editor bietet eine komfortable Entwicklungsumgebung direkt im Admin Client:

  • Syntax-Highlighting: C#-Code wird farblich hervorgehoben — Schlüsselwörter, Strings, Kommentare etc.
  • Funktionsbaum: Auf der linken Seite sehen Sie alle verfügbaren API-Objekte und deren Methoden, gruppiert nach Kategorie (DataItem, Form, Filter, ...)
  • Per Klick einfügen: Klicken Sie auf eine Methode im Funktionsbaum, um den passenden Aufruf an der Cursor-Position einzufügen
  • Live-Reload: Im Admin Client wird geänderter Code bei der nächsten Auslösung sofort ausgeführt — kein Neustart nötig
  • Fehleranzeige: Kompilier- und Laufzeitfehler werden dem Benutzer als Fehlermeldung angezeigt
Beispiel-Snippet: OnBeforeSaving mit Pflichtfeld-Prüfung
</>
FormCode · OnBeforeSaving
if (DataItem.IsNullOrEmpty("kunde_id"))
{
    await Display.ShowDialogAndWaitAsync(
        "Fehler", "Bitte einen Kunden auswählen.");
    CancelSaving = true;
}
6

Rezeptkarten — Häufige Anwendungsfälle

Rezept
Feldvalidierung vor dem Speichern

Container: FormCode  |  Hook: OnBeforeSaving
Prüft, ob ein Pflichtfeld ausgefüllt ist. Falls nicht, wird eine Fehlermeldung angezeigt und das Speichern abgebrochen.

// Hook: OnBeforeSaving
if (DataItem.IsNullOrEmpty("pflichtfeld"))
{
    await Display.ShowDialogAndWaitAsync("Fehler",
        "Das Pflichtfeld muss ausgefüllt sein!");
    CancelSaving = true;
}
Rezept
Felder automatisch berechnen

Container: FormCode  |  Hook: OnFieldValueChanged
Setzt den Anzeigenamen automatisch zusammen, wenn Vorname oder Nachname geändert wird.

// Hook: OnFieldValueChanged
if (Form.ValueChangedFieldName == "vorname"
    || Form.ValueChangedFieldName == "nachname")
{
    var vorname = DataItem.GetDataValue("vorname")?.ToString() ?? "";
    var nachname = DataItem.GetDataValue("nachname")?.ToString() ?? "";
    Form.SetValue("anzeigename", $"{vorname} {nachname}");
}
Rezept
Felder und Unterdialoge bedingt ein-/ausblenden

Container: FormCode  |  Hook: OnFieldValueChanged
Blendet ein Detailfeld ein, wenn der Typ auf „Erweitert“ steht.

// Hook: OnFieldValueChanged
if (Form.ValueChangedFieldName == "typ")
{
    var typ = DataItem.GetDataValue("typ")?.ToString();
    Form.SetHidden("detail_bereich", typ != "Erweitert");
    Form.ShowSubdialog("erweiterte_daten", typ == "Erweitert");
}
Rezept
Folgedatensatz beim Speichern erzeugen (Server)

Container: EntityCode  |  Hook: OnAfterSaveEntity
Erzeugt nach dem Anlegen eines neuen Datensatzes automatisch einen Eintrag in einer Zieltabelle.

// Hook: OnAfterSaveEntity
if (IsNewRecord)
{
    var neuerDatensatz = DataAccess.CreateDataItem();
    neuerDatensatz.SetValue("quelltabelle_id",
        DataItem.GetDataValue("id"));
    neuerDatensatz.SetValue("status", "Neu");
    await DataAccess.CreateEntityDataItem(
        "ZielEntitaet", neuerDatensatz);
}
Rezept
Filter und Sortierung auf einer Datenquelle

Container: DatasourceCode  |  Hook: OnBeforeGetData
Filtert die Datenquelle auf aktive Datensätze der letzten 30 Tage und sortiert absteigend.

// Hook: OnBeforeGetData
Filter.SetFilter("status", Compare.Equals, "Aktiv");
Filter.AndFilter("erstelldatum",
    Compare.GreaterThanOrEqual,
    DateTime.Today.AddDays(-30));
Sort.SetOrderBy("erstelldatum", true);
Rezept
Berechtigungen dynamisch setzen

Container: PermissionCode  |  Hook: GetPermissions
Abteilungsleiter erhalten Schreib- und Löschrechte, alle anderen nur Leserechte.

// Hook: GetPermissions
if (User.RoleId == new Guid("...abteilungsleiter-guid..."))
{
    Permission.ReadWrite = true;
    Permission.Delete = true;
}
else
{
    Permission.Read = true;
    Permission.ReadWrite = false;
}
Rezept
Bedingte Zeilenformatierung (Farbmarkierung)

Container: ItemAppearance  |  Hook: GetAppearanceCondition
Färbt überfällige Zeilen rot ein.

// Hook: GetAppearanceCondition
var status = DataItem.GetDataValue("status")?.ToString();
if (status == "Ueberfaellig")
{
    IsActive = true;
    CellStyle = "background-color: #ffcccc;";
}
Rezept
Nummernkreis für automatische Belegnummern

Container: FormCode  |  Hook: OnBeforeSaving
Vergibt automatisch eine Belegnummer aus einem Nummernkreis, falls das Feld noch leer ist.

// Hook: OnBeforeSaving
if (DataItem.IsNullOrEmpty("belegnummer"))
{
    var nummer = await NumberRange.GetNewNumberAsync(
        new Guid("...nummernkreis-guid..."));
    Form.SetValue("belegnummer", nummer);
}
Rezept
Massendaten aus Kreuzprodukt erzeugen — BulkCreate (Server)

Container: EntityCode  |  Hook: OnAfterSaveEntity
Erzeugt automatisch Datensätze aus dem Kreuzprodukt zweier Entities. Beispiel: Wenn eine Schulung auf „Aktiv“ gesetzt wird, werden für jeden Teilnehmer × jeden Block Anwesenheitsdatensätze angelegt.

// Hook: OnAfterSaveEntity
if (!IsNewRecord
    && DataAccess.IsFieldValueChanged("SchulungStatusId"))
{
    var schulungId = DataItem.GetDataValue("SchulungId");
    var count = await DataAccess.BulkCreate("SpSchulungBlockZuweisung")
        .FromCrossJoin("Teilnahme", "SchulungBlock")
        .Where("SchulungId", schulungId)
        .Map("TeilnehmerId", "Teilnahme")
        .Map("SchulungBlockId", "SchulungBlock")
        .Map("SchulungId", schulungId)
        .Map("Status", "Geplant")
        .Execute();
}
BulkCreate — Fluent-API

Die Methode DataAccess.BulkCreate("zielEntity") gibt einen Builder zurück, der mit folgenden Methoden konfiguriert wird:

.FromCrossJoin("A", "B")Quell-Entities für das Kreuzprodukt
.Where("feld", wert)Filter auf beide Quell-Entities
.Map("zielFeld", "quellEntity")Feld aus Quell-Entity übernehmen
.Map("zielFeld", "quellEntity", "quellFeld")Feld mit anderem Namen übernehmen
.Map("zielFeld", konstanterWert)Festen Wert setzen
.Execute()Ausführen — gibt Anzahl erzeugter Datensätze zurück
Rezept
Mehrere Datensätze lesen mit Filter (Server)

Container: EntityCode  |  Hook: OnAfterSaveEntity
Liest alle Datensätze einer Entity mit einfachem Filter.

// Hook: OnAfterSaveEntity
var schulungId = DataItem.GetDataValue("SchulungId");
var teilnahmen = await DataAccess.GetEntityData(
    "Teilnahme", "SchulungId", schulungId,
    "TeilnehmerId", "TeilnahmeStatusId");

foreach (var t in teilnahmen)
{
    var name = t.GetDataValueAsString("TeilnehmerId");
    // ... weitere Verarbeitung
}
7

Tipps und Warnungen

Best Practices
  • Halten Sie Code-Snippets kurz und fokussiert — ein Hook, ein Zweck.
  • Nutzen Sie Kommentare, damit auch Kollegen den Code verstehen.
  • Verwenden Sie den Funktionsbaum im Editor, um die korrekte Methodensignatur einzufügen.
  • Testen Sie neuen Code zuerst im Admin Client, bevor Sie ihn für Endanwender freigeben.
  • Prüfen Sie Feldnamen auf exakte Schreibweise — Tippfehler führen zu Laufzeitfehlern.
Sicherheit & Performance
  • Endlosschleifen vermeiden: Wenn OnFieldValueChanged selbst ein Feld setzt, kann das erneut OnFieldValueChanged auslösen. Prüfen Sie immer Form.ValueChangedFieldName, um nur auf das relevante Feld zu reagieren.
  • Keine schweren Operationen in Schleifen: Vermeiden Sie await-Aufrufe innerhalb von Schleifen über große Datenmengen.
  • Server-Code ist verbindlich: EntityCode läuft serverseitig und kann nicht vom Client umgangen werden — ideal für sicherheitskritische Validierungen.
  • Fehlerbehandlung: Laufzeitfehler im Code werden dem Benutzer als Fehlermeldung angezeigt. Fangen Sie erwartbare Fehler ab, um verständliche Meldungen zu liefern.
  • Feldnamen sind technische Namen: Verwenden Sie die technischen Feldnamen der Entität, nicht die Anzeigenamen.
Achtung — Änderungen wirken sofort Dynamischer Code wirkt sich auf alle Anwender aus, die den betroffenen Dialog, die Datenquelle oder das Ansichtselement nutzen. Testen Sie Änderungen immer zuerst im Admin Client, bevor Sie sie in einer Produktivumgebung freischalten.
8

Häufig gestellte Fragen (FAQ)

Welche Programmiersprache wird verwendet?

Dynamischer Code wird in C# geschrieben. Es handelt sich um Code-Snippets, die zur Laufzeit kompiliert und ausgeführt werden. Sie benötigen kein eigenes Projekt und keinen Compiler — der Code wird direkt im Admin Client eingegeben.

Muss ich await verwenden?

Ja, bei allen Methoden mit dem Suffix Async. Ohne await wird die Methode zwar aufgerufen, aber das Ergebnis wird nicht abgewartet, was zu unerwartetem Verhalten führen kann. Beispiel:

// Richtig:
await Display.ShowDialogAndWaitAsync("Titel", "Text");

// Falsch (Dialog wird nicht abgewartet):
Display.ShowDialogAndWaitAsync("Titel", "Text");

Was passiert bei einem Fehler im Code?

Kompilier- und Laufzeitfehler werden dem Benutzer als Fehlermeldung angezeigt. Der fehlerhafte Code verhindert die Ausführung des Hooks, aber die Anwendung läuft weiter. Im Admin Client können Sie den Code sofort korrigieren und erneut testen.

Was ist der Unterschied zwischen Form.SetValue und DataItem.SetValue?

Form.SetValue setzt den Wert im Formular (UI-seitig) — die Änderung ist sofort sichtbar und löst ggf. ein OnFieldValueChanged aus. DataItem.SetValue ändert den Wert direkt im Datenobjekt — vor allem in Server-Containern (EntityCode) relevant, wo kein Formular existiert.

Kann ich auf die Datenbank zugreifen?

Ja — auf zwei Wegen:

  • Serverseitig (EntityCode): Über DataAccess können Sie direkt Datensätze lesen, anlegen, ändern und löschen. Mit DataAccess.BulkCreate() können Sie Massendaten aus dem Kreuzprodukt zweier Entities erzeugen. Mit DataAccess.GetEntityData() können Sie mehrere Datensätze mit einfachem Filter lesen.
  • Clientseitig (FormCode, FormActionCode): Über Datasource können Sie Daten über Entitäten und Datenquellen lesen und speichern.

Kann ich Variablen zwischen Hooks teilen?

Ja, über Form.SetVariable("name", wert) und Form.GetVariable("name") können Sie Werte zwischen verschiedenen Hooks desselben Formulars austauschen.

Wie verhindere ich das Speichern bei einer Validierung?

Im Hook OnBeforeSaving setzen Sie CancelSaving = true. Zeigen Sie dem Benutzer vorher mit Display.ShowDialogAndWaitAsync eine verständliche Fehlermeldung an.

Wie finde ich die technischen Feldnamen?

Die technischen Feldnamen finden Sie in der Entitäts-Konfiguration im Admin Client. Jede Spalte hat einen internen Namen (z. B. kunden_name), der im Code verwendet werden muss. Der Anzeigename (z. B. „Kundenname“) funktioniert im Code nicht.