UserControls in WinUI 3: SeriesTileControl und DependencyProperties (Teil 31)

Sobald du in einer App dieselbe Kachel an drei verschiedenen Stellen brauchst, hast du ein Problem. Entweder kopierst du das XAML dreimal — und pflegst ab sofort drei identische Codeblöcke — oder du baust ein UserControl. In EchoPlay tauchen Serien-Kacheln mit Cover, Titel, Overlay und Kontextmenü in der lokalen Mediathek, der Online-Mediathek und dem Dashboard auf. Ein UserControl kapselt dieses Layout einmal, und alle Pages binden sich daran. Ändert sich das Design, änderst du genau eine Datei.

Die Controls in EchoPlay

EchoPlay nutzt vier eigene UserControls, die jeweils einen klar abgegrenzten visuellen Baustein darstellen. Das wichtigste ist das SeriesTileControl: eine 148×166 Pixel große Kachel mit Cover-Bild, Titel-Overlay, Favoriten-Stern und einem konfigurierbaren Kontextmenü über einen „…“-Button. Das Entscheidende daran ist das Wort „konfigurierbar“ — das Kontextmenü wird nicht fest verdrahtet, sondern von außen als XAML-Property übergeben. Die lokale Mediathek zeigt andere Menüeinträge als die Online-Mediathek, aber das Kachel-Layout bleibt identisch.

<ctrl:SeriesTileControl
    CoverImage="{x:Bind CoverImage, Mode=OneWay}"
    Title="{x:Bind Title}"
    CountText="{x:Bind CountText}"
    StarVisibility="{x:Bind StarVisibility, Mode=OneWay}"
    SelectedIndicatorVisibility="{x:Bind SelectedIndicatorVisibility, Mode=OneWay}">
    <ctrl:SeriesTileControl.ContextFlyoutMenu>
        <MenuFlyout>
            <MenuFlyoutItem x:Uid="SeriesDetailsItem" Click="OnDetailsClick"/>
        </MenuFlyout>
    </ctrl:SeriesTileControl.ContextFlyoutMenu>
</ctrl:SeriesTileControl>

Das EpisodeTileControl ist der kleinere Bruder: eine 148×148 Pixel große Kachel für einzelne Episoden. Es zeigt Cover, Titel, Folgennummer und einen konfigurierbaren Action-Button — zum Beispiel „Im Browser öffnen“ für Online-Episoden oder „Cover suchen“ für lokale Dateien. Auch hier gilt: Das Layout ist fest, das Verhalten kommt von außen.

<ctrl:EpisodeTileControl
    CoverImage="{x:Bind CoverImage, Mode=OneWay}"
    Title="{x:Bind DisplayText}"
    ActionGlyph="&#xE712;"
    TileCommand="{x:Bind OpenInBrowserCommand}"/>

Das EpisodeAccordionControl ist das Akkordeon-Panel, das sich zwischen den Serien-Grids aufklappt. Es enthält ein GridView für Episoden mit dynamischer Breitenberechnung — die Anzahl der Kacheln pro Zeile passt sich der Fensterbreite an. Besonders interessant ist der injizierbare Header-Bereich: Die lokale Mediathek zeigt dort Filter und Tabs, die Online-Mediathek zeigt Sortierung und einen Schließen-Button. Beide nutzen dasselbe Control, nur der Header-Content unterscheidet sich.

// Dynamische Breite: passt sich der Fensterbreite an
int tilesPerRow = Math.Max(1, (int)(ActualWidth / TileSlotWidth));
OuterBorder.Width = tilesPerRow * TileSlotWidth + 4;

Das vierte Control ist das EmptyStatePanel — ein einfacher Hinweis mit Icon und optionalem Button, der angezeigt wird, wenn eine Liste leer ist. Klingt trivial, aber ohne diesen Baustein müsstest du den „Keine Einträge vorhanden“-Text in jeder Page einzeln implementieren.

DependencyProperties: Der Schlüssel zur Wiederverwendbarkeit

Damit ein UserControl Daten von außen entgegennehmen kann, braucht es sogenannte DependencyProperties. Das sind keine normalen C#-Properties — sie registrieren sich beim WinUI-3-Eigenschaftssystem und ermöglichen dadurch erst x:Bind in XAML, Animationen, visuelle Zustände und Change-Callbacks. Ohne DependencyProperty funktioniert kein Binding in XAML.

public static readonly DependencyProperty TitleProperty =
    DependencyProperty.Register(
        nameof(Title),
        typeof(string),
        typeof(SeriesTileControl),
        new PropertyMetadata(string.Empty));

public string Title
{
    get => (string)GetValue(TitleProperty);
    set => SetValue(TitleProperty, value);
}

Das Muster sieht auf den ersten Blick nach viel Boilerplate aus. Aber es ist bewusst so gestaltet: Die statische DependencyProperty.Register-Zeile definiert den Namen, den Typ, den Besitzer und einen Standardwert. Das eigentliche Property mit get und set delegiert an GetValue und SetValue — das sind Methoden aus der WinUI-Basisklasse, die das Binding-System über Änderungen informieren. Würdest du ein normales { get; set; } verwenden, würde XAML nichts davon mitbekommen, wenn sich der Wert ändert.

AccordionSplitHelper: Geteilte Logik auslagern

Wenn du eine Serie anklickst, klappt ein Akkordeon auf und die Serien-Kacheln teilen sich in zwei Gruppen: die oberhalb und die unterhalb des Akkordeons. Diese Berechnung — an welcher Stelle der Schnitt erfolgt — ist in beiden Mediathek-Pages identisch. Statt den Code zu duplizieren, steckt die Logik im AccordionSplitHelper. Beide Pages rufen dieselbe Methode auf und bekommen zwei saubere Listen zurück.

int splitIndex = AccordionSplitHelper.CalculateSplitIndex(
    selectedIndex, allSeries.Count, availableWidth);

(IReadOnlyList<T> top, IReadOnlyList<T> bottom) =
    AccordionSplitHelper.Split(allSeries, splitIndex);

Die Methode CalculateSplitIndex berücksichtigt die aktuelle Fensterbreite und die Position der angeklickten Serie. Die Split-Methode ist generisch — sie arbeitet mit jedem Listentyp, weil sie den Typparameter T verwendet. Der Rückgabetyp ist ein IReadOnlyList-Tupel, also zwei unveränderliche Listen. So kann der Aufrufer die Ergebnisse nicht versehentlich verändern.

Warum sich der Aufwand lohnt

UserControls mit DependencyProperties zu bauen fühlt sich anfangs nach Overhead an. Aber sobald du das zweite Mal dasselbe Layout brauchst, sparst du bereits Zeit. In EchoPlay nutzen drei verschiedene Pages dasselbe SeriesTileControl — mit jeweils unterschiedlichen Kontextmenüs und Verhaltensweisen. Eine Designänderung an der Kachel wirkt sich sofort überall aus. Der ViewModel-Code bleibt sauber, weil die visuelle Darstellung komplett im Control liegt. Und der AccordionSplitHelper zeigt, dass auch reine Berechnungslogik ohne UI-Bezug wiederverwendbar sein sollte, wenn sie an mehreren Stellen gebraucht wird.

Die gezeigten Code-Beispiele dienen zur Veranschaulichung. Nutzung auf eigene Verantwortung. Mehr dazu