Handler .NET MAUI 10: Personalizzazione dei Controlli Nativi (Guida 2026)

Gli Handler di .NET MAUI 10 sono il ponte tra i controlli cross-platform e le view native. Impara a personalizzarli con PropertyMapper, CommandMapper ed esempi iOS/Android.

Handler .NET MAUI 10: Guida Completa 2026

Aggiornato: 12 luglio 2026

Gli Handler in .NET MAUI 10 sono le classi che collegano ogni controllo cross-platform (come Entry, Button o CollectionView) al corrispondente controllo nativo (rispettivamente UITextField/EditText, UIButton/AppCompatButton, UICollectionView/RecyclerView). Sostituiscono i vecchi Custom Renderer di Xamarin.Forms e permettono di personalizzare i controlli nativi senza sottoclassare, grazie a un sistema di dizionari chiamati Mapper. In questa guida vedremo cosa succede davvero sotto il cofano quando XAML incontra la piattaforma, con esempi che ho estratto da un porting Xamarin che ho seguito l'anno scorso.

  • Ogni controllo .NET MAUI ha un Handler che crea la PlatformView nativa e la mantiene sincronizzata con la VirtualView cross-platform tramite un Mapper.
  • Il PropertyMapper è un Dictionary<string, Action> che si può estendere con AppendToMapping, PrependToMapping o ModifyMapping senza sottoclassare il controllo.
  • Gli Handler sono globali per tipo: modificare il Mapper di Entry influenza ogni Entry dell'app, quindi usa BindableProperty personalizzate se ti serve una personalizzazione mirata.
  • I Custom Renderer di Xamarin.Forms possono essere migrati agli Handler tramite AddCompatibilityRenderer, ma la riscrittura in un Handler nativo è quasi sempre più performante.
  • Gli eventi HandlerChanging e HandlerChanged sono il posto giusto per iscriversi e disiscriversi da eventi nativi, evitando memory leak sui delegate iOS.

Cosa sono gli Handler in .NET MAUI?

Nella documentazione ufficiale, Microsoft definisce l'Handler come «il componente responsabile dell'istanziazione della view nativa sottostante e del mapping tra l'API del controllo cross-platform e l'API della view nativa». Tradotto in pratica: quando scrivi <Entry Text="Ciao" /> nel tuo XAML, il framework istanzia un Microsoft.Maui.Controls.Entry (la VirtualView), che chiede al proprio Handler di creare la controparte nativa. Su iOS quella controparte è un UITextField; su Android è un AppCompatEditText. L'Handler mantiene un riferimento a entrambe le view e agisce da traduttore bidirezionale.

La differenza fondamentale rispetto all'approccio dei vecchi Renderer è che l'Handler non è più il controllo: è un oggetto separato, leggero, che si occupa solo di far comunicare due mondi. Il controllo cross-platform (la VirtualView) rimane puro C# gestito, mentre la view nativa (la PlatformView) vive nel suo runtime di piattaforma. Questa separazione è ciò che ha permesso, in .NET 8 e .NET 9, di ridurre drasticamente il footprint di memoria e i tempi di startup, e in .NET 10 di introdurre il trimming aggressivo senza rompere le personalizzazioni utente.

Un Handler tipico eredita da ViewHandler<TVirtualView, TPlatformView> e vive nel namespace Microsoft.Maui.Handlers. Esistono classi base specializzate come ContentViewHandler, LayoutHandler ed ElementHandler per casi che non richiedono una view visibile.

Handler vs Custom Renderer: perché Microsoft ha cambiato architettura

Chi arriva da Xamarin.Forms ricorda i Custom Renderer come classi che ereditavano da EntryRenderer, ButtonRenderer o ViewRenderer<TView, TNativeView>, e che venivano scoperte a runtime tramite ExportRenderer e Assembly Scanning. Funzionava, ma aveva quattro problemi seri: (1) l'Assembly Scanning obbligava il runtime a riflettere su ogni assembly caricato, rallentando lo startup; (2) i Renderer ereditavano dal controllo nativo e ne subclassavano il comportamento, con tutte le fragilità del caso; (3) subscribersi a eventi nativi richiedeva pattern ripetitivi di OnElementChanged/OnElementPropertyChanged; (4) il coupling con il controllo cross-platform era così stretto che aggiornare Xamarin.Forms rompeva spesso i renderer personalizzati.

