Vlastní Handlery v .NET MAUI 9: Kompletní průvodce pro iOS a Android (2026)

Kompletní průvodce vlastními handlery v .NET MAUI 9. Naučte se, kdy stačí AppendToMapping a kdy potřebujete napsat vlastní handler pro iOS a Android.

Vlastní Handlery v .NET MAUI 9 (2026)

Aktualizováno: 19. srpna 2026

Vlastní handler v .NET MAUI 9 je tenká architektonická vrstva, která propojuje cross-platform ovládací prvek (např. Entry) s konkrétní nativní implementací (UITextField na iOS, AppCompatEditText na Androidu) a umožňuje vám přepsat nebo doplnit její chování bez dědění a bez psaní vlastního rendereru. Na rozdíl od Xamarin.Forms Custom Rendererů se handlery skládají z Mapperů, tedy slovníků, které mapují změnu C# vlastnosti na volání nativního API. V tomto průvodci ukážu, jak handlery skutečně fungují pod kapotou, kdy si vystačíte s úpravou existujícího mapperu a kdy potřebujete napsat vlastní handler od základu.

  • Handlery v .NET MAUI 9 nahrazují Xamarin.Forms Custom Renderers a jsou o řád rychlejší díky přímému mapování bez reflexe.
  • V 80 % případů nový handler nepotřebujete. Stačí zavolat Mapper.AppendToMapping() nebo ModifyMapping() v MauiProgram.cs.
  • Handlery mají dvě metody životního cyklu: ConnectHandler pro registraci event handlerů a DisconnectHandler pro cleanup nativních zdrojů.
  • PropertyMapper reaguje na změny bindovatelných vlastností, CommandMapper na explicitní příkazy jako Focus().
  • Platformové úpravy patří do složek Platforms/iOS/ a Platforms/Android/ nebo do souborů s #if IOS / #if ANDROID.
  • Vždy odregistrujte native event handlery v DisconnectHandler, jinak vzniknou memory leaks, které iOS profiler odhalí až v produkci.

Architektura handleru pod kapotou

Když v MAUI napíšete <Entry Placeholder="Jméno" />, cross-platform třída Microsoft.Maui.Controls.Entry sama o sobě nic nevykresluje. Za scénou existuje rozhraní IEntry (v Microsoft.Maui) a k němu EntryHandler, který drží referenci na platformový view: UITextField na iOS, AppCompatEditText na Androidu, TextBox na WinUI. Handler samotný je jen instalatér. Neobsahuje logiku vykreslování, jen definuje statický slovník PropertyMapper, který říká „když se změní Placeholder, zavolej metodu MapPlaceholder“.

Honestly, rozdíl oproti Xamarin.Forms rendererům je zásadní. Renderer dědil z ViewRenderer<TControl, TNativeView>, přepisoval OnElementPropertyChanged a hledal změněnou vlastnost přes reflexi či switch na string názvech. V praxi to znamenalo desítky mikrosekund navíc při každém PropertyChanged a hluboký dědičný strom, který kvůli povinné znalosti base třídy komplikoval unit testy. Handlery jsou naopak plochá struktura, kde mapper je obyčejný Dictionary<string, Action<IElementHandler, IElement>>, který volá metody přímo, bez reflexe. Podle oficiální dokumentace Microsoftu k handlerům to přineslo měřitelné zrychlení layoutu o 30–50 % na středně složitých stránkách.

Jak upravit existující handler bez vlastní třídy

Většina vývojářů, kteří přecházejí z Xamarin.Forms, sáhne po vlastním handleru zbytečně brzy. Pokud potřebujete jen změnit vzhled nebo přidat chování k existující kontrolce (třeba schovat spodní čáru pod Entry na Androidu nebo vypnout copy/paste na iOS), použijte AppendToMapping nebo ModifyMapping přímo v MauiProgram.cs. Tento přístup je preferovaný, protože zachovává původní implementaci Microsoftu a jen ji rozšíří.

// MauiProgram.cs, globální úprava všech Entry v aplikaci
public static MauiApp CreateMauiApp()
{
    var builder = MauiApp.CreateBuilder();
    builder
        .UseMauiApp<App>()
        .ConfigureMauiHandlers(handlers =>
        {
            Microsoft.Maui.Handlers.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
                });
        });
    return builder.Build();
}

Klíčem k pochopení je, že AppendToMapping spustí vaši lambda za původním mapováním. Pokud chcete původní chování úplně nahradit (třeba přepsat, jak se renderuje Placeholder), použijte ModifyMapping. První parametr (v příkladu výše "NoUnderline") je jen unikátní klíč, který si vymyslíte; slouží k tomu, aby stejné mapování bylo možné později odregistrovat.

Vlastní handler od základu: krok za krokem

