Ü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
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.
| Hook | Beschreibung |
|---|---|
OnInitialized | Beim Initialisieren des Formulars |
OnDataLoaded | Nach dem Laden der Formulardaten |
OnFieldValueChanged | Bei Änderung eines Feldwertes |
OnBeforeSaving | Vor dem Speichern — hier können Sie mit CancelSaving = true abbrechen |
OnAfterSaving | Nach dem Speichern |
OnAfterFormClosedAsync | Nach 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).
| Hook | Beschreibung |
|---|---|
OnBeforeSaveEntity | Vor dem Speichern — IsNewRecord zeigt an, ob es ein neuer Datensatz ist |
OnAfterSaveEntity | Nach dem Speichern — z. B. Folgedatensätze erzeugen |
OnBeforeDeleteEntity | Vor dem Löschen — z. B. Abhängigkeiten prüfen |
OnAfterDeleteEntity | Nach dem Löschen — z. B. Aufräumarbeiten |
OnAfterGetData | Nach 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.
| Hook | Beschreibung |
|---|---|
OnBeforeGetData | Vor dem Laden — Filter und Sortierung setzen |
OnCreateData | Eigene Ergebnis-Datensätze erzeugen |
OnAfterGetData | Nach dem Laden — Ergebnisse nachbearbeiten |
OnProcessRow | Wird für jede einzelne Ergebniszeile aufgerufen |
Weitere Container (Spezialzwecke)
| Container | Zweck | Ort |
|---|---|---|
FormElementCode | Code für einzelne Formularfelder — Validierung, Parameter, Filter | Client |
FormActionCode | Code für Aktionen im Dialog — Sichtbarkeit, Ausführung | Client |
ViewElementCode | Code für Ansichtselemente — Filter, Bedingungen, Infokarten | Client |
ViewElementActionCode | Code für Aktionen in Ansichtselementen | Client |
PermissionCode | Dynamische Berechtigungen — Lesen, Schreiben, Löschen steuern | Client |
ItemAppearance | Bedingte Formatierung — Zeilen/Zellen farblich hervorheben | Client |
FkConfigurationCode | Filter für Lookup-Felder (Fremdschlüssel) | Server |
UdmBaseCode | Code für Nachrichtenvorlagen | Server |
ElementMappingCode | Filter für Element-Mappings | Client |
PredefinedFilterCode | Vordefinierte Filter-Bedingungen | Client |
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.
- Dialog-Konfiguration öffnen Navigieren Sie im Admin Client zum gewünschten Dialog und öffnen Sie dessen Konfiguration (über das Kontextmenü oder die Admin-Verwaltung).
- 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“).
-
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. - 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.
- 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.
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.
| Methode | Beschreibung |
|---|---|
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 / Property | Beschreibung |
|---|---|
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.ValueChangedFieldName | Name 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.
| Methode | Beschreibung |
|---|---|
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:
| Wert | Bedeutung |
|---|---|
Compare.Equals | Gleich |
Compare.NotEquals | Ungleich |
Compare.Contains | Enthält |
Compare.GreaterThan | Größer als |
Compare.LessThan | Kleiner als |
Compare.IsNull | Ist leer (null) |
Compare.IsNotNull | Ist nicht leer |
Compare.IsTrue / Compare.IsFalse | Boolean-Prüfung |
User — Benutzer-Informationen
Zugriff auf den angemeldeten Benutzer.
| Property / Methode | Beschreibung |
|---|---|
User.Username | Benutzername |
User.UserId | Benutzer-ID (Guid) |
User.RoleId | Rollen-ID |
User.PositionId | Positions-ID |
User.GetUserFieldValue("feld") | Beliebigen Feldwert des Benutzers lesen |
Display — UI-Helfer (Dialoge und Navigation)
Für Meldungsdialoge, Bestätigungsabfragen und Navigation.
| Methode | Beschreibung |
|---|---|
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
| API | Beschreibung | Verfügbar in |
|---|---|---|
DataAccess | Direkter Datenbankzugriff (CRUD) — nur serverseitig | EntityCode |
Datasource | Datenquellen-Operationen (Lesen, Speichern, Löschen) | FormCode, FormActionCode |
NumberRange | Nummernkreise — neue Nummern generieren | EntityCode, FormCode |
Permission | Berechtigungs-Flags setzen (Read, ReadWrite, Delete, ...) | PermissionCode |
Sort | Sortierung setzen | FormCode, DatasourceCode |
Parameter | Parameter lesen und setzen | FormElementCode, ViewElementCode |
Reporting | Berichte erzeugen und anzeigen | FormCode, FormActionCode |
ImportData | CSV-Import durchführen | FormActionCode |
FormElement | Zugriff auf das einzelne Formularfeld | FormElementCode |
Async müssen mit await aufgerufen werden,
z. B. await Display.ShowDialogAndWaitAsync("Titel", "Text").
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
if (DataItem.IsNullOrEmpty("kunde_id"))
{
await Display.ShowDialogAndWaitAsync(
"Fehler", "Bitte einen Kunden auswählen.");
CancelSaving = true;
}
Rezeptkarten — Häufige Anwendungsfälle
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;
}
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}");
}
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");
}
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);
}
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);
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;
}
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;";
}
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);
}
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();
}
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 |
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
}
Tipps und Warnungen
- 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.
- Endlosschleifen vermeiden: Wenn
OnFieldValueChangedselbst ein Feld setzt, kann das erneutOnFieldValueChangedauslösen. Prüfen Sie immerForm.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.
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
DataAccesskönnen Sie direkt Datensätze lesen, anlegen, ändern und löschen. MitDataAccess.BulkCreate()können Sie Massendaten aus dem Kreuzprodukt zweier Entities erzeugen. MitDataAccess.GetEntityData()können Sie mehrere Datensätze mit einfachem Filter lesen. - Clientseitig (FormCode, FormActionCode): Über
Datasourcekö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.