Gli Handler risolvono tutti questi problemi con un design a dictionary di azioni. Come spiega la documentazione ufficiale sugli Handler, invece di sottoclassare un renderer per modificare una proprietà, si modifica il Mapper. Il Mapper è un dizionario che associa il nome di una proprietà cross-platform a un'azione da eseguire sulla view nativa. Aggiungere una personalizzazione significa aggiungere una entry al dizionario, non ereditare da nulla.

Il risultato è misurabile: nei benchmark interni di Microsoft, la stessa app Xamarin.Forms convertita a .NET MAUI 10 mostra tempi di rendering della prima schermata inferiori del 30–45% su Android, principalmente grazie all'assenza dell'Assembly Scanning. Per un'analisi approfondita di questi guadagni, consulta la nostra guida all'ottimizzazione delle performance di .NET MAUI.

PropertyMapper e CommandMapper: il cuore del sistema

Ogni Handler espone due dizionari statici pubblici: il PropertyMapper e il CommandMapper. Il PropertyMapper contiene voci del tipo [nameof(Entry.Text)] = MapText, dove MapText è un metodo statico con firma (IEntryHandler handler, IEntry entry) => { ... }. Quando la proprietà Text cambia sulla VirtualView, il framework invoca l'azione corrispondente, che a sua volta aggiorna la PlatformView.

Il CommandMapper serve per istruzioni una tantum che portano dati opzionali (ad esempio «metti il focus» o «invalida il layout»). Non è un ICommand nel senso MVVM del termine, è semplicemente un dizionario di azioni con un payload opzionale. Ecco un esempio minimale di come guardare il PropertyMapper di EntryHandler nel codice sorgente:

public static IPropertyMapper<IEntry, IEntryHandler> Mapper = new PropertyMapper<IEntry, IEntryHandler>(ViewHandler.ViewMapper)
{
    [nameof(IEntry.Text)]                = MapText,
    [nameof(IEntry.TextColor)]           = MapTextColor,
    [nameof(IEntry.IsPassword)]          = MapIsPassword,
    [nameof(IEntry.Placeholder)]         = MapPlaceholder,
    [nameof(IEntry.PlaceholderColor)]    = MapPlaceholderColor,
    [nameof(IEntry.HorizontalTextAlignment)] = MapHorizontalTextAlignment,
    [nameof(IEntry.ReturnType)]          = MapReturnType,
    [nameof(IEntry.Keyboard)]            = MapKeyboard,
};

Nota che il Mapper eredita da ViewHandler.ViewMapper: le proprietà comuni a tutte le view (BackgroundColor, Opacity, IsVisible...) sono gestite dal Mapper base, e ogni Handler specifico aggiunge solo ciò che gli è proprio. È un design elegante che ricorda la composizione classica dei componenti UIKit.

PlatformView e VirtualView: accedere ai controlli nativi

Dentro qualsiasi Handler, la proprietà PlatformView ti dà il riferimento diretto al controllo nativo. Il tipo di PlatformView dipende dalla piattaforma: su iOS è UITextField per un Entry, su Android è AppCompatEditText, su Mac Catalyst è ancora UITextField perché condivide il runtime UIKit. La proprietà VirtualView punta invece all'oggetto IEntry cross-platform.

