.NET MAUI Handler verstehen: Custom Controls ohne Custom Renderer in 2026

Wie .NET MAUI 10 Handler funktionieren, wann ein eigener Handler nötig ist und wie wir Custom Controls ohne alte Xamarin Renderer bauen – mit Code für iOS und Android.

.NET MAUI 10 Handler Guide (2026)

Aktualisiert: 27. Juni 2026

Ein .NET MAUI Handler ist eine schlanke Brücke zwischen einem plattformunabhängigen MAUI-Control (dem VirtualView) und seiner nativen Entsprechung auf iOS, Android, Mac Catalyst oder Windows (dem PlatformView). Statt – wie bei Xamarin.Forms – ein ganzes Renderer-Subclass zu bauen, beschreiben wir in einem PropertyMapper nur noch, welche Eigenschaft welche native Methode triggert. Das Ergebnis: weniger Allokationen, deutlich schnellere View-Erstellung und sauber testbarer Code – wenn man weiß, wo die Stolperfallen liegen.

  • Handler ersetzen Xamarin Custom Renderer – sie sind partial classes mit PropertyMapper und CommandMapper statt vererbungsbasierter Subclasses.
  • Ein Handler hält genau zwei Referenzen: VirtualView (MAUI-Control) und PlatformView (nativer View). Beide werden lazy erzeugt und bei Re-Parenting korrekt entkoppelt.
  • Für 90 % der Fälle reicht es, in MauiProgram.cs den Mapper eines vorhandenen Handlers zu erweitern – ein eigener Handler ist seltener nötig, als wir denken.
  • In .NET MAUI 10 sind Microsoft.Maui.Controls.Compatibility und damit auch alte Renderer noch verfügbar, aber als legacy markiert – wir migrieren neue Features konsequent auf Handler.
  • Handler sind unit-testbar, ohne ein Gerät zu starten: das Mapper-Dictionary ist statisch und lässt sich isoliert auf Korrektheit prüfen.

Was ist ein .NET MAUI Handler?

Ein Handler in .NET MAUI ist eine partial class, die die Übersetzung zwischen einem VirtualView (z. B. Entry, Button, CollectionView) und der nativen Plattform-View regelt. Die Klasse ist bewusst dünn: sie hält die beiden Referenzen, registriert einen PropertyMapper mit Schlüssel-Wert-Paaren der Art „Wenn sich Text ändert, rufe UpdateText() auf", und kapselt einen CommandMapper für Befehle wie Focus oder Unfocus.

In der Praxis bedeutet das: wir vererben nicht mehr, wir konfigurieren. Der Handler-Lifecycle wird vom MAUI-Framework gesteuert, nicht von unserer eigenen Subclass. Ein Beispiel: das Setzen von Entry.Placeholder löst keinen Override aus, sondern einen Lookup im Mapper, der bei .NET MAUI 10 als FrozenDictionary intern gehalten wird. Das spart bei jedem Property-Update einen Allokations-Roundtrip – ein Detail, das auf Listen mit 200 Items schnell den Unterschied zwischen 60 fps und sichtbarem Jank macht.

Wer aus der Xamarin-Welt kommt, kennt das Gefühl: jede Custom-Logik landete früher in einem Renderer mit Lifecycle-Spaghetti. Mit Handlern arbeiten wir deklarativ – und können in den meisten Fällen Performance-Optimierungen aus unserem MAUI-Guide direkt anwenden, weil die Handler-Erzeugung selbst zur Hot Path zählt.

Handler vs. Custom Renderer: Was hat sich geändert?

Die Frage höre ich in jedem Migrations-Kickoff. Die kurze Antwort: Handler sind nicht „bessere Renderer" – sie sind ein anderes Architekturmodell. Renderer beruhten auf Vererbung (EntryRenderer : ViewRenderer<Entry, UITextField>) und enthielten Lifecycle-Hooks wie OnElementChanged. Handler trennen die Datenebene (Mapper) von der Lifecycle-Ebene (ConnectHandler, DisconnectHandler) und vererben nicht von einem Renderer-Stack mit vier Layern.

