.NET MAUI Handlers: Byg Custom Controls med Platform-Specifik Kode (iOS & Android) - 2026
Handlers afløser Xamarin Renderers i .NET MAUI. Lær at bygge en custom Entry med platform-specifik kode til iOS (UIKit) og Android (AppCompat), inklusive registrering, lifecycle og memory leak-håndtering.
En Handler i .NET MAUI er en let, platform-specifik klasse, der mapper en cross-platform kontrol (som Button eller Entry) til den native view på hver platform: UIButton på iOS, AppCompatButton på Android, og så videre. I modsætning til Xamarin.Forms' tunge Renderers er Handlers stateless, hurtigere at instantiere, og bruger et mapper-baseret design, hvor du kan tilføje eller overskrive præcis den property, du har brug for, uden at arve fra hele kontrollen. Ærligt talt er det her et af de største mind-shifts, jeg selv mødte, da jeg porterede min første kommercielle Forms-app over til MAUI. I denne guide bygger jeg en custom Entry med platform-kode for både iOS og Android, så du kan se mønsteret end-to-end.
Handlers erstattede Custom Renderers i .NET MAUI og er ca. 30% hurtigere at oprette ifølge Microsofts egne benchmarks fra MAUI 8.
Mønstret er PropertyMapper (statiske properties) plus CommandMapper (imperative kald), ikke arv som i Xamarin.Forms.
Brug AppendToMapping globalt for at customisere ALLE instanser af en kontrol uden at skrive en hel handler.
Custom handlers registreres i MauiProgram.cs via ConfigureMauiHandlers. Uden registrering bliver din kode aldrig kaldt.
PlatformView giver dig den ægte native view (f.eks. UITextField). Det er her, du skriver UIKit- eller Android.Views-kode direkte.
Handler-lifecyclen følger Connect, property mapping, Disconnect. Glem ikke at unsubscribe events i Disconnect, ellers lækker du hukommelse.
Hvad er Handlers i .NET MAUI?
En handler er den bro, der oversætter en cross-platform .NET MAUI-kontrol til en native view på den aktuelle platform. Når du skriver <Button Text="Send" /> i XAML, ender det med, at ButtonHandler instantierer en UIButton på iOS, en AppCompatButton på Android, og en MauiButton (en wrapper omkring Microsoft.UI.Xaml.Controls.Button) på Windows. Handleren læser cross-platform-propertien Text via sin mapper og kalder den korrekte native API. På iOS er det SetTitle(...), og på Android er det bare Text = ....
Den interessante del, og det, der adskiller MAUI fra Xamarin.Forms, er at ButtonHandler aldrig instantieres alene. Den lever inde i en handler chain, hvor MAUI's dependency injection vælger den rette platform-implementering ud fra #if IOS, #if ANDROID osv. Det betyder, at du kan customisere én property uden at arve fra hele handleren, hvilket holder din kode minimal og let at vedligeholde. Microsofts officielle dokumentation forklarer arkitekturen i detaljer på Microsoft Learn: Handlers.
Hvis du kommer fra Xamarin.Forms og overvejer at flytte din kodebase, anbefaler jeg, at du læser min komplette guide til migrering fra Xamarin.Forms til .NET MAUI først. Handler-mønstret er en af de største mentale forskelle, du støder på undervejs.
Handlers vs. Xamarin Renderers: forskellen i praksis
Hvis du har skrevet en Custom Renderer i Xamarin.Forms, husker du sikkert kedlen. Arv fra EntryRenderer, override OnElementChanged, tjek om Control er null, husk at unsubscribe i den gamle e.OldElement, og så videre. Renderers var tunge, holdt referencer til både cross-platform- og platform-objekter, og var berygtede for memory leaks, hvis man glemte cleanup.
Handlers vender modellen på hovedet. I stedet for arv bruger du komposition via mappers: en statisk dictionary, der mapper en property-name til en delegate, som ved hvordan den skal opdatere den native view. Du tilføjer en entry i mapperen, du arver ikke fra noget. Resultatet er, at handlers er stateless, kan instantieres billigt, og at platform-koden er adskilt fra cross-platform-koden i partial classes.
Aspekt
Xamarin.Forms Renderer
.NET MAUI Handler
Designmønster
Arv (override-baseret)
Komposition (mapper-baseret)
Properties opdateres via
OnElementPropertyChanged
PropertyMapper dictionary
Cross-platform reference
Element
VirtualView
Native reference
Control
PlatformView
Lifecycle hooks
OnElementChanged
ConnectHandler / DisconnectHandler
Performance
Tungere instantiering
Ca. 30% hurtigere i MAUI 8+
Memory footprint
Højere (stateful)
Lavere (stateless mapper)
PropertyMapper og CommandMapper forklaret
Hver handler i .NET MAUI har to mappers: en PropertyMapper og en CommandMapper. PropertyMapper håndterer deklarative ændringer. Når en property som BackgroundColor ændres, kalder MAUI den tilknyttede delegate, som derefter opdaterer den native view. CommandMapper håndterer imperative kald, for eksempel når du vil sige "rul til toppen nu" eller "vis tastaturet", som ikke er et state-skift, men en handling.
For EntryHandler ser PropertyMapper sådan ud internt (forenklet fra MAUI's kildekode på GitHub):
Hver entry peger på en statisk metode med signaturen void Map<Name>(EntryHandler handler, IEntry entry). Når MAUI's binding-system opdager, at Text er ændret, slår den op i mapperen, finder MapText, og kalder den. Det er hele magien.
Det smukke ved dette design er, at du kan tilføje, overskrive, eller prepend entries på mapperen, uden at arve fra EntryHandler selv. Det er det, vi udnytter i næste sektion.
Hvordan customiserer du en kontrol uden at skrive en handler?
De fleste tutorials hopper direkte til at skrive en custom handler, men 80% af gangen behøver du det ikke. Hvis du bare vil ændre, hvordan Entry ser ud globalt (for eksempel fjerne den blå underline på Android), kan du gøre det i én linje i MauiProgram.cs med AppendToMapping:
Og det er det. Hver eneste Entry i appen har nu de native customiseringer. Forskellen mellem AppendToMapping, PrependToMapping og ModifyMapping:
AppendToMapping: kører efter den eksisterende mapping (typisk det rette valg).
PrependToMapping: kører før, hvilket sjældent giver mening, da din ændring straks overskrives.
ModifyMapping: erstatter den eksisterende mapping helt og giver dig adgang til den oprindelige delegate, hvis du vil kalde den eksplicit.
Byg en custom Entry med platform-specifik kode
Så, lad os bygge en BorderlessEntry, der fjerner alle native borders/underlines og samtidig eksponerer en ny property MaxLength, der reelt blokerer indtastning ud over længden (modsat MAUI's standard MaxLength, som opfører sig forskelligt på iOS og Android). Vi gør det "rigtigt" med en partial class i cross-platform-mappen og platform-implementeringer i Platforms/iOS og Platforms/Android.
Først cross-platform-kontrollen:
// Controls/BorderlessEntry.cs
namespace MyApp.Controls;
public class BorderlessEntry : Entry
{
public static readonly BindableProperty MaxInputLengthProperty =
BindableProperty.Create(
nameof(MaxInputLength),
typeof(int),
typeof(BorderlessEntry),
defaultValue: int.MaxValue);
public int MaxInputLength
{
get => (int)GetValue(MaxInputLengthProperty);
set => SetValue(MaxInputLengthProperty, value);
}
}
Dernæst handlerens partial class i cross-platform-mappen. Den definerer mapperen, men ingen platform-kode:
// Handlers/BorderlessEntryHandler.cs
using Microsoft.Maui.Handlers;
using MyApp.Controls;
namespace MyApp.Handlers;
public partial class BorderlessEntryHandler : EntryHandler
{
public static readonly IPropertyMapper<BorderlessEntry, BorderlessEntryHandler> CustomMapper =
new PropertyMapper<BorderlessEntry, BorderlessEntryHandler>(EntryHandler.Mapper)
{
[nameof(BorderlessEntry.MaxInputLength)] = MapMaxInputLength,
};
public BorderlessEntryHandler() : base(CustomMapper) { }
}
Bemærk to ting. Vi arver fra EntryHandler for at genbruge alle eksisterende mappings (Text, Placeholder, osv.), og vi peger på en ny mapper-instans, der har EntryHandler.Mapper som parent. Det er sådan, du udvider uden at miste basis-adfærd.
iOS: skriv direkte mod UIKit
Nu kommer den del, cross-platform-tutorials altid skipper: den ægte platform-kode. På iOS er PlatformView en MauiTextField, der arver fra UITextField. Vi rammer Apples officielle UIKit-API direkte. Apples dokumentation for UITextField er din ven her.
// Handlers/BorderlessEntryHandler.iOS.cs
using Microsoft.Maui.Handlers;
using MyApp.Controls;
using UIKit;
namespace MyApp.Handlers;
public partial class BorderlessEntryHandler
{
public static void MapMaxInputLength(BorderlessEntryHandler handler, BorderlessEntry entry)
{
// Subscribe på native event for at blokere indtastning
var textField = handler.PlatformView;
textField.ShouldChangeCharacters = (field, range, replacement) =>
{
var currentLength = field.Text?.Length ?? 0;
var newLength = currentLength - (int)range.Length + replacement.Length;
return newLength <= entry.MaxInputLength;
};
}
protected override void ConnectHandler(MauiTextField platformView)
{
base.ConnectHandler(platformView);
// Fjern alle native borders
platformView.BorderStyle = UITextBorderStyle.None;
platformView.Layer.BorderWidth = 0;
platformView.BackgroundColor = UIColor.Clear;
}
protected override void DisconnectHandler(MauiTextField platformView)
{
// VIGTIGT: nul-stil delegate for at undgå memory leaks
platformView.ShouldChangeCharacters = null;
base.DisconnectHandler(platformView);
}
}
Bemærk DisconnectHandler. Den kaldes ikke automatisk af MAUI for hver handler. Du skal aktivere den manuelt i MauiProgram.cs (vi kommer til det). Glemmer du det, lækker du hukommelse, fordi delegaten holder en reference til entry'en. Jeg ramte præcis den her bug i en app, jeg shippede sidste år, og den voksede stille og roligt 4 MB pr. push-navigation, før jeg fandt årsagen.
Android: skriv direkte mod AppCompat
På Android er PlatformView en AppCompatEditText. Vi bruger Android's standard InputFilter til at håndhæve længden. Det er en mere "Android-native" tilgang end at lytte til text-changed events.
// Handlers/BorderlessEntryHandler.Android.cs
using Android.Content.Res;
using Android.Graphics;
using Android.Text;
using AndroidX.AppCompat.Widget;
using Microsoft.Maui.Handlers;
using MyApp.Controls;
namespace MyApp.Handlers;
public partial class BorderlessEntryHandler
{
public static void MapMaxInputLength(BorderlessEntryHandler handler, BorderlessEntry entry)
{
var editText = handler.PlatformView;
editText.SetFilters(new IInputFilter[]
{
new InputFilterLengthFilter(entry.MaxInputLength)
});
}
protected override void ConnectHandler(AppCompatEditText platformView)
{
base.ConnectHandler(platformView);
// Fjern den blå underline
platformView.BackgroundTintList = ColorStateList.ValueOf(Color.Transparent);
platformView.SetBackgroundColor(Color.Transparent);
}
protected override void DisconnectHandler(AppCompatEditText platformView)
{
platformView.SetFilters(new IInputFilter[0]);
base.DisconnectHandler(platformView);
}
}
Lægger du mærke til, hvor lidt cross-platform "kompromis" der er i koden? Det er hele pointen med handlers. Du skriver ægte UIKit på iOS og ægte Android-views på Android, men trigget af én XAML-kontrol. Dette mønster fungerer også glimrende sammen med en korrekt MVVM-arkitektur, hvor view modellen ikke ved noget om handlers overhovedet.
Registrér din handler i MauiProgram.cs
Uden registrering bliver din custom handler aldrig kaldt. MAUI bruger standard EntryHandler for alle Entry-objekter, også dine subclassede. Registreringen er to-trins: aktivér disconnect-mappingen globalt, og fortæl MAUI, hvilken handler der hører til hvilken kontrol-type.
// MauiProgram.cs
using Microsoft.Maui.Handlers;
using MyApp.Controls;
using MyApp.Handlers;
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
// VIGTIGT: aktivér Disconnect for ALLE handlers globalt
Microsoft.Maui.Handlers.ViewHandler.ViewMapper
.AppendToMapping("Disconnect", (handler, view) => { });
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureMauiHandlers(handlers =>
{
handlers.AddHandler<BorderlessEntry, BorderlessEntryHandler>();
});
return builder.Build();
}
}
Handler lifecycle og memory leaks
En handler følger denne lifecycle:
Constructor: instantierer mapperen (gør ALDRIG noget tungt her).
SetVirtualView: modtager cross-platform-kontrollen og kalder CreatePlatformView.
Den klassiske leak ser sådan ud. Du registrerer en event handler i ConnectHandler, men glemmer at fjerne den i DisconnectHandler. Resultatet er, at native view'et holder en reference til din .NET-handler, som holder en reference til VirtualView, som holder din ViewModel. Hele page-træet kan ikke garbage-collectes. Dette bliver pinefuldt synligt i performance-profiler efter et par hundrede navigations-cyklusser. Hvis du vil dykke dybere i performance-emnet, har jeg en hel guide om ydeevneoptimering i .NET MAUI, der dækker hukommelsesprofilering med dotMemory og Xcode Instruments.
Hvornår skal du bruge Handlers vs. Effects?
Spørgsmålet kommer op i hvert eneste community-call. Mit pragmatiske svar baseret på shipping flere MAUI-apps:
Brug AppendToMapping til globale, simple ændringer (farver, borders, fonts). Det er én linje kode og påvirker alle instanser.
Brug en Behavior til opt-in adfærd, der ikke kræver platform-API'er (validation, auto-formatting af input).
Brug en custom Handler, når du har brug for en ny property, der ikke findes i base-kontrollen, eller når du har brug for at kalde platform-specifikke API'er som UIKit/Android.Views direkte.
Effects findes stadig som compatibility-shim, men er deprecated for nye apps. Microsoft anbefaler eksplicit at bruge handlers fremover.
I praksis ender jeg med ca. 80% AppendToMapping, 15% custom handlers og 5% Behaviors. Effects bruger jeg aldrig længere i ny kode.
Almindelige faldgruber og fejlfinding
De problemer, jeg ser folk falde i:
"Min handler kaldes aldrig": du har glemt at registrere den i ConfigureMauiHandlers. Verificér ved at sætte et breakpoint i konstruktøren.
"PlatformView er null": du tilgår den i constructor i stedet for ConnectHandler. PlatformView eksisterer først efter Connect.
"Mappingen kaldes to gange ved første render": det er korrekt adfærd. MAUI kalder først for default-værdien, derefter når XAML-binding sætter den ægte værdi.
"Hukommelsen vokser ved navigation": du har ikke aktiveret den globale Disconnect-shim, eller du unsubscriber ikke events i DisconnectHandler.
"#if IOS findes ikke": sørg for, at din .csproj har <TargetFrameworks>net9.0-ios;net9.0-android</TargetFrameworks>, og at filen ligger uden for Platforms/, hvis du vil have alle #if compileret samtidigt.
Microsofts MAUI GitHub repository er guld værd, når du sidder fast. Koden for de indbyggede handlers er den bedste reference, der findes, og issues-trackeren har ofte løsninger til obskure platform-bugs, der ikke er dokumenteret andre steder.
Ofte stillede spørgsmål
Hvad er forskellen mellem en Handler og en Renderer i .NET MAUI?
En Renderer er Xamarin.Forms-mønstret, der bruger arv og er stateful. En Handler er MAUI's nye mønster, der bruger en mapper-baseret komposition, er stateless og ca. 30% hurtigere at instantiere. Renderers understøttes stadig via en compatibility-shim, men er ikke det anbefalede mønster for nye apps.
Kan jeg bruge custom renderers fra Xamarin.Forms i .NET MAUI?
Ja, midlertidigt. Microsoft.Maui.Controls.Compatibility-namespacet leverer renderer-understøttelse for migrerede projekter. Det er dog deprecated, og Microsoft har annonceret, at det fjernes i en kommende major release. Migrér aktivt til handlers.
Hvor skal jeg placere min handler-kode i projektstrukturen?
Den cross-platform partial class og mapperen kan ligge i en almindelig Handlers/-mappe i projektroden. Platform-specifik kode (iOS/Android-partials) skal ligge i Platforms/iOS/ og Platforms/Android/, så multi-targeting-compileren kun bygger dem for den rette platform.
Hvorfor kaldes DisconnectHandler ikke automatisk?
Microsoft valgte at gøre Disconnect opt-in for at bevare bagudkompatibilitet med tidlige MAUI-previews, hvor mange apps forventede, at deres event-subscriptions overlevede mellem navigation. Konsekvensen er hyppige memory leaks. Aktivér det globalt med ViewHandler.ViewMapper.AppendToMapping("Disconnect", ...) i MauiProgram.cs.
Kan en custom handler arve fra en eksisterende handler?
Ja, og det anbefales. Arv fra fx EntryHandler for at genbruge alle dens eksisterende mappings (Text, Placeholder, Keyboard osv.). Definér en ny PropertyMapper med base-mapperen som parent og tilføj kun dine egne nye mappings ovenpå.
Sådan opsætter du type-safe REST API-integration i .NET MAUI med IHttpClientFactory, Refit og Polly. Fra MauiProgram til authenticated calls, med produktionsklare kodeeksempler jeg selv har shippet.
Lær at bygge en hurtig, sikker og vedligeholdbar lokal database i .NET MAUI med enten sqlite-net-pcl eller Entity Framework Core 9. Inkluderer setup, repository-mønster, migrationer, SQLCipher-kryptering og performance-tips med fungerende C#-eksempler.