Vlastní handler potřebujete ve třech scénářích: (1) chcete zabalit nativní kontrolku, která v MAUI vůbec neexistuje (např. MKMapView na iOS, když Microsoft.Maui.Controls.Maps nestačí), (2) potřebujete cross-platform kontrolku s vlastním rozhraním, které dědí z View, nebo (3) chcete úplně nahradit stávající platformovou implementaci jinou (třeba Entry postavený nad AutoCompleteTextView). Ukážu vám první scénář, jednoduchý RatingView s pěti hvězdičkami.

V minulém projektu, kde jsme portovali interní ratingový widget z Xamarin.Forms, jsem si přesně tuhle strukturu napsal několikrát. Vyplatilo se to. Cross-platform kontrolka a její interface vypadají takto:

// Controls/RatingView.cs, cross-platform část
public class RatingView : View, IRatingView
{
    public static readonly BindableProperty ValueProperty =
        BindableProperty.Create(nameof(Value), typeof(int), typeof(RatingView), 0,
            propertyChanged: (b, o, n) => ((RatingView)b).OnValueChanged());

    public int Value
    {
        get => (int)GetValue(ValueProperty);
        set => SetValue(ValueProperty, value);
    }

    public event EventHandler<int>? ValueSelected;

    void OnValueChanged() => ValueSelected?.Invoke(this, Value);
}

public interface IRatingView : IView
{
    int Value { get; set; }
}

Sdílená část handleru (soubor Handlers/RatingViewHandler.cs) je jen deklarace mapperu a partial třída. Implementace se doplní v platformových souborech:

// Handlers/RatingViewHandler.cs, shared partial
public partial class RatingViewHandler
{
    public static IPropertyMapper<IRatingView, RatingViewHandler> PropertyMapper =
        new PropertyMapper<IRatingView, RatingViewHandler>(ViewHandler.ViewMapper)
        {
            [nameof(IRatingView.Value)] = MapValue
        };

    public RatingViewHandler() : base(PropertyMapper) { }

    public static void MapValue(RatingViewHandler handler, IRatingView view)
        => handler.UpdateStars(view.Value);
}

Handler zaregistrujete stejně jako v předchozí sekci. V ConfigureMauiHandlers zavoláte handlers.AddHandler<RatingView, RatingViewHandler>(). Toto je architektonicky totéž, co dřív dělala anotace [assembly: ExportRenderer] v Xamarin.Forms, jen bez assembly scanningu při startu, což je jeden z důvodů, proč MAUI aplikace startují zhruba o 20 % rychleji. Pokud vás optimalizace startupu zajímá do hloubky, doporučuji můj předchozí článek optimalizace výkonu v .NET MAUI.

Platformová implementace pro iOS a Android

Platformové části handleru žijí ve složkách Platforms/iOS/ a Platforms/Android/. Kompilátor je zahrne do buildu jen pro odpovídající target framework, což znamená, že v iOS souboru můžete volně používat UIKit bez preprocesorových direktiv. Pojďme napsat obě strany našeho RatingViewHandler.

iOS: UIStackView s pěti UIImageView

// Platforms/iOS/RatingViewHandler.iOS.cs
using UIKit;

public partial class RatingViewHandler : ViewHandler<IRatingView, UIStackView>
{
    UIImageView[] _stars = Array.Empty<UIImageView>();

    protected override UIStackView CreatePlatformView()
    {
        var stack = new UIStackView
        {
            Axis = UILayoutConstraintAxis.Horizontal,
            Spacing = 4,
            Distribution = UIStackViewDistribution.EqualSpacing
        };

        _stars = Enumerable.Range(0, 5).Select(i =>
        {
            var iv = new UIImageView(UIImage.GetSystemImage("star"))
            {
                UserInteractionEnabled = true,
                TintColor = UIColor.SystemYellow
            };
            var tap = new UITapGestureRecognizer(() => VirtualView.Value = i + 1);
            iv.AddGestureRecognizer(tap);
            stack.AddArrangedSubview(iv);
            return iv;
        }).ToArray();

        return stack;
    }

    internal void UpdateStars(int value)
    {
        for (int i = 0; i < _stars.Length; i++)
        {
            _stars[i].Image = UIImage.GetSystemImage(i < value ? "star.fill" : "star");
        }
    }
}

Všimněte si dvou detailů, které Apple dokumentace k UIImageView zmiňuje jen okrajově. UserInteractionEnabled je u UIImageView ve výchozím stavu false (na rozdíl od většiny ostatních views), takže bez něj gesture recognizer nedostane žádný tap. A druhý detail: SF Symbols přes UIImage.GetSystemImage automaticky respektují TintColor, takže se v dark módu obarví správně bez další práce.

Android: LinearLayout s pěti ImageView

