Mit dem vierten Tag des Adventskalenders verändert sich die Perspektive auf die Anwendung grundlegend. Während die Benutzer in den bisherigen Ausbaustufen vor allem auf vorbereitete Strukturen reagierten, tritt nun ein Element der aktiven Gestaltung hinzu. Die „Overview“ war bislang ein zentraler Spiegel des Systemzustands: Sie zeigte alle gespeicherten Kurzlinks, erlaubte deren Filterung und lieferte mit dem Detaildialog einen vertieften Einblick in einzelne Objekte. Doch die Struktur und Sichtbarkeit der Spalten waren bis zu diesem Punkt unveränderlich. Der Benutzer sah stets die Auswahl, die der Entwickler als sinnvoll definiert hatte. Mit der Einführung der dynamischen Spaltensichtbarkeit wird dieser Zustand aufgehoben.
Der Quelltext für diese Version befindet sich auf GitHub unter https://github.com/svenruppert/url-shortener/tree/feature/advent-2025-day-04
Hier ist der Screenshot der Version, die wir nun implementieren.


Das Grid in der OverviewView wird damit von einem statischen Anzeigeelement zu einem personalisierbaren Werkzeug. Die Benutzer können selbst bestimmen, welche Spalten sichtbar sind und welche ausgeblendet werden. Dieser Schritt erscheint auf den ersten Blick geringfügig, führt jedoch zu einer neuen Form der Interaktion zwischen Benutzer und Anwendung. Nicht länger legt das System die Oberflächenstruktur fest, sondern reagiert auf individuelle Präferenzen. Die Anwendung tritt in einen Dialog mit dem Benutzer ein, in dem die Sicht auf Daten Teil der Personalisierung wird.
Diese Erweiterung knüpft unmittelbar an die in den vorherigen Tagen geschaffene Grundlage an. Die Filter- und Suchmechanismen aus dem ersten Teil haben die Kontrolle über den Datenfluss übernommen. Die Integration von EclipseStore hat den Zustand sichtbar gemacht und zugleich die dauerhafte Speicherung im System etabliert. Der Detaildialog schuf schließlich die Trennung zwischen der Übersicht und dem Objektkontext. Vor diesem Hintergrund bildet die neue Funktion den nächsten logischen Schritt: Sie verschiebt die Kontrolle über die Darstellung der Daten vom System hin zum Benutzer, ohne die zugrunde liegende technische Struktur zu verkomplizieren.
Aus Sicht der Benutzererfahrung ist dieser Wandel erheblich. Wer täglich mit der Administrationsoberfläche arbeitet, entwickelt einen eigenen Rhythmus im Umgang mit Daten. Manche Benutzer konzentrieren sich auf ablaufende Links, andere auf manuelle Aliase oder auf den Traffic ihrer Kurzlinks. Für alle diese Szenarien ist eine statische Spaltenanordnung hinderlich. Die Möglichkeit, irrelevante Informationen auszublenden und die Ansicht auf die eigenen Arbeitsprozesse zuzuschneiden, schafft nicht nur Komfort, sondern auch Effizienz. Der Benutzer wird so vom passiven Beobachter zum aktiven Gestalter seiner Arbeitsumgebung.
Die Umsetzung dieses Prinzips in Vaadin basiert auf einem bewussten Gleichgewicht zwischen Einfachheit und Persistenz. Einerseits soll die Funktion unmittelbar verfügbar sein: Ein Klick öffnet einen Dialog, eine Handvoll Checkboxen steuert die Sichtbarkeit der Spalten, und das Ergebnis wird sofort sichtbar. Andererseits sollen diese Einstellungen nicht flüchtig bleiben. Die gewählte Konfiguration wird dauerhaft gespeichert und bei jedem Neustart automatisch wiederhergestellt. Diese Persistenz im visuellen Verhalten greift die Idee des sichtbaren Systemzustands wieder auf und überträgt sie auf die Benutzerebene.
Mit der Einführung der dynamischen Spaltensichtbarkeit beginnt somit ein neues Kapitel in der Entwicklung der Administrationsoberfläche: Es geht nicht mehr allein um die Darstellung von Daten, sondern um die Gestaltung des eigenen Arbeitskontextes. Diese Funktion markiert einen Paradigmenwechsel im UI-Design des Projekts. Statt vorgegebener Strukturen entsteht eine flexible, lernende Oberfläche, die sich an individuelle Arbeitsweisen anpasst und damit einen entscheidenden Schritt hin zu echter Benutzerzentrierung macht.
Konzept: Sichtbarkeit als Benutzerpräferenz
Die Einführung der dynamischen Spaltensichtbarkeit markiert nicht nur eine ergonomische Verbesserung der Benutzeroberfläche, sondern auch eine konzeptionelle Erweiterung der Anwendungsarchitektur. Zum ersten Mal wird ein Aspekt der Benutzerinteraktion dauerhaft personalisiert – unabhängig von der Session oder dem Systemzustand. Diese Neuerung erfordert ein eigenes Modell für Präferenzen, das sowohl auf der Client- als auch auf der Serverseite konsistent funktioniert. Sichtbarkeit wird damit zu einer gespeicherten Eigenschaft, die zwischen Benutzer und Anwendung vermittelt wird.
Die zentrale Idee besteht darin, dass jede Ansicht in der Anwendung über eine eigene Identität verfügt. Für die OverviewView bedeutet das, dass ihre Konfiguration – etwa welche Spalten sichtbar sind – eindeutig unter einer View-ID gespeichert wird. Parallel dazu wird jede Benutzerinstanz, etwa mittels einer User-ID, identifiziert und als Träger individueller Präferenzen behandelt. Diese beiden Dimensionen – Benutzer und Ansicht – bilden den Schlüssel zur Speicherung von Sichtbarkeitsinformationen. Das Konzept folgt somit einem klaren Schema: einer Benutzer-ID, einer View-ID und einer Zuordnung von Spaltennamen zu Wahrheitswerten.
Die Einführung dieser Logik erforderte eine neue Schnittstelle zwischen der UI und dem Backend. Die Anwendung musste lernen, mit semantischen Informationen zu Benutzerentscheidungen umzugehen, nicht nur mit Datenobjekten. Auf der Serverseite übernimmt dies der neue PreferencesHandler, der als REST-Endpunkt dient und sowohl das Laden als auch das Speichern der Einstellungen ermöglicht. Auf der Clientseite kommuniziert die neue Komponente PreferencesClient mit diesem Handler und verwaltet die Übertragung der Datenstrukturen. Das verbindende Format bleibt dabei bewusst einfach: eine Map<String, Boolean>, in der jeder Schlüssel dem Spaltennamen entspricht und der Wert die Sichtbarkeit beschreibt. Diese Klarheit ermöglicht, den Mechanismus generisch zu halten und ihn später auch auf andere Bereiche der Anwendung auszudehnen.
Die Entscheidung, Präferenzen als eigenständige Entität zu modellieren, stellt einen wichtigen architektonischen Schritt dar. Sie trennt Anwendungsdaten – wie Kurzlinks und Ablaufinformationen – von den Präsentationsdaten, die das Verhalten des Benutzers innerhalb der Oberfläche steuern. Auf diese Weise wird die Persistenzschicht um eine neue Kategorie erweitert: nicht mehr nur um fachliche, sondern auch um kontextuelle Zustände. Dieses Muster erinnert an die Trennung zwischen Domänenlogik und UI-Logik, die im Clean-Architecture-Ansatz zentral ist, und überträgt sie konsequent auf das Verhalten der Benutzeroberfläche.
Ein weiteres Ziel der Implementierung ist die Transparenz. Die Präferenzen sollen nicht als Blackbox wirken, sondern jederzeit nachvollziehbar bleiben. Änderungen an der Spaltenkonfiguration werden deshalb unmittelbar sichtbar und zugleich gespeichert. Der Benutzer spürt keinen Bruch zwischen der Interaktion und dem Ergebnis, und dennoch wird sein Verhalten dauerhaft bewahrt. Diese doppelte Natur – flüchtige Reaktion und persistente Speicherung – macht das System zu einem lernenden Werkzeug. Die Anwendung merkt sich, wie ihre Benutzer arbeiten, und reproduziert dieses Verhalten automatisch beim nächsten Aufruf.
Damit erweitert sich das UI-Konzept um eine semantische Schicht: Sichtbarkeit wird nicht mehr als technische Eigenschaft der Grid-Komponente verstanden, sondern als Ausdruck einer individuellen Arbeitsweise. Die Anwendung selbst wird zu einem personalisierbaren Medium, das sich an den Menschen anpasst, nicht umgekehrt. In dieser Perspektive wird die Dynamik der Spaltensichtbarkeit zu einem Symbol für den Übergang von standardisierter Bedienung zu personalisierter Kontrolle – einem Prinzip, das sich in den folgenden Kapiteln auch in der technischen Umsetzung widerspiegeln wird.
Die neue UI-Interaktion: ColumnVisibilityDialog
Mit der Einführung der dynamischen Spaltensichtbarkeit erhält die Benutzeroberfläche der Anwendung eine neue Interaktionsebene. Der bisher unveränderliche Aufbau der OverviewView wird um eine Komponente erweitert, die es dem Benutzer ermöglicht, die Struktur seiner Arbeitsumgebung direkt zu beeinflussen. Im Zentrum steht dabei der ColumnVisibilityDialog – ein Dialogfenster, das alle verfügbaren Spalten der Übersichtstabelle auflistet und deren Sichtbarkeit über einfache Checkboxen steuert. Er verkörpert die Idee einer konfigurierbaren, aber intuitiv bedienbaren Oberfläche.
Das Konzept des Dialogs folgt einer bewussten Reduktion: kein überladenes Einstellungsmenü, keine versteckten Optionen, sondern ein klarer, fokussierter Ort für eine einzige Aufgabe. Beim Öffnen des Dialogs liest die Anwendung die gespeicherten Präferenzen des aktuellen Benutzers über den PreferencesClient ein und setzt die Checkboxen entsprechend den gespeicherten Werten. Jede Checkbox repräsentiert eine Spalte der OverviewView – etwa „Shortcode“, „URL“, „Created“ oder „Expires“. Änderungen werden direkt sichtbar: Sobald der Benutzer eine Checkbox an- oder abwählt, wird die entsprechende Spalte im Grid ein- oder ausgeblendet. Damit entsteht ein unmittelbares Wechselspiel zwischen Aktion und Reaktion, das die Wahrnehmung von Kontrolle stärkt.
Der ColumnVisibilityDialog ist bewusst als eigenständige Komponente implementiert. Er erweitert die Vaadin-Klasse Dialog und folgt derselben architektonischen Philosophie wie der bereits bekannte DetailsDialog aus den vorherigen Tagen. Durch diese Parallele fügt er sich nahtlos in die bestehende Struktur ein: Er verfügt über eine klare Lifecycle-Logik, eigene Ereignisse sowie ein minimalistisches Layout, das auf Verständlichkeit und Effizienz ausgelegt ist. Der Dialog enthält typischerweise eine vertikale Liste von Checkboxen, die dynamisch aus der Spaltenkonfiguration des Grids generiert wird. Eine Schaltfläche zum Speichern aktualisiert die Präferenzen dauerhaft über den PreferencesClient, während ein „Reset“-Button die ursprüngliche Standardansicht wiederherstellt.
Technisch betrachtet basiert der Dialog auf einem einfachen, aber eleganten Datenfluss. Beim Öffnen wird über den Client eine Map<String, Boolean> geladen, deren Schlüssel den Spaltenbezeichnern entsprechen. Diese Map wird in UI-Elemente umgewandelt, wobei die aktuelle Sichtbarkeit direkt im Grid gespiegelt wird. Ein Klick auf „Save“ triggert die Methode saveColumnVisibilities(userId, viewId, visibilityMap), die die geänderten Einstellungen an den Server übermittelt. Das JSON-Format bleibt bewusst flach, um sowohl menschlich lesbar als auch leicht testbar zu sein. Die serverseitige Speicherung in EclipseStore sorgt dafür, dass diese Änderungen nicht nur für die aktuelle Sitzung, sondern auch dauerhaft erhalten bleiben.
Das Zusammenspiel von Dialog, Grid und Client zeigt die Stärke des modularen Ansatzes. Der ColumnVisibilityDialog kennt keine Details zur Persistenz oder zur Grid-Implementierung – er arbeitet ausschließlich über klar definierte Schnittstellen. Dadurch bleibt die Komponente unabhängig, wiederverwendbar und leicht erweiterbar. Dieses Muster ist in der Vaadin-Architektur von besonderer Bedeutung, da es die lose Kopplung zwischen Benutzerinteraktion und Systemzustand gewährleistet.
Aus Sicht der Benutzererfahrung erfüllt der Dialog zwei Aufgaben gleichzeitig. Er bietet Kontrolle ohne Komplexität und schafft Vertrauen in die Stabilität des Systems. Die Benutzer erleben, dass ihre individuellen Entscheidungen nicht nur vorübergehend wirken, sondern strukturell in der Anwendung verankert werden. Jede Anpassung ist reproduzierbar, jede Änderung ist reversibel. Dadurch entsteht eine natürliche Balance zwischen Freiheit und Sicherheit, die den Charakter des gesamten Projekts widerspiegelt. Der ColumnVisibilityDialog wird so zu einem sichtbaren Ausdruck der Idee, dass eine Administrationsoberfläche nicht nur Werkzeuge bereitstellen, sondern sich auch an die Arbeitsweise ihrer Benutzer anpassen soll.
Zur Veranschaulichung folgt ein Originalausschnitt aus dem Dialog, der die Kopplung zwischen Checkboxen und Grid-Spalten sowie das Sammeln von Änderungen für eine optionale Sammelpersistenz zeigt. Der Code arbeitet direkt an den Spaltenschlüsseln des Grids und spiegelt Zustandsänderungen ohne Umwege in die Oberfläche zurück.
// com.svenruppert.urlshortener.ui.vaadin.components.ColumnVisibilityDialog
// Auszug: Checkbox-Bindung und Bulk-Änderungssammlung
// Für Bulk-Apply (optional) sammeln wir Änderungen
Map<String, Boolean> pending = new LinkedHashMap<>();
grid.getColumns().forEach(col -> {
final String key = col.getKey();
if (key == null) return; // nur adressierbare Spalten
boolean visible = state.getOrDefault(key, true);
col.setVisible(visible);
var cb = new Checkbox(col.getHeaderText() != null ? col.getHeaderText() : key, visible);
cb.addValueChangeListener(ev -> {
boolean v = Boolean.TRUE.equals(ev.getValue());
col.setVisible(v);
pending.put(key, v);
});
form.add(cb);
});
var btnApply = new Button("Apply bulk", _ -> {
if (!pending.isEmpty()) {
service.setBulk(new LinkedHashMap<>(pending));
pending.clear();
}
close();
});
btnApply.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
Der Dialog delegiert die eigentliche Persistenz bewusst an den Service. Dieser Knotenpunkt abstrahiert den Transport und schreibt die geänderten Sichtbarkeiten in einem Rutsch auf den Server. Das folgende Fragment zeigt die dazugehörige Methode im Service, die den Roundtrip kapselt und zugleich fehlertolerant bleibt.
// com.svenruppert.urlshortener.ui.vaadin.tools.ColumnVisibilityService
// Auszug: Bulk-Persist der Spaltensichtbarkeiten
public void setBulk(Map<String, Boolean> changes) {
if (changes == null || changes.isEmpty()) return;
try {
client.editBulk(userId, viewId, changes);
} catch (IOException | InterruptedException e) {
logger().warn("Persist bulk failed {}: {}", changes.keySet(), e.toString());
}
}
Auf diese Weise bleibt der ColumnVisibilityDialog schlank und fokussiert auf die Interaktion, während die Persistenzlogik zentral über den Service und den zugrunde liegenden Client abgewickelt wird. Diese Teilung der Verantwortlichkeiten stellt sicher, dass sich die Benutzerführung klar und reaktionsfreudig anfühlt, ohne die technische Integrität der Anwendung zu gefährden.
Integration in die OverviewView
Die Einführung des ColumnVisibilityDialog wäre unvollständig geblieben, wenn sie nicht organisch in die bestehende OverviewView eingebettet worden wäre. Genau an dieser Stelle zeigt sich die Stärke der Architektur, die von Anfang an auf lose Kopplung und klare Zuständigkeiten ausgelegt war. Die OverviewView dient als zentrales Fenster der Anwendung – sie verbindet Daten, Filter, Paging und Interaktion. Nun erweitert sie dieses Spektrum um eine persistente Personalisierungsdimension.
Im Zentrum der Integration steht ein unscheinbarer, aber bedeutungsvoller Button in der Suchleiste: das Zahnrad-Icon. Dieses Symbol fungiert als Einstiegspunkt in die Konfigurationslogik. Die Entscheidung, den Dialog nicht in einem Menü oder in einer separaten Ansicht zu verstecken, folgt der Idee einer jederzeit verfügbaren Kontrolle. Die Benutzer sollen die Darstellung der Daten genau dort anpassen können, wo sie sie betrachten. Der Codeausschnitt aus der OverviewView verdeutlicht diese direkte Einbindung:
btnSettings.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
btnSettings.getElement().setProperty("title", "Column visibility");
btnSettings.addClickListener(_ -> new ColumnVisibilityDialog<>(grid, columnVisibilityService).open());
Mit nur wenigen Zeilen wird die gesamte Funktionalität angebunden. Der Button öffnet eine neue Instanz des Dialogs, die das aktuelle Grid und die zugehörige Serviceinstanz erhält. Diese Serviceinstanz verwaltet alle Operationen zu den gespeicherten Präferenzen. Auf diese Weise entsteht ein klarer Datenfluss: Die OverviewView ruft den Dialog auf, der Dialog interagiert mit dem ColumnVisibilityService, und dieser kommuniziert wiederum über den ColumnVisibilityClient mit dem Server.
Die Initialisierung des Services erfolgt beim Anfügen der View an die UI. Diese Phase ist ideal, um Benutzerkontexte zu laden und initiale Zustände zu erstellen. Der relevante Codeblock aus der Methode onAttach zeigt diesen Ablauf:
@Override
protected void onAttach(AttachEvent attachEvent) {
super.onAttach(attachEvent);
this.columnVisibilityService = new ColumnVisibilityService(columnVisibilityClient, "admin", "overview");
var keys = grid.getColumns().stream()
.map(Grid.Column::getKey)
.filter(Objects::nonNull)
.toList();
var vis = columnVisibilityService.mergeWithDefaults(keys);
grid.getColumns().forEach(c -> {
var k = c.getKey();
if (k != null) c.setVisible(vis.getOrDefault(k, true));
});
refresh();
subscription = StoreEvents.subscribe(_ -> getUI().ifPresent(ui -> ui.access(this::refresh)));
}
Beim Aufbau der Ansicht wird zunächst der Service erzeugt und mit Benutzer- und View-ID verknüpft. Anschließend werden alle bekannten Spaltennamen ermittelt und deren Sichtbarkeit mit den gespeicherten Präferenzen abgestimmt. Jede Spalte wird daraufhin entsprechend ihrer gespeicherten Einstellung angezeigt oder ausgeblendet. Auf diese Weise sind die Spaltenzustände nicht flüchtig, sondern konsistent zwischen Sitzungen und Neustarts.
Diese Integration ist bewusst dezent umgesetzt. Der Dialog wird nur aufgerufen, wenn der Benutzer aktiv wird, und der Service berücksichtigt dabei die gespeicherten Einstellungen automatisch. So wird die Anwendung nicht durch neue Interaktionen überladen, sondern ihr Verhalten wird organisch um ein kontextbewusstes Gedächtnis erweitert.
Ein Aspekt dieser Architektur liegt in der Art, wie sie die Balance zwischen Kontrolle und Einfachheit bewahrt. Der Benutzer muss keine Konfiguration laden oder speichern – alles geschieht im Hintergrund. Die View bleibt responsiv, der Server hält den Zustand fest, und der Dialog fungiert als Schnittstelle zwischen beiden. Diese unsichtbare Verbindung zwischen Oberfläche und Persistenzschicht verleiht der Anwendung jene Leichtigkeit, die sie trotz wachsender Funktionsvielfalt intuitiv und effizient hält.
Cheers Sven