AspektXamarin Custom Renderer.NET MAUI Handler
ArchitekturmusterVererbung (Renderer-Subclass)Komposition (Mapper-Dictionary)
Lifecycle-HooksOnElementChanged, OnElementPropertyChangedConnectHandler, DisconnectHandler
Plattform-Referenzthis.ControlPlatformView
MAUI-Control-Referenzthis.ElementVirtualView
PerformanceHoch (mehrere Renderer-Layer)Niedrige Allokation, statischer Mapper
ErweiterbarkeitSubclassing erforderlichMapper global oder pro Control modifizierbar
TestbarkeitGerät/Emulator notwendigMapper-Dictionary unit-testbar
Status in .NET MAUI 10Legacy (via Compatibility-Paket)Primäres Modell

Was wir in Migrationsprojekten besonders schätzen: die Möglichkeit, das Verhalten eines bestehenden Controls global zu modifizieren, ohne ein neues Control zu erfinden. Bei Xamarin mussten wir einen CustomEntry ableiten, einen CustomEntryRenderer registrieren – und das in jedem Projekt erneut. Bei MAUI reichen drei Zeilen in MauiProgram.cs, mehr dazu unten.

Wie funktioniert die Handler-Architektur intern?

Wer Handler nur konsumiert, kann diesen Abschnitt überspringen. Wer aber Custom Controls baut oder Bugs jagt, sollte die Innereien kennen. Jeder Handler erbt von ViewHandler<TVirtualView, TPlatformView> aus dem Namespace Microsoft.Maui.Handlers. Diese Basisklasse hält zwei Felder – nicht mehr, nicht weniger – und delegiert alles andere an statische Mapper-Dictionaries.

Der zentrale Mechanismus ist der PropertyMapper<TVirtualView, TViewHandler>. Er ist eine Map von String-Keys (Property-Namen) auf Action<THandler, TView>-Delegates. Wenn MAUI ein Property-Change-Event auf dem VirtualView empfängt, schlägt das Framework den passenden Delegate nach und ruft ihn auf. Das ist deutlich schneller als reflective Property Change Notifications.

Daneben gibt es den CommandMapper für imperative Befehle wie „fokussiere mich" oder „setze die Caret-Position". Beide Mapper sind statisch – sie existieren genau einmal pro Handler-Typ in der App. Das hat zwei wichtige Konsequenzen: erstens, das Modifizieren eines Mappers betrifft alle Instanzen des Controls; zweitens, die Mapper sind thread-safe nur, solange wir sie zur Startup-Zeit konfigurieren und danach nicht mehr anfassen.

Der Handler selbst durchläuft folgenden Lifecycle: Konstruktor → SetVirtualViewConnectHandler (hier abonnieren wir native Events) → laufender Betrieb → DisconnectHandler (hier deregistrieren wir alles, was wir abonniert haben) → SetVirtualView(null). Wenn wir DisconnectHandler vergessen, leakt der Handler – im Zweifel über die gesamte App-Session.

Eigenen Custom Handler in .NET MAUI 10 erstellen

Bauen wir einen echten Custom Handler für ein BorderlessEntry-Control: ein Entry ohne die hässliche Underline auf Android und ohne den Default-Border auf iOS. Das ist eines der häufigsten Wünsche in unseren Apps, und es eignet sich perfekt, um die Mechanik zu zeigen.

Schritt 1: Plattformunabhängiges Control definieren

// Controls/BorderlessEntry.cs
namespace MyApp.Controls;

public class BorderlessEntry : Entry
{
    // Keine zusätzliche API nötig – wir wollen nur das Aussehen ändern.
}

Schritt 2: Partial Handler-Klasse anlegen

// Handlers/BorderlessEntryHandler.cs
using Microsoft.Maui.Handlers;

namespace MyApp.Handlers;

public partial class BorderlessEntryHandler : EntryHandler
{
    public static IPropertyMapper<BorderlessEntry, BorderlessEntryHandler> PropertyMapper =
        new PropertyMapper<BorderlessEntry, BorderlessEntryHandler>(EntryHandler.Mapper)
        {
            // Wir registrieren hier eigene Properties oder
            // überschreiben das Verhalten bestehender Eigenschaften.
        };

    public BorderlessEntryHandler() : base(PropertyMapper) { }
}

Schritt 3: Plattform-Implementierungen schreiben

Wir erstellen je eine partial Implementation pro Plattform, gesteuert durch Multi-Targeting in der .csproj:

