A .NET MAUI Handler egy vékony, kétirányú leképező réteg, amely a cross-platform vezérlő tulajdonságait a natív iOS UIView-hoz vagy Android View-hoz köti egy PropertyMapper szótáron keresztül, anélkül hogy a Xamarin.Forms renderer-eknél megszokott extra wrapper hierarchiát létrehozná. Ez az útmutató végigmegy a Handler architektúra minden rétegén (a PlatformView/VirtualView viszonytól kezdve az AppendToMapping mintákon át a saját, több platformra célzó custom control írásáig), és megmutatja, mi történik valójában a felszín alatt, amikor egy Button Android-on MaterialButton-ná, iOS-en pedig UIButton-ná válik.
A Handler minden .NET MAUI vezérlőnek van (interfészen keresztül, pl. IButton, IEntry), és a PlatformView property adja vissza a valódi natív kontrollt.
A PropertyMapper egy Dictionary<string, Action>: minden cross-platform property-hez tartozik egy delegate, ami frissíti a natív viewt. Ez váltja fel az OnElementPropertyChanged switch-case-eket.
A CommandMapper ugyanez, csak imperatív parancsokra (pl. Focus, Unfocus), amiket a MAUI a natív viewnak küld.
Beépített vezérlőt globálisan az AppendToMapping, PrependToMapping vagy ModifyMapping hívásokkal szabhatsz testre. Figyelem: minden ilyen példányra hat.
Saját custom control esetén partial osztályokkal a Platforms/iOS és Platforms/Android mappákba tesszük a natív implementációt, és MauiProgram.CreateMauiApp-ban ConfigureMauiHandlers-szel regisztráljuk.
A Handler-ek globálisak: nincs per-page izoláció, ezért a mapper módosítás mindig „app-wide" hatású, hacsak feltételes logikát nem építesz be.
Mi az a Handler architektúra a .NET MAUI-ban?
Röviden: a Handler egy .NET MAUI-ban minden UI vezérlőhöz tartozó, platformonként partial-ként létező osztály, amelynek egyetlen dolga van. A cross-platform „virtual view" (pl. a Microsoft.Maui.Controls.Button mögötti IButton interfész) tulajdonságait és parancsait átfordítani a natív platformon élő valódi vezérlőre. iOS-en ez egy UIButton, Android-on egy Google.Android.Material.Button.MaterialButton, Windows-on pedig egy Microsoft.UI.Xaml.Controls.Button.
Fontos, hogy a MAUI szándékosan a Material Design komponensre bind-ol Android-on (nem a plain android.widget.Button-ra), mert ezek a Google által ajánlott, ripple-t és elevációt támogató kontrollok. Ha valaha megnézed az Android UI Inspectorral, tényleg a Material réteg jelenik meg, nem az AppCompat. Én is meglepődtem ezen az első MAUI projektnél, mert Xamarin.Forms-ban még az AppCompat volt az alap.
A Handler minden osztályhoz elérhetővé tesz két kulcsfontosságú property-t: PlatformView és VirtualView. A PlatformView a natív típusra van kasztolva (iOS-en UIButton, Android-on MaterialButton), és bármely natív API-t közvetlenül hívhatod rajta, például iOS-en handler.PlatformView.SetImage(image, UIControlState.Normal). A VirtualView pedig a MAUI absztrakt vezérlőt adja vissza (a IButton interfészt), amin keresztül a cross-platform property értékeket olvasod.
Ez az egyszerű kétirányú kapcsolat az egész architektúra alapja. Nincs örökléses wrapper, nincs harmadik réteg. Ha a MAUI hivatalos Handlers overview dokumentációját megnyitod, ugyanezt az „inverz" viszonyt fogod látni: a platform már nem a keretrendszert szolgálja ki, hanem fordítva.
Handler vs Renderer: mi változott a Xamarin.Forms óta?
Ha Xamarin.Forms-ból jönnél (mint a legtöbb MAUI fejlesztő ebben az évben), a legszembetűnőbb különbség, hogy nincs többé ElementChanged vagy OnElementPropertyChanged. A Renderer-modell egy örökléses hierarchián alapult: minden Renderer a natív view-ból származott (pl. iOS-en ButtonRenderer : UIButton), ami azt jelentette, hogy a MAUI keretrendszer minden vezérlőhöz létrehozott egy wrappelt subclass-t, és minden property változásra egy nagy switch-ben reagáltunk. Ez lassú volt, felfújta az UI treet, és a memóriában a natív hierarchián két „duplikált" node jelent meg.
A Handler modell ezt teljesen kidobja. A natív view már nem örököl semmiből, csak példányosítjuk (CreatePlatformView()), majd a PropertyMapper szótáron át kötjük össze a MAUI property-kkel. Az alábbi táblázat összefoglalja a legfontosabb különbségeket, amiket a Xamarin.Forms-ról MAUI-ra migrálókkal a legtöbbet átbeszéltem.
Szempont
Xamarin.Forms Renderer
.NET MAUI Handler
Alap minta
Öröklés (Renderer : NativeView)
Kompozíció (Handler → PlatformView property)
Property változás
OnElementPropertyChanged switch
PropertyMapper Dictionary<string, Action>
Regisztráció
[assembly: ExportRenderer]
ConfigureMauiHandlers().AddHandler<T,H>
Cross-platform interfész
Nincs (közvetlenül a View)
IView-származék (pl. IButton, IEntry)
UI overhead
Wrapper subclass minden vezérlőre
Nincs wrapper, natív típus közvetlenül
Testreszabás
Új Renderer + assembly attribútum
AppendToMapping / PrependToMapping
Tesztelhetőség
Nehéz (natív dep. minden szinten)
Könnyű (mapper delegate mockolható)
A gyakorlatban ez pár tíz százalékos memória-csökkenést és mérhető layout gyorsulást jelent, és nem véletlen, hogy a MAUI teljesítménye érezhetően jobb komplex listáknál. Erről bővebben írtam a .NET MAUI teljesítményoptimalizálás cikkben, ahol pontosabb számokat is találsz a Native AOT-vel párosított handler layerről.
A PropertyMapper és a CommandMapper működése
A PropertyMapper típusa a MAUI forráskódban lényegében ez: Dictionary<string, Action<IElementHandler, IElement>>. A kulcs a cross-platform property neve (jellemzően nameof(IButton.Text) formában), a value pedig egy delegate, amit a keretrendszer akkor hív, amikor a MAUI vezérlőn megváltozik a bekötött érték. Ez a design szándékosan minimalista: nincs reflection, nincs runtime string parsing, csak egy dictionary lookup és egy delegate hívás.
Nézzük egy konkrét MAUI példát, ahogy egy egyszerű Handler-nél kinéz a mapper (a MAUI forrás mintájára):
public partial class CustomEntryHandler : ViewHandler<ICustomEntry, UIView>
{
public static IPropertyMapper<ICustomEntry, CustomEntryHandler> Mapper =
new PropertyMapper<ICustomEntry, CustomEntryHandler>(ViewHandler.ViewMapper)
{
[nameof(ICustomEntry.Text)] = MapText,
[nameof(ICustomEntry.PlaceholderColor)] = MapPlaceholderColor,
[nameof(ICustomEntry.IsPassword)] = MapIsPassword,
};
public static ICommandMapper<ICustomEntry, CustomEntryHandler> CommandMapper =
new CommandMapper<ICustomEntry, CustomEntryHandler>(ViewHandler.ViewCommandMapper)
{
[nameof(ICustomEntry.Focus)] = MapFocus,
[nameof(ICustomEntry.Unfocus)] = MapUnfocus,
};
public CustomEntryHandler() : base(Mapper, CommandMapper) { }
}
A MapText egy statikus metódus, ami a handlert és a virtuális viewt kapja: static void MapText(CustomEntryHandler h, ICustomEntry v) => h.PlatformView.SetText(v.Text). A statikus signature nem véletlen: a MAUI csapata szándékosan úgy tervezte, hogy a mapping függvények mentesek legyenek példány-állapottól, mert így könnyebb őket felülírni, cserélni és tesztelni.
A CommandMapper ehhez képest imperatív parancsokat közvetít. Amikor a UI thread myEntry.Focus()-t hív, a MAUI ezt nem property változásként kezeli, hanem a CommandMapper-be belookup-olja a Focus kulcsot, és meghívja a hozzá tartozó delegatet. Ez a szétválasztás azért lényeges, mert a Focus/Unfocus/gesztus jellegű dolgok nem property értékek, hanem egyszeri „utasítások", és a Dictionary így természetesen szeparálja őket a state-től.
Hogyan szabjuk testre a beépített vezérlők natív viselkedését?
A leggyakoribb Handler-igény nem az, hogy nulláról vezérlőt írj, hanem hogy egy MAUI Entry vagy Button valamilyen platform-specifikus viselkedést kapjon. Például az iOS UITextFieldReturnKeyType-ja legyen UIReturnKeyType.Done, vagy az Android Entry alján tűnjön el az az idegesítő aláhúzás. Erre való a három mapper-módosító metódus: AppendToMapping, PrependToMapping és ModifyMapping. Mind a három a MAUI beépített mapperjét bővíti, a különbség csak az, hogy a te delegate-ed a meglévő MAUI mapping előtt vagy után fut, illetve teljesen felváltja azt.
Az iOS Entry aláhúzás-eltávolítása például így néz ki (globálisan, minden Entry-re):
#if IOS
using Microsoft.Maui.Handlers;
using UIKit;
Microsoft.Maui.Handlers.EntryHandler.Mapper.AppendToMapping(
"NoBorder",
(handler, view) =>
{
handler.PlatformView.BorderStyle = UITextBorderStyle.None;
handler.PlatformView.BackgroundColor = UIColor.Clear;
});
#endif
#if ANDROID
using Microsoft.Maui.Handlers;
using Android.Graphics.Drawables;
Microsoft.Maui.Handlers.EntryHandler.Mapper.AppendToMapping(
"NoUnderline",
(handler, view) =>
{
handler.PlatformView.Background = new ColorDrawable(
Android.Graphics.Color.Transparent);
});
#endif
Ezt tipikusan a MauiProgram.CreateMauiApp metódusban futtatjuk, a builder.Build() előtt. Ez fontos: a mapper globális állapot, ezért ha később hozzányúlsz, minden korábban létrehozott Handler példány is felveszi az új viselkedést a következő layout ciklusnál, nem csak az utána példányosítottak.
Mikor válassz AppendToMapping és PrependToMapping között? Egyszerű ökölszabály: ha a MAUI alap viselkedésére rá akarsz építeni (pl. a MAUI beállítja a szöveg színét, te utána még finomítasz), akkor AppendToMapping. Ha viszont az alap MAUI mapping véletlenül felülírja a te értékedet (mert később fut), akkor PrependToMapping-gal fusson a tiéd, majd hagyd, hogy a MAUI utána tegye a dolgát. A ModifyMapping-ot ritkán használjuk, mert az teljesen leváltja a MAUI beépített delegate-jét, és 90%-ban az alap logika elvesztésével jár.
Saját cross-platform vezérlő létrehozása lépésről lépésre
Nézzük meg, hogyan írjunk egy egyszerű custom controlt, mondjuk egy „ShimmerLabel"-t, ami egy shimmer (fényváltó) animációt mutat, amíg tartalmat töltünk. Ezt a MAUI-ban 4 lépésben csináljuk meg. A hivatalos Microsoft Create custom controls with .NET MAUI handlers útmutató is ezt a mintát követi, csak itt egy realisztikusabb példával megyünk végig.
1. Interfész és cross-platform osztály
// Controls/IShimmerLabel.cs
public interface IShimmerLabel : IView
{
string Text { get; set; }
bool IsShimmering { get; set; }
Color HighlightColor { get; set; }
}
// Controls/ShimmerLabel.cs
public class ShimmerLabel : View, IShimmerLabel
{
public static readonly BindableProperty TextProperty =
BindableProperty.Create(nameof(Text), typeof(string),
typeof(ShimmerLabel), string.Empty);
public static readonly BindableProperty IsShimmeringProperty =
BindableProperty.Create(nameof(IsShimmering), typeof(bool),
typeof(ShimmerLabel), false);
public static readonly BindableProperty HighlightColorProperty =
BindableProperty.Create(nameof(HighlightColor), typeof(Color),
typeof(ShimmerLabel), Colors.LightGray);
public string Text { get => (string)GetValue(TextProperty); set => SetValue(TextProperty, value); }
public bool IsShimmering { get => (bool)GetValue(IsShimmeringProperty); set => SetValue(IsShimmeringProperty, value); }
public Color HighlightColor { get => (Color)GetValue(HighlightColorProperty); set => SetValue(HighlightColorProperty, value); }
}
2. Partial Handler osztály (shared)
// Handlers/ShimmerLabelHandler.cs (shared, minden platformon fordul)
using Microsoft.Maui.Handlers;
public partial class ShimmerLabelHandler
{
public static IPropertyMapper<IShimmerLabel, ShimmerLabelHandler> PropertyMapper =
new PropertyMapper<IShimmerLabel, ShimmerLabelHandler>(ViewMapper)
{
[nameof(IShimmerLabel.Text)] = MapText,
[nameof(IShimmerLabel.IsShimmering)] = MapIsShimmering,
[nameof(IShimmerLabel.HighlightColor)] = MapHighlightColor,
};
public ShimmerLabelHandler() : base(PropertyMapper) { }
}
4. Platform-specifikus partial (Android) és regisztráció
// Platforms/Android/Handlers/ShimmerLabelHandler.cs
using Android.Animation;
using Android.Widget;
using Microsoft.Maui.Handlers;
public partial class ShimmerLabelHandler : ViewHandler<IShimmerLabel, TextView>
{
ObjectAnimator? _animator;
protected override TextView CreatePlatformView() =>
new TextView(Context ?? throw new InvalidOperationException("No Context"));
static void MapText(ShimmerLabelHandler h, IShimmerLabel v) =>
h.PlatformView.Text = v.Text;
static void MapHighlightColor(ShimmerLabelHandler h, IShimmerLabel v) =>
h.PlatformView.SetTextColor(v.HighlightColor.ToPlatform());
static void MapIsShimmering(ShimmerLabelHandler h, IShimmerLabel v)
{
h._animator?.Cancel();
if (!v.IsShimmering) return;
h._animator = ObjectAnimator.OfFloat(h.PlatformView, "alpha", 1f, 0.3f);
h._animator.SetDuration(800);
h._animator.RepeatMode = ValueAnimatorRepeatMode.Reverse;
h._animator.RepeatCount = ValueAnimator.Infinite;
h._animator.Start();
}
}
// MauiProgram.cs
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureMauiHandlers(handlers =>
{
handlers.AddHandler<ShimmerLabel, ShimmerLabelHandler>();
});
return builder.Build();
}
Ez után XAML-ből ugyanolyan first-class polgárként tudod használni: <controls:ShimmerLabel Text="{Binding UserName}" IsShimmering="{Binding IsLoading}" />. Ha az MVVM oldalról jönnél, és a Handlerbe DI-jal szeretnél service-eket injektálni (pl. logger), az szintén megoldható. Erről a .NET MAUI Dependency Injection útmutatóban megtalálod a szolgáltatás-élettartamok részleteit.
Platform-specifikus kód: partial osztályok és feltételes fordítás
A partial osztályos megközelítés (amit fent láttunk) messze a legtisztább minta, mert a .csproj a Platforms/iOS, Platforms/Android, Platforms/Windows mappákat automatikusan a megfelelő target framework alá fordítja. Ez azt jelenti, hogy az iOS partial csak akkor kerül lefordításra, ha a net10.0-ios targetet buildeled, ezért lehet benne bátran using UIKit;. Nincs #if IOS, nincs guard, nincs macera.
Az alternatíva a feltételes fordítás egyetlen fájlon belül, amit főleg akkor használunk, ha app-oldalon (tehát nem library-ben) csak egy pár sor natív kódot kell injektálni:
public static void ApplyPlatformTweaks()
{
#if IOS
UIKit.UIApplication.SharedApplication.KeyWindow!
.WindowScene!.StatusBarManager!.SetStatusBarHidden(true, UIKit.UIStatusBarAnimation.Fade);
#elif ANDROID
var activity = Platform.CurrentActivity;
activity?.Window?.AddFlags(Android.Views.WindowManagerFlags.Fullscreen);
#endif
}
Egy köztes megoldás a részleges class + [Conditional("IOS")] attribútum, de a saját tapasztalatom szerint ez inkább zavart okoz, mint tisztaságot. Én az elmúlt évben egyszer belefutottam, hogy a build átment, de release módban az attribútum által kivágott hívás miatt a natív hozzáférés csendben elveszett. Maradj a partial + Platforms mappa mintánál, mert a MAUI csapata is ezt ajánlja a Porting Custom Renderers to Handlers wiki oldalon, és ez lesz a hosszú távon karbantartható struktúra.
Gyakori hibák és éles projektekben bevált gyakorlatok
Az elmúlt évben végigcsináltam pár Xamarin.Forms → MAUI migrációt, és összegyűjtöttem a leggyakoribb Handler-buktatókat. Ezek nem trükkösek, de mindegyikbe belefutottunk élesben (őszintén, volt köztük olyan, ami két napig tartott, mire megtaláltuk).
1. Elfelejtett DisconnectHandler
A MAUI 8 óta a Handler példány automatikusan nem hívja meg a DisconnectHandler-t view eldobásakor. Ez opt-in a ViewHandler.ViewMapper beállítással. Ha natív eseményekre iratkoztál fel (pl. iOS UITextField.EditingChanged), és nem iratkozol le a DisconnectHandler()-ben, memory leak-et csinálsz. A biztonságos minta:
A AppendToMapping hívást a MauiProgram.CreateMauiApp-on belül végezd, még a Build() előtt. Ha később csinálod (pl. egy App.xaml.cs konstruktorban), akkor az addig példányosodott Handler-ek már nem futtatják a mapping-et frissítéskor. Ez sok fejlesztőt megzavart, mert Xamarin.Forms-ban a Renderer regisztráció bárhonnan is működött.
3. Handler property módosítás UI szálon kívül
A PlatformView-t iOS-en és Android-on is szigorúan a UI szálról érheted el. Ha egy háttérszálon fut a mapping delegate-ed (mert pl. egy async callback-ből triggerelted), csomagold be: MainThread.BeginInvokeOnMainThread(() => handler.PlatformView.SomeMethod()). Az iOS Metal renderelés csendes segfaultot dob, ha nem a main threaden vagy, Android-on legalább kap egy CalledFromWrongThreadException-t az ember. Én pont ebbe futottam bele egy tavalyi projektnél, és jó két órámba került, amíg a stack trace-ből kiderült, hogy a hiba egy távoli Task.Run-ból eredt.
4. A Handler globális jellegének félreértése
Sokan próbálnak „csak ezen az egy oldalon" jellegű mapper módosítást csinálni, ami a Handler modelben csapdába vezet. Ha a viselkedés page-specifikus, oldd meg a Behaviors API-val vagy egy dedikált MyEntry : Entry subclass-szal. Ez a szemlélet jobban illeszkedik a MAUI kompozíciós filozófiájához, és az MVVM oldalt tisztán tartja. A CommunityToolkit.Mvvm útmutatóban részleteztem, hogy egy jól strukturált ViewModel milyen könnyen jön össze forrásgenerátorokkal.
5. Windows platform hiánya
Ha kizárólag iOS + Android-ra fejlesztesz, könnyű elfelejteni a Windows/MacCatalyst partial osztályokat. A build ilyenkor átmegy (mert a Windows target nem szerepel a .csproj-ban), de amint valaki windows-ra próbál buildelni, forráshiány miatt hasal el. Vagy add hozzá az összes platformot, vagy explicit zárd ki a <TargetFrameworks>-ben.
Gyakran ismételt kérdések
Használhatók még a Xamarin.Forms Renderer-ek .NET MAUI-ban?
Igen, van egy kompatibilitási réteg (Microsoft.Maui.Controls.Compatibility), amiben a régi Renderer-ek átmenetileg futnak, de ezt csak migráció alatt ajánlott használni. Új kódhoz mindenképpen Handler-t írj, mert a Renderer réteget a Microsoft „csak migrációs eszközként" pozicionálja, és a jövőben deprikálódhat.
Mikor használjunk PrependToMapping-et AppendToMapping helyett?
Akkor PrependToMapping, ha a MAUI beépített mapping delegate-je felülírná a te értékedet (mert később fut a láncban). Így a tiéd először beállít valamit, aztán a MAUI ráteszi a maga alapját. Az AppendToMapping a gyakoribb, akkor kell, ha a MAUI beállítja az alapokat, és utána finomítasz.
A Handler módosítás csak egy adott oldalon érvényes?
Nem. A Handler mapper globális állapot, ezért a módosítás az app egész életciklusán át, minden ilyen típusú vezérlőre hat. Ha oldal-specifikus viselkedést szeretnél, csinálj egy subclass-t (pl. MyEntry : Entry) és annak írj külön Handler-t, vagy használj Behavior-t.
Hogyan érem el a natív iOS UIView-ot vagy Android View-ot a Handlerben?
A handler.PlatformView property adja vissza. Típusa a Handler-ben megadott generikus paraméter (pl. UIButton iOS-en, MaterialButton Android-on). Innen bármely natív API-t közvetlenül hívhatsz. A cross-platform absztrakt viewhoz pedig a handler.VirtualView ad hozzáférést.
Kell-e Handler-t regisztrálni MauiProgram-ban a beépített MAUI vezérlők testreszabásához?
Nem. Ha csak beépített vezérlőt (Entry, Button, Label stb.) módosítasz AppendToMapping-gal, elég a mapper hívás a CreateMauiApp-ban. Regisztráció (AddHandler) csak akkor kell, ha teljesen új, saját cross-platform vezérlőt vezetsz be, aminek nincs MAUI beépített párja.
Gyakorlati .NET MAUI teljesítményoptimalizálás: hidegindítás 900 ms alá, Native AOT beállítás, Handler mapper trükkök, memóriaszivárgás felderítés és dotnet-trace használat. Valós kódmintákkal a .NET 9/10 verzióhoz.
A CommunityToolkit.Mvvm forrásgenerátorai 60-70%-kal rövidebbé teszik a .NET MAUI ViewModel-eket. Gyakorlati útmutató ObservableProperty, RelayCommand, Messenger és validáció témákban éles projektből származó tippekkel.