// Platforms/Android/RatingViewHandler.Android.cs
using Android.Views;
using Android.Widget;
using AndroidX.AppCompat.Widget;

public partial class RatingViewHandler : ViewHandler<IRatingView, LinearLayout>
{
    AppCompatImageView[] _stars = Array.Empty<AppCompatImageView>();

    protected override LinearLayout CreatePlatformView()
    {
        var context = Context ?? throw new InvalidOperationException("Context is null");
        var layout = new LinearLayout(context) { Orientation = Orientation.Horizontal };

        _stars = Enumerable.Range(0, 5).Select(i =>
        {
            var iv = new AppCompatImageView(context);
            iv.SetImageResource(Resource.Drawable.star_outline);
            iv.Click += (_, _) => VirtualView.Value = i + 1;
            var lp = new LinearLayout.LayoutParams(
                ViewGroup.LayoutParams.WrapContent,
                ViewGroup.LayoutParams.WrapContent) { MarginEnd = 12 };
            layout.AddView(iv, lp);
            return iv;
        }).ToArray();

        return layout;
    }

    internal void UpdateStars(int value)
    {
        for (int i = 0; i < _stars.Length; i++)
        {
            _stars[i].SetImageResource(i < value
                ? Resource.Drawable.star_filled
                : Resource.Drawable.star_outline);
        }
    }
}

Životní cyklus: ConnectHandler a DisconnectHandler

Handler má dvě zásadní metody životního cyklu. ConnectHandler se volá po vytvoření platformového view a připojení k virtuálnímu view, DisconnectHandler pak před uvolněním. To je jediné správné místo pro registraci a odregistraci native event handlerů. Ve výše uvedeném příkladu jsem to zjednodušil na inline lambdu v CreatePlatformView, ale v produkci to udělejte správně, jinak vzniknou memory leaks.

protected override void ConnectHandler(UIStackView platformView)
{
    base.ConnectHandler(platformView);
    foreach (var star in _stars)
    {
        star.AddGestureRecognizer(_tapGesture);
    }
}

protected override void DisconnectHandler(UIStackView platformView)
{
    foreach (var star in _stars)
    {
        star.RemoveGestureRecognizer(_tapGesture);
    }
    _tapGesture.Dispose();
    base.DisconnectHandler(platformView);
}

Co je zásadní vědět: MAUI ve výchozím stavu DisconnectHandler nezavolá automaticky. Musíte to explicitně povolit voláním Microsoft.Maui.Handlers.ViewHandler.ViewMapper.ModifyMapping(...) nebo lépe nastavit App.Current.Windows[0].Handler.DisconnectHandler() při navigaci pryč. Důvod je zpětná kompatibilita s aplikacemi migrovanými z Xamarin.Forms, které na to nespoléhají. I hit this exact bug when shipping a media-heavy tab bar, kde nezavřený AVPlayer držel snímek z předchozího videa v paměti přes několik navigací. Podrobný postup a memory-leak checklist jsem sepsal v článku o migraci z Xamarin.Forms na .NET MAUI.

Jaký je rozdíl mezi Handlerem, Rendererem a Effectem?

Tato otázka se objevuje na Stack Overflow prakticky týdně a odpověď určuje, kolik kódu napíšete zbytečně. Krátká verze: Renderery a Effecty jsou Xamarin.Forms koncepty. V .NET MAUI existuje jen Handler. Effecty jsou podle oficiální GitHub diskuze týmu MAUI obsoletní od .NET 8 a jejich API je označeno [Obsolete] v .NET 9. Renderery jsou pořád podporované přes kompatibilní balíček Microsoft.Maui.Controls.Compatibility, ale pouze pro migraci; u nového kódu nemají místo.

VlastnostHandler (MAUI)Custom Renderer (Xamarin.Forms)Effect (Xamarin.Forms)
RegistraceConfigureMauiHandlers[assembly: ExportRenderer][assembly: ResolutionGroupName]
VýkonVysoký (žádná reflexe)Střední (reflexe, dědičnost)Nejnižší (dvojité vytváření)
RozsahCelá kontrolkaCelá kontrolkaPřidané chování bez náhrady
Cross-platform codeAno (partial class)Ne (assembly per platform)Ano
MAUI 9 statusDoporučený přístupCompat balíček, jen pro migraciObsoletní
Kdy použítVždy pro nový kódNikdy pro nový kódNikdy (nahradí AppendToMapping)

Praktické pravidlo: pokud upravujete jednu vlastnost jedné kontrolky, použijte AppendToMapping. Pokud potřebujete držet stav mezi několika vlastnostmi (např. counter, který resetuje po změně jiné vlastnosti), napište vlastní handler. Effecty už nevytvářejte, ani když je najdete v tutoriálech z roku 2023.