// Platforms/Android/Handlers/BorderlessEntryHandler.Android.cs
using Android.Graphics.Drawables;

namespace MyApp.Handlers;

public partial class BorderlessEntryHandler
{
    protected override void ConnectHandler(AppCompatEditText platformView)
    {
        base.ConnectHandler(platformView);
        platformView.Background = new ColorDrawable(Android.Graphics.Color.Transparent);
    }
}
// Platforms/iOS/Handlers/BorderlessEntryHandler.iOS.cs
using UIKit;

namespace MyApp.Handlers;

public partial class BorderlessEntryHandler
{
    protected override void ConnectHandler(MauiTextField platformView)
    {
        base.ConnectHandler(platformView);
        platformView.BorderStyle = UITextBorderStyle.None;
    }
}

Schritt 4: Handler in MauiProgram.cs registrieren

// MauiProgram.cs
public static MauiApp CreateMauiApp()
{
    var builder = MauiApp.CreateBuilder()
        .UseMauiApp<App>()
        .ConfigureMauiHandlers(handlers =>
        {
            handlers.AddHandler<BorderlessEntry, BorderlessEntryHandler>();
        });

    return builder.Build();
}

Das war's. Kein XAML-Markup, keine Bindable Properties, keine drei verschachtelten Layouts. Wir verwenden den Control jetzt überall wie ein normales Entry:

<controls:BorderlessEntry Placeholder="Suchen..." Text="{Binding Query}" />

Bestehende Handler erweitern statt neu schreiben

In 90 % der Fälle brauchen wir gar keinen eigenen Handler – wir wollen nur eine Eigenschaft des Standard-Controls anders behandeln. MAUI macht das trivial, weil die Mapper statisch und öffentlich sind. Beispiel: alle Entry-Instanzen sollen auf Android ihre Underline verlieren, ohne dass wir eine neue Klasse einführen:

// MauiProgram.cs
EntryHandler.Mapper.AppendToMapping("NoUnderline", (handler, view) =>
{
#if ANDROID
    handler.PlatformView.BackgroundTintList =
        Android.Content.Res.ColorStateList.ValueOf(Android.Graphics.Color.Transparent);
#elif IOS
    handler.PlatformView.BorderStyle = UIKit.UITextBorderStyle.None;
#endif
});

Dieser eine Aufruf in CreateMauiApp() patcht jeden Entry in der App – inklusive der, die in Bibliotheken Dritter steckt. Genau das war mit Xamarin Custom Renderern strukturell unmöglich. Für komplexere Szenarien gibt es ModifyMapping (ersetzt einen vorhandenen Mapper-Eintrag) und PrependToMapping (führt Code vor dem Standard-Update aus).

In MVVM-Architekturen passt das gut zu unseren bestehenden Mustern; wer MVVM mit CommunityToolkit konsequent einsetzt, kann Handler-Anpassungen wie reine UI-Concerns behandeln und die Logik im ViewModel halten.

Custom Handler im Team einführen

Wir haben Handler in drei produktiven Apps eingeführt – einmal bei einer Xamarin-Migration, zweimal in Greenfield-Projekten. Drei Lessons Learned, die ich jedem Team mitgebe:

  1. Genau ein Ort für Mapper-Konfiguration. Wenn jeder Entwickler in einem beliebigen ViewModel EntryHandler.Mapper.AppendToMapping aufrufen darf, habt ihr nach drei Sprints einen Mapper-Dschungel. Bei uns gibt es eine HandlerCustomization.cs-Klasse, die alle Anpassungen kapselt und in MauiProgram.cs einmal aufgerufen wird.
  2. Pull Requests für Mapper-Änderungen kennzeichnen. Mapper sind statisch und global – ein Bug betrifft die ganze App, oft nur auf einer Plattform. Wir verlangen für jede Mapper-PR Screenshots aus iOS- und Android-Simulator. Das hat uns mehrfach vor Hotfixes bewahrt.
  3. Disconnect ist kein Optional. In Code-Reviews achten wir explizit darauf, dass jeder ConnectHandler einen passenden DisconnectHandler hat. Native Event-Subscriptions, die nicht entkoppelt werden, sind die Nr.-1-Quelle für Speicherlecks in MAUI-Apps.

Typische Fehler und wie wir sie vermeiden