Ecco un esempio concreto che ho scritto letteralmente decine di volte: vogliamo rimuovere il bordo di default degli Entry su iOS (l'iconico rettangolo grigio) e trasformarli in campi «flat» come Material Design. Nel MauiProgram.cs:

using Microsoft.Maui.Handlers;
#if IOS
using UIKit;
#endif

public static MauiApp CreateMauiApp()
{
    var builder = MauiApp.CreateBuilder();

#if IOS
    Microsoft.Maui.Handlers.EntryHandler.Mapper.AppendToMapping("NoBorderiOS", (handler, view) =>
    {
        // handler.PlatformView è UITextField su iOS
        handler.PlatformView.BorderStyle = UITextBorderStyle.None;
        handler.PlatformView.BackgroundColor = UIColor.Clear;
    });
#endif

    builder.UseMauiApp<App>();
    return builder.Build();
}

Il compilatore #if IOS è fondamentale perché UITextField esiste solo nel target iOS. Su Android faresti la stessa cosa con handler.PlatformView.SetBackground(null) dentro un blocco #if ANDROID. Se preferisci mantenere il codice organizzato per piattaforma, sposta questi mapping dentro file Platforms/iOS/EntryHandlerCustomizations.cs e chiamali dal MauiProgram.cs. Onestamente, in progetti oltre i 50 controlli custom, questa organizzazione fa la differenza tra un file MauiProgram.cs leggibile e uno che ti fa venire l'ansia solo ad aprirlo.

Come personalizzare un Handler esistente

Ci sono tre metodi per intervenire su un Mapper esistente, ed è importante scegliere quello giusto. La guida ufficiale alla customizzazione degli Handler li documenta tutti e tre.

PrependToMapping

Modifica il Mapper prima che le azioni di default di .NET MAUI vengano applicate. Utile quando devi impostare uno stato nativo che poi verrà sovrascritto dalle proprietà cross-platform. Raramente necessario nella pratica (l'ho usato forse due volte in tre anni).

ModifyMapping

Sostituisce completamente il mapping esistente per una proprietà. Ricevi come parametro l'azione originale e puoi decidere di chiamarla o meno. È il modo giusto per bloccare un comportamento di default:

ButtonHandler.Mapper.ModifyMapping(nameof(Button.TextColor), (handler, view, originalAction) =>
{
    // Ignoriamo il TextColor cross-platform e usiamo sempre il colore del brand
#if IOS
    handler.PlatformView.SetTitleColor(UIColor.SystemBlue, UIControlState.Normal);
#elif ANDROID
    handler.PlatformView.SetTextColor(Android.Graphics.Color.ParseColor("#1E88E5"));
#endif
});

AppendToMapping

È l'opzione che userai il 90% delle volte. Aggiunge la tua azione dopo che .NET MAUI ha applicato le proprie. Il nome della chiave («NoBorderiOS» nell'esempio precedente) è arbitrario: serve solo se poi vuoi rimuovere quel mapping con Mapper.RemoveMapping().

Come creare un Handler personalizzato da zero

Quando il controllo nativo che ti serve non ha un equivalente cross-platform (per esempio MKMapView di Apple o il PhotoView di Chris Banes su Android), è il momento di scrivere un Handler completamente nuovo. La documentazione Microsoft sui controlli custom descrive il pattern completo, ma il flusso essenziale è questo.

Prima definisci l'interfaccia della VirtualView e il controllo cross-platform:

public interface IVideoPlayer : IView
{
    string SourceUri { get; }
    bool IsPlaying { get; }
    void Play();
    void Pause();
}

public class VideoPlayer : View, IVideoPlayer
{
    public static readonly BindableProperty SourceUriProperty =
        BindableProperty.Create(nameof(SourceUri), typeof(string), typeof(VideoPlayer));

    public string SourceUri
    {
        get => (string)GetValue(SourceUriProperty);
        set => SetValue(SourceUriProperty, value);
    }

    public bool IsPlaying { get; private set; }

    public void Play()  => Handler?.Invoke(nameof(IVideoPlayer.Play));
    public void Pause() => Handler?.Invoke(nameof(IVideoPlayer.Pause));
}

Poi crei una classe partial per l'Handler, con la definizione dei Mapper condivisi:

public partial class VideoPlayerHandler
{
    public static IPropertyMapper<IVideoPlayer, VideoPlayerHandler> PropertyMapper =
        new PropertyMapper<IVideoPlayer, VideoPlayerHandler>(ViewHandler.ViewMapper)
        {
            [nameof(IVideoPlayer.SourceUri)] = MapSourceUri,
        };

    public static CommandMapper<IVideoPlayer, VideoPlayerHandler> CommandMapper =
        new(ViewHandler.ViewCommandMapper)
        {
            [nameof(IVideoPlayer.Play)]  = MapPlay,
            [nameof(IVideoPlayer.Pause)] = MapPause,
        };

    public VideoPlayerHandler() : base(PropertyMapper, CommandMapper) { }
}

Le due implementazioni platform-specific vivono in file separati: Platforms/iOS/VideoPlayerHandler.iOS.cs e Platforms/Android/VideoPlayerHandler.Android.cs. Ognuna eredita da ViewHandler<IVideoPlayer, AVPlayerViewController> (su iOS) o ViewHandler<IVideoPlayer, ExoPlayerView> (su Android), e implementa i metodi statici MapSourceUri, MapPlay, MapPause. Infine registri l'Handler in MauiProgram.cs:

builder.ConfigureMauiHandlers(handlers =>
{
    handlers.AddHandler<VideoPlayer, VideoPlayerHandler>();
});

A questo punto il tuo <VideoPlayer SourceUri="..." /> in XAML sarà completamente funzionante e integrato con Data Binding, MVVM Toolkit e Dependency Injection. Per approfondire i pattern MVVM che si sposano meglio con gli Handler custom, la nostra guida su MVVM e Dependency Injection è il complemento naturale a questa.

Ciclo di vita degli Handler e gestione degli eventi nativi

Uno degli errori più comuni con gli Handler è iscriversi a eventi nativi senza mai disiscriversi, causando memory leak invisibili. Il problema è particolarmente insidioso su iOS, dove i delegate mantengono riferimenti forti al target. Il framework offre due eventi specifici per gestire questo ciclo di vita in modo pulito: HandlerChanging e HandlerChanged.

HandlerChanging viene sollevato subito prima che un Handler venga sostituito. Nel suo HandlerChangingEventArgs trovi due proprietà: NewHandler (l'Handler in arrivo) e OldHandler (quello uscente). Quando OldHandler non è null, è il momento perfetto per rimuovere event handler, delegate e observer nativi:

public class VideoPlayer : View, IVideoPlayer
{
    public VideoPlayer()
    {
        HandlerChanging += OnHandlerChanging;
    }

    private void OnHandlerChanging(object? sender, HandlerChangingEventArgs e)
    {
        if (e.OldHandler is VideoPlayerHandler oldHandler)
        {
#if IOS
            // Ferma la riproduzione e libera l'AVPlayer
            oldHandler.PlatformView.Player?.Pause();
            oldHandler.PlatformView.Player = null;
#endif
        }
    }
}

Se hai bisogno di sapere quando la nuova view nativa è pronta (per esempio per iniziare a caricare un video), sovrascrivi OnHandlerChanged nella classe VirtualView. Non fidarti mai del costruttore per accedere all'Handler: nel momento in cui il costruttore gira, l'Handler non è ancora stato istanziato. Questa è una trappola in cui ho visto cadere anche sviluppatori con dieci anni di esperienza su Xamarin.

Migrazione dai Custom Renderer di Xamarin.Forms

Se stai portando un'app Xamarin.Forms su .NET MAUI 10, hai due strade per i tuoi Custom Renderer esistenti. La prima è temporanea e si chiama compatibility bridge: aggiungi Microsoft.Maui.Controls.Compatibility e registri i vecchi renderer con AddCompatibilityRenderer. Funziona, ma paghi un overhead di runtime e perdi tutti i benefici dell'architettura Handler.

builder.ConfigureMauiHandlers(handlers =>
{
    handlers.AddCompatibilityRenderer(typeof(MyCustomEntry), typeof(MyCustomEntryRenderer));
});

La seconda strada, quella che raccomando in ogni progetto che rimarrà in manutenzione più di sei mesi, è la riscrittura completa come Handler. Il pattern di conversione è quasi meccanico: le proprietà che gestivi in OnElementPropertyChanged diventano voci del PropertyMapper; le personalizzazioni in OnElementChanged quando e.NewElement era non-null vanno in ConnectHandler; quelle con e.OldElement non-null vanno in DisconnectHandler. Il tempo di conversione di un Renderer medio è di 30–60 minuti, ma il risultato è codice più leggero e testabile.

Un dettaglio spesso trascurato: in Xamarin.Forms i Renderer si registravano con ExportRenderer, un attributo che veniva scoperto tramite Assembly Scanning. In .NET MAUI la registrazione è esplicita tramite ConfigureMauiHandlers. È più codice da scrivere, ma il compilatore ora sa esattamente quali tipi servono, e il linker può fare tree-shaking aggressivo. Se hai già affrontato la migrazione della navigazione, la nostra guida su Shell Navigation, routing e deep linking copre il complemento naturale per la parte di UX.

Errori comuni e best practice di produzione

Ho visto le stesse trappole ripetersi in molti progetti che sono passati da Xamarin ad Handler MAUI. Ecco quelle che vale la pena conoscere prima di finirci dentro.

1. Modificare il Mapper dopo che l'app è avviata

Le modifiche al Mapper vanno fatte prima di builder.Build(). Se le applichi in un'altra parte del ciclo di vita, i controlli già renderizzati non vedranno le modifiche fino a quando non verranno ricreati. In uno dei miei primi porting ho perso un pomeriggio intero prima di capire perché lo styling non si applicava a runtime.

2. Confondere ConnectHandler e OnHandlerChanged

ConnectHandler(TPlatformView platformView) è un metodo protetto dell'Handler, chiamato una volta creata la view nativa. OnHandlerChanged è un metodo della VirtualView. Il primo è dove iscrivi eventi nativi; il secondo è dove notifichi il resto della tua app che l'Handler è pronto.

3. Ignorare i target Windows e Mac Catalyst

Anche se il tuo focus è iOS/Android, tenere Windows e Mac Catalyst nei tuoi target rende esplicite le assunzioni piattaforma-specifiche. Aggiungere #if WINDOWS nei tuoi Handler dall'inizio evita di ritrovarsi con un porting doloroso più avanti.

4. Bloccare i test

Gli Handler sono difficili da testare in isolamento perché dipendono dal runtime nativo. Il pattern che funziona è isolare la logica di business in una classe iniettata nel controllo cross-platform, testarla con xUnit, e trattare l'Handler come un semplice adapter di rendering. Se hai già impostato una pipeline di test, il resto viene naturale.

5. Dimenticare le release notes di .NET 10

Le release notes ufficiali del repository dotnet/maui documentano ogni cambio di API negli Handler minor per minor. Prima di aggiornare in produzione, controllare quella pagina risparmia ore di debugging.

Domande frequenti

Qual è la differenza principale tra Handler e Custom Renderer?

Un Custom Renderer di Xamarin.Forms ereditava dal renderer nativo e sottoclassava il comportamento. Un Handler di .NET MAUI è un oggetto separato, leggero, che collega VirtualView e PlatformView tramite un Mapper. Il Mapper è un dizionario che si estende senza sottoclassare, il che rende le personalizzazioni più semplici da mantenere e da testare.

Come si registra un Handler personalizzato in .NET MAUI 10?

Nel metodo CreateMauiApp del MauiProgram.cs si usa builder.ConfigureMauiHandlers(h => h.AddHandler<MyControl, MyControlHandler>()). A differenza di Xamarin.Forms, la registrazione è esplicita: niente Assembly Scanning, niente attributi di riflessione.

Posso personalizzare un Handler solo per alcuni controlli invece che globalmente?

Sì. Il pattern consigliato è aggiungere una BindableProperty personalizzata (per esempio EnableCustomBorder) al controllo cross-platform, e leggere quel valore dentro l'azione del Mapper. In alternativa, crea un controllo derivato (ad esempio FlatEntry : Entry) e registra un Handler dedicato solo per il tipo derivato.

Gli Handler funzionano anche su Windows e Mac Catalyst?

Sì, l'architettura Handler è cross-platform per design. Ogni Handler ha implementazioni platform-partial per iOS, Android, Windows (WinUI 3) e Mac Catalyst. Devi solo aggiungere il target framework corrispondente al tuo .csproj e implementare i metodi statici del Mapper per quella piattaforma.

Come si evitano memory leak quando si personalizza un Handler?

Iscriversi agli eventi nativi in ConnectHandler e disiscriversi in DisconnectHandler. Su iOS, evitare di assegnare la VirtualView come delegate diretto di controlli UIKit; usare invece una classe delegate separata che manteniamo con un riferimento debole. Il pattern HandlerChanging con controllo di OldHandler è il modo ufficiale per cleanup deterministico.

David O'Reilly
Sull'Autore David O'Reilly

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