Debugování a testování handlerů

Handlery se dají překvapivě dobře testovat, protože mapper je čistý slovník. Pro unit testy stačí zavolat statickou MapValue metodu s mockem IRatingView a ověřit, že se zavolá správná platformová operace. Samozřejmě potřebujete abstrakci nad platformovým view, což je důvod, proč doporučuji vytáhnout skutečnou práci s nativními objekty do samostatné třídy typu StarRenderer. Podrobný postup s xUnit a Appium najdete v mém předchozím článku o testování .NET MAUI aplikací.

Pro runtime debugging platí tři pravidla, která si Microsoft dokumentace jen neochotně přiznává: (1) breakpoint v MapValue se občas neaktivuje kvůli AOT kompilaci, použijte místo toho Debug.WriteLine nebo strukturované logování přes ILogger, (2) na iOS simulátoru je PlatformView dostupné až po ConnectHandler, dřívější přístup vrátí null, (3) Hot Reload při změně handleru vždycky vyžaduje full rebuild, tzv. „MetadataUpdater“ totiž nedokáže patchovat statické slovníky mapperu.

Nejčastější chyby a jak jim předejít

Za dva roky, co učím týmy přecházet z Xamarin.Forms na MAUI, jsem zjistil, že vývojáři dělají zhruba pět opakujících se chyb. První je volat VirtualView v konstruktoru handleru. V tu chvíli ještě není nastavené, protože k propojení dojde až v SetVirtualView. Druhá je zapomínat na null-checky u PlatformView, protože handler může být „prázdný“ v okamžiku, kdy byl view uvolněn, ale ještě nedosloužil garbage collectoru.

Třetí chyba se týká vlákna. Handler metody se volají na UI vlákně, ale pokud v MapValue spustíte async operaci (např. stažení obrázku), musíte se před přístupem k PlatformView vrátit na UI vlákno přes MainThread.BeginInvokeOnMainThread. iOS UIKit tuhle chybu odhalí okamžitě crashem s „UIView must be used on main thread“, Android View toleruje víc, ale výsledek je nedeterministický. Čtvrtá chyba: nesprávné pořadí registrace. ConfigureMauiHandlers musí být za UseMauiApp, jinak MAUI zavolá výchozí handler dřív než ten váš. A pátá: nemazání resources v DisconnectHandler, zvlášť pokud handler drží CoreImage filtry, Android Bitmapy nebo Firebase listenery.

Často kladené otázky

Co je Handler v .NET MAUI a proč nahradil Custom Renderer?

Handler je architektonická vrstva, která propojuje cross-platform ovládací prvek s nativní implementací přes slovník tzv. Mapperů. Nahradil Custom Renderer z Xamarin.Forms proto, že Renderer používal reflexi, dědičnost a byl pomalejší. Handler je plochá struktura bez reflexe, což zrychlilo layout o 30–50 %.

Jak vytvořit vlastní Handler v .NET MAUI 9?

Vytvořte cross-platform View třídu, definujte interface odvozený z IView, napište partial třídu MyHandler se statickým PropertyMapper, a doplňte platformové implementace ve složkách Platforms/iOS/ a Platforms/Android/, kde přepíšete CreatePlatformView, ConnectHandler a DisconnectHandler. Nakonec ho zaregistrujete přes ConfigureMauiHandlers(h => h.AddHandler<MyView, MyHandler>()).

Kdy použít AppendToMapping místo vlastního handleru?

Vždy, když jen upravujete jednu nebo dvě vlastnosti existující kontrolky a nepotřebujete držet vlastní stav. AppendToMapping je jednodušší, testovatelnější a nezavře cestu k budoucím upgradům .NET MAUI, protože nechává původní implementaci Microsoftu nedotčenou.

Proč se DisconnectHandler nezavolá automaticky?

Kvůli zpětné kompatibilitě. Xamarin.Forms na to nespoléhal a MAUI ho z výchozího stavu vypnul, aby se migrované aplikace nechovaly nečekaně. V produkci ho musíte volat ručně při navigaci pryč ze stránky, nebo přes HandlerProperties.SetDisconnectPolicy, jinak vznikají memory leaks kolem event handlerů.

Fungují Xamarin.Forms Custom Renderers v .NET MAUI 9?

Ano, přes kompatibilní balíček Microsoft.Maui.Controls.Compatibility a atribut [assembly: ExportRenderer]. Toto řešení je ale určeno jen pro dočasnou migraci. Microsoft plánuje kompat vrstvu udržovat minimálně do .NET 10 (listopad 2026), ale nový kód by měl vždy používat Handlery.

David O'Reilly
O Autorovi David O'Reilly

Native iOS/Android specialist turned MAUI advocate. Writes about the gritty platform details most cross-platform tutorials skip.