Aus über einem Jahr Code-Review-Notizen, hier die fünf Fehler, die wir immer wieder sehen:

  • Mapper zur Laufzeit verändern. Tut das nicht. Mapper sind nicht thread-safe nach App-Start. Wenn ihr „dynamisches" Verhalten braucht, prüft die Bedingung im Mapper-Delegate, nicht durch Hinzufügen/Entfernen von Keys.
  • Plattform-Felder im VirtualView referenzieren. Ich habe schon Handler gesehen, die PlatformView in eine BindableProperty der Control-Klasse stopfen. Das führt zwangsläufig zu Crashes, sobald das Control re-parented wird – der Handler wechselt, das Bindable Object bleibt.
  • Async-Code in Mappern. Mapper-Delegates sind synchrone Update-Funktionen. Async-Code dort verschluckt Exceptions still und führt zu non-deterministischen UI-States. Wenn ihr Async-Initialisierung braucht, startet sie in ConnectHandler und schreibt das Ergebnis später zurück.
  • Falsches Multi-Targeting in der .csproj. Vergesst die <ItemGroup Condition="$(TargetFramework.Contains('-android'))">-Filter nicht, sonst wandert iOS-Code in den Android-Build und ihr habt Compiler-Errors, die nichts mit eurem Code zu tun haben.
  • Tests gegen den falschen Mapper. Unit-Tests sollten gegen das PropertyMapper-Dictionary laufen, nicht gegen den Handler selbst. Letzteres erfordert einen MauiContext und macht Tests langsam und brüchig – ein Thema, das wir in unserem Leitfaden zu Teststrategien in .NET MAUI ausführlicher beleuchten.

Wer sich tiefer einlesen will: das offizielle .NET MAUI GitHub-Repository enthält den Quellcode aller Standard-Handler und ist die einzige zuverlässige Quelle, wenn das Verhalten unklar ist. Für Breaking Changes lohnt sich ein Blick in die Release Notes zu .NET 10 – dort sind alle Handler-API-Änderungen explizit dokumentiert.

Häufig gestellte Fragen

Werden Xamarin Custom Renderer in .NET MAUI 10 noch unterstützt?

Ja – über das Paket Microsoft.Maui.Controls.Compatibility, das auch in .NET MAUI 10 noch geliefert wird. Es ist aber als Legacy gekennzeichnet und für neue Features nicht empfohlen. Für laufende Migrationen ein nützlicher Zwischenschritt, langfristig sollten alle Renderer auf Handler portiert werden.

Wann sollte ich einen eigenen Handler statt Mapper-Modifikationen schreiben?

Wenn ihr eine neue, eigene Plattform-View braucht (etwa einen Wrapper um eine native Bibliothek). Geht es nur darum, eine vorhandene Eigenschaft anders zu interpretieren – z. B. Standard-Entry ohne Underline – reicht AppendToMapping in MauiProgram.cs.

Kann ich Handler unit-testen, ohne ein Gerät zu starten?

Ja. Das PropertyMapper-Dictionary ist eine reine .NET-Datenstruktur und lässt sich in einem normalen xUnit-Test asserten. Was nicht ohne Plattform geht, ist die Interaktion mit dem nativen PlatformView – dafür brauchen wir Device-Tests mit Appium oder dem MAUI Device-Test-Framework.

Was passiert, wenn ich DisconnectHandler vergesse?

Der Handler bleibt im Speicher, weil native Event-Subscriptions ihn am Leben halten. Bei häufig erzeugten Controls (Listen, Modals) führt das innerhalb von Minuten zu wachsendem Speicherverbrauch und schließlich zu Out-of-Memory-Crashes auf Android. Immer entkoppeln, was in ConnectHandler abonniert wurde.

Funktionieren Handler auch mit .NET MAUI Blazor Hybrid?

Ja. Handler arbeiten auf der MAUI-Control-Ebene und sind unabhängig davon, ob das umgebende Layout aus XAML, C# oder einem BlazorWebView stammt. Custom Handler für den BlazorWebView selbst sind ebenfalls möglich, etwa um JavaScript-Bridge-Verhalten plattformspezifisch anzupassen.

Priya Sharma
Über den Autor Priya Sharma

Cross-platform engineering lead who's shipped apps to millions on both Play Store and App Store. Believes shared codebases shouldn't mean shared mediocrity.