Handlers Personnalisés dans .NET MAUI 10 : Guide Complet pour Contrôles Natifs (2026)

Guide pratique pour créer, enregistrer et migrer des handlers personnalisés dans .NET MAUI 10, avec code d'exemple pour iOS et Android et pièges à éviter.

Handlers .NET MAUI 10 : Guide Complet (2026)

Mis à jour : 15 septembre 2026

Un handler personnalisé dans .NET MAUI 10 est une classe qui fait le pont entre un contrôle multiplateforme (VirtualView) et son équivalent natif sur chaque plateforme (PlatformView : UIView sur iOS, Android.Views.View sur Android, FrameworkElement sur Windows). Contrairement aux Custom Renderers de Xamarin.Forms, les handlers utilisent une architecture découplée basée sur un PropertyMapper et un CommandMapper, plus performante et testable. Ce guide couvre la création, l'enregistrement et la migration complète en 2026, avec les pièges que j'ai rencontrés en migrant plusieurs apps de production.

  • Les handlers remplacent les Custom Renderers de Xamarin.Forms avec un modèle mapper-based dépourvu de réflexion, réduisant d'environ 40 % le temps de rendu initial.
  • Un handler suit le contrat IViewHandler et hérite typiquement de ViewHandler<TVirtualView, TPlatformView>, avec une classe partielle par plateforme (#if IOS, #if ANDROID).
  • Le PropertyMapper associe les propriétés .NET aux méthodes natives ; on peut le modifier globalement avec AppendToMapping sans créer un handler complet.
  • Les handlers doivent être enregistrés dans MauiProgram.cs via ConfigureMauiHandlers() pour que MAUI les reconnaisse.
  • La migration depuis un Renderer Xamarin passe par la conversion de OnElementChanged vers ConnectHandler/DisconnectHandler, et de OnElementPropertyChanged vers des entrées de mapper.
  • .NET MAUI 10 introduit des handlers découpés (slim handlers) qui permettent d'omettre le PlatformView pour les contrôles purement virtuels comme les décorateurs.

Qu'est-ce qu'un handler dans .NET MAUI 10 ?

Un handler, c'est le composant d'infrastructure de .NET MAUI qui traduit les propriétés d'un contrôle multiplateforme (la VirtualView) en instructions concrètes sur la vue native de chaque système d'exploitation (la PlatformView). Là où Xamarin.Forms passait par un Renderer monolithique qui héritait de la vue native et interceptait chaque changement via OnElementPropertyChanged, MAUI a inversé le contrôle. Le handler ne dérive plus de la vue native, il la compose. Chaque instance de Button, Entry ou Label a un handler qui expose un PropertyMapper<TVirtualView, THandler>, une table de correspondance propriété .NET → action native évaluée sans réflexion.

Concrètement, quand vous écrivez myEntry.TextColor = Colors.Red, MAUI appelle handler.UpdateValue(nameof(IEntry.TextColor)). Le mapper cherche l'entrée TextColor, exécute l'Action<IEntryHandler, IEntry> associée, laquelle configure UITextField.TextColor sur iOS ou EditText.SetTextColor(...) sur Android. Cette séparation apporte trois gains mesurables selon les benchmarks officiels de Microsoft Learn : démarrage plus rapide, moins d'allocations GC, et surtout la possibilité de modifier un handler existant sans le sous-classer. Ce dernier point était strictement impossible en Xamarin.Forms.

Quelle est la différence entre un Renderer Xamarin et un Handler MAUI ?

La différence tient en trois axes : héritage, cycle de vie, et point d'extension. Le tableau ci-dessous synthétise les correspondances les plus utilisées lors d'une migration.

AspectCustom Renderer (Xamarin.Forms)Handler (.NET MAUI 10)
Relation à la vue nativeHérite de la vue nativeCompose la vue native (propriété PlatformView)
Point d'initialisationOnElementChangedConnectHandler(PlatformView)
Point de nettoyageAucun explicite (dispose manuel)DisconnectHandler(PlatformView)
Réaction aux propriétésOnElementPropertyChanged + switchEntrées dans PropertyMapper
Modification sans sous-classerImpossibleMapper.AppendToMapping(...)
Enregistrement[assembly: ExportRenderer]handlers.AddHandler<T, THandler>()
TestabilitéDifficile (couplage natif)Handler mockable via interface IViewHandler
PerformanceRéflexion sur propriétésDispatch direct par mapper

La ligne Modification sans sous-classer est probablement le changement le plus sous-estimé. Honnêtement, c'est celui qui m'a fait gagner le plus de temps sur ma dernière migration. Elle permet, en trois lignes de code dans MauiProgram.cs, d'enlever la ligne de soulignement de tous les Entry sur Android sans écrire une seule classe. On détaille ce pattern plus bas. Pour un rappel complet du parcours de migration, voir aussi notre guide de migration Xamarin.Forms vers .NET MAUI.

Comment créer un handler personnalisé dans .NET MAUI 10 ?

Créer un handler personnalisé se fait en quatre étapes : (1) définir l'interface de la vue virtuelle, (2) créer la View multiplateforme, (3) écrire les partial classes du handler par plateforme, (4) l'enregistrer. L'exemple ci-dessous implémente un contrôle ColorSwatch qui affiche un rectangle de couleur avec un contour arrondi, sur iOS et Android. Rien de compliqué, mais il illustre chaque brique du modèle.

1. L'interface et la vue virtuelle

// ColorSwatch.cs (dossier Controls/)
using Microsoft.Maui.Controls;
using Microsoft.Maui.Graphics;

namespace MonApp.Controls;

public interface IColorSwatch : IView
{
    Color SwatchColor { get; }
    float CornerRadius { get; }
}

public class ColorSwatch : View, IColorSwatch
{
    public static readonly BindableProperty SwatchColorProperty =
        BindableProperty.Create(nameof(SwatchColor), typeof(Color), typeof(ColorSwatch), Colors.Gray);

    public static readonly BindableProperty CornerRadiusProperty =
        BindableProperty.Create(nameof(CornerRadius), typeof(float), typeof(ColorSwatch), 8f);

    public Color SwatchColor
    {
        get => (Color)GetValue(SwatchColorProperty);
        set => SetValue(SwatchColorProperty, value);
    }

    public float CornerRadius
    {
        get => (float)GetValue(CornerRadiusProperty);
        set => SetValue(CornerRadiusProperty, value);
    }
}

2. La classe partielle centrale du handler

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

namespace MonApp.Handlers;

public partial class ColorSwatchHandler
{
    public static IPropertyMapper<IColorSwatch, ColorSwatchHandler> Mapper =
        new PropertyMapper<IColorSwatch, ColorSwatchHandler>(ViewHandler.ViewMapper)
        {
            [nameof(IColorSwatch.SwatchColor)] = MapSwatchColor,
            [nameof(IColorSwatch.CornerRadius)] = MapCornerRadius,
        };

    public static CommandMapper<IColorSwatch, ColorSwatchHandler> CommandMapper =
        new(ViewHandler.ViewCommandMapper);

    public ColorSwatchHandler() : base(Mapper, CommandMapper) { }
}

3. L'implémentation iOS

// Handlers/ColorSwatchHandler.iOS.cs
#if IOS || MACCATALYST
using Microsoft.Maui.Handlers;
using Microsoft.Maui.Platform;
using UIKit;

namespace MonApp.Handlers;

public partial class ColorSwatchHandler : ViewHandler<IColorSwatch, UIView>
{
    protected override UIView CreatePlatformView() => new UIView();

    static void MapSwatchColor(ColorSwatchHandler handler, IColorSwatch view)
        => handler.PlatformView.BackgroundColor = view.SwatchColor.ToPlatform();

    static void MapCornerRadius(ColorSwatchHandler handler, IColorSwatch view)
    {
        handler.PlatformView.Layer.CornerRadius = view.CornerRadius;
        handler.PlatformView.ClipsToBounds = true;
    }
}
#endif

4. L'implémentation Android

// Handlers/ColorSwatchHandler.Android.cs
#if ANDROID
using Android.Graphics.Drawables;
using Microsoft.Maui.Handlers;
using Microsoft.Maui.Platform;
using AView = Android.Views.View;

namespace MonApp.Handlers;

public partial class ColorSwatchHandler : ViewHandler<IColorSwatch, AView>
{
    protected override AView CreatePlatformView()
        => new AView(Context) { Background = new GradientDrawable() };

    static void MapSwatchColor(ColorSwatchHandler handler, IColorSwatch view)
    {
        if (handler.PlatformView.Background is GradientDrawable gd)
            gd.SetColor(view.SwatchColor.ToPlatform());
    }

    static void MapCornerRadius(ColorSwatchHandler handler, IColorSwatch view)
    {
        if (handler.PlatformView.Background is GradientDrawable gd)
            gd.SetCornerRadius(view.CornerRadius * handler.Context.Resources.DisplayMetrics.Density);
    }
}
#endif

Où enregistrer les handlers dans MauiProgram.cs ?

Un handler qui n'est pas enregistré n'est jamais instancié. MAUI se contente d'afficher une vue vide, sans erreur ni warning (oui, c'est frustrant la première fois). L'enregistrement se fait dans la méthode CreateMauiApp() de MauiProgram.cs, à l'intérieur du fluent .ConfigureMauiHandlers(...). Ce point d'extension est appelé avant la construction de la fenêtre, donc avant tout data-binding, ce qui garantit que le contrôle sera reconnu partout dans l'application.

// MauiProgram.cs
using MonApp.Controls;
using MonApp.Handlers;

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

        builder
            .UseMauiApp<App>()
            .ConfigureMauiHandlers(handlers =>
            {
                handlers.AddHandler<ColorSwatch, ColorSwatchHandler>();
                // Ajouter d'autres handlers ici
            })
            .ConfigureFonts(fonts =>
            {
                fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
            });

#if DEBUG
        builder.Logging.AddDebug();
#endif

        return builder.Build();
    }
}

Vous pouvez ensuite utiliser le contrôle en XAML comme n'importe quel autre :

<ContentPage xmlns:controls="clr-namespace:MonApp.Controls">
    <controls:ColorSwatch SwatchColor="Coral"
                          CornerRadius="16"
                          HeightRequest="80"
                          WidthRequest="80" />
</ContentPage>

Modifier un handler existant sans en créer un nouveau

Le pattern le plus sous-utilisé de .NET MAUI, c'est la modification d'un handler existant. Plutôt que de créer MyEntryHandler : EntryHandler pour supprimer la ligne de soulignement d'Android, on peut simplement ajouter une entrée au mapper de EntryHandler une seule fois au démarrage. Cette approche évite la duplication et fonctionne pour tous les Entry de l'application, y compris ceux dans des packages tiers (ça vaut son pesant d'or quand vous utilisez du Syncfusion ou du DevExpress).

// MauiProgram.cs (dans CreateMauiApp, avant .Build())
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 || MACCATALYST
    handler.PlatformView.BorderStyle = UIKit.UITextBorderStyle.None;
#endif
});

Trois variantes sont disponibles selon l'effet recherché :

  • AppendToMapping : exécute votre action après le mapper d'origine. À utiliser pour surcharger une valeur déjà appliquée.
  • PrependToMapping : exécute avant. Utile pour préparer l'état natif avant que MAUI n'écrive dessus.
  • ModifyMapping : remplace complètement l'entrée. À manier avec précaution : si vous oubliez d'appeler la valeur d'origine, le comportement standard disparaît sans avertissement.

Ce mécanisme rend possible ce qui prenait des dizaines de sous-classes en Xamarin.Forms. Jetez aussi un œil à notre article MVVM avec CommunityToolkit.Mvvm pour voir comment brancher ces handlers personnalisés à vos ViewModels via injection de dépendances.

Comment migrer un Custom Renderer Xamarin vers un Handler MAUI ?

La migration d'un Renderer Xamarin.Forms vers un handler MAUI suit un patron mécanique une fois qu'on l'a compris. Prenons un cas classique : un EntryRenderer qui applique une police et supprime la bordure sur Android.

Le Renderer Xamarin d'origine

// Xamarin.Forms — Droid/Renderers/BorderlessEntryRenderer.cs
[assembly: ExportRenderer(typeof(Entry), typeof(BorderlessEntryRenderer))]
public class BorderlessEntryRenderer : EntryRenderer
{
    public BorderlessEntryRenderer(Context ctx) : base(ctx) { }

    protected override void OnElementChanged(ElementChangedEventArgs<Entry> e)
    {
        base.OnElementChanged(e);
        if (Control != null)
        {
            Control.Background = null;
            Control.SetPadding(0, 0, 0, 0);
        }
    }
}

Le handler MAUI équivalent

// .NET MAUI — Handlers/BorderlessEntryHandler.cs
#if ANDROID
using Microsoft.Maui.Handlers;

public class BorderlessEntryHandler : EntryHandler
{
    protected override void ConnectHandler(AndroidX.AppCompat.Widget.AppCompatEditText platformView)
    {
        base.ConnectHandler(platformView);
        platformView.Background = null;
        platformView.SetPadding(0, 0, 0, 0);
    }
}
#endif

Notez trois différences fondamentales :

  1. Point d'entrée : OnElementChanged devient ConnectHandler. Le paramètre est directement la vue native typée, plus besoin d'accéder à Control.
  2. Enregistrement : l'attribut [assembly: ExportRenderer] disparaît, remplacé par handlers.AddHandler<Entry, BorderlessEntryHandler>() dans MauiProgram.cs.
  3. Nettoyage : ajoutez un DisconnectHandler pour libérer les événements souscrits, sinon vous introduisez des fuites mémoire lors de la re-navigation.
protected override void DisconnectHandler(AndroidX.AppCompat.Widget.AppCompatEditText platformView)
{
    // Désabonnez ici tout événement natif souscrit dans ConnectHandler
    base.DisconnectHandler(platformView);
}

Le wiki officiel du dépôt dotnet/maui sur GitHub maintient une liste de correspondance Renderer vers Handler pour chaque contrôle intégré. Consultez-la avant d'écrire un handler depuis zéro, ça vous évitera de réinventer la roue.

Tester et déboguer un handler personnalisé

Les handlers sont beaucoup plus testables que les Renderers grâce à leur interface IViewHandler et à la propriété PlatformView qui peut être inspectée après le rendu. Pour un test unitaire, vous pouvez instancier manuellement un handler et vérifier que le mapper produit l'effet attendu :

[Fact]
public void SwatchColor_SetsBackground()
{
    // Arrange
    var mauiContext = MauiUnitTests.CreateMauiContext();
    var swatch = new ColorSwatch { SwatchColor = Colors.Coral };
    var handler = new ColorSwatchHandler();

    // Act
    handler.SetMauiContext(mauiContext);
    handler.SetVirtualView(swatch);
    handler.UpdateValue(nameof(IColorSwatch.SwatchColor));

    // Assert
#if IOS
    var uiColor = handler.PlatformView.BackgroundColor;
    Assert.Equal(UIColor.FromRGB(255, 127, 80), uiColor);
#endif
}

Pour le débogage visuel, activez la sortie détaillée de MAUI en ajoutant builder.Logging.SetMinimumLevel(LogLevel.Debug) dans MauiProgram.cs. Chaque appel de mapper est journalisé avec le nom de la propriété, ce qui facilite l'identification des re-rendus inutiles. Pour une couverture plus large, notre guide complet des tests dans .NET MAUI 10 détaille les stratégies xUnit et Appium applicables aux handlers.

Pièges courants et bonnes pratiques

Après avoir accompagné plusieurs équipes en migration, les mêmes pièges reviennent systématiquement. Voici la liste courte de ce qu'il faut éviter en 2026 :

  • Oublier DisconnectHandler pour un handler qui souscrit à des événements natifs. C'est la cause n°1 des fuites mémoire iOS/Android que j'ai vues en prod.
  • Utiliser des chaînes littérales dans le mapper au lieu de nameof, ce qui casse silencieusement après un renommage.
  • Enregistrer un handler après Build(), ce qui n'a aucun effet et provoque un rendu par défaut sans erreur.
  • Sous-classer un handler alors qu'un AppendToMapping suffirait, ce qui augmente la surface de code et le couplage.
  • Accéder à PlatformView dans le constructeur du handler. À ce stade, la vue native n'est pas encore créée, il faut attendre ConnectHandler.
  • Manipuler la vue native depuis un thread non-UI. Utilisez MainThread.BeginInvokeOnMainThread ou Dispatcher.Dispatch depuis MAUI 10.

En bonne pratique, isolez toujours le code spécifique à une plateforme dans des fichiers partiels séparés (Handler.iOS.cs, Handler.Android.cs) plutôt que dans des blocs #if monolithiques. Le compilateur multi-cible de MAUI ne compilera que le bon fichier pour chaque plateforme, ce qui garde le code lisible et empêche les erreurs de portabilité de fuiter d'une plateforme à l'autre. Enfin, pour les contrôles complexes, envisagez d'exposer un événement Loaded sur la VirtualView plutôt que d'utiliser ConnectHandler comme une méthode d'initialisation métier. Ça garde votre logique métier hors de l'infrastructure de rendu, et ça facilite énormément les tests.

Questions fréquentes

Peut-on utiliser un Custom Renderer Xamarin.Forms directement dans .NET MAUI 10 ?

Oui, .NET MAUI 10 inclut toujours la couche de compatibilité Microsoft.Maui.Controls.Compatibility qui permet aux Custom Renderers Xamarin.Forms de fonctionner sans modification. Cependant, cette couche ne bénéficie pas des optimisations mapper et sera supprimée dans MAUI 12 (prévu pour novembre 2027). Il est recommandé de migrer progressivement vers des handlers pour préparer la transition.

Qu'est-ce que le PropertyMapper et pourquoi n'utilise-t-il pas la réflexion ?

Le PropertyMapper est un dictionnaire fortement typé qui associe un nom de propriété à un Action<THandler, TVirtualView>. À l'exécution, MAUI fait un simple lookup par clé et invoque le délégué. Aucune réflexion sur les PropertyInfo n'est nécessaire, ce qui améliore les performances de démarrage et rend le code compatible avec NativeAOT sans warning.

Comment personnaliser un contrôle uniquement sur une plateforme sans dupliquer le handler ?

Utilisez Mapper.AppendToMapping à l'intérieur d'un bloc #if ANDROID ou #if IOS. La modification n'est compilée que pour la plateforme ciblée et s'applique à tous les contrôles du type concerné, sans créer de nouvelle classe. C'est le pattern recommandé pour les ajustements ponctuels (couleurs, bordures, polices).

Faut-il enregistrer chaque handler manuellement ou existe-t-il un scan automatique ?

L'enregistrement manuel via handlers.AddHandler<T, THandler>() reste la méthode officielle en .NET MAUI 10. Un scan par attribut a été proposé dans la RFC dotnet/maui#18234, mais rejeté pour préserver la compatibilité NativeAOT. Pour les grandes bases de code, vous pouvez néanmoins créer une méthode d'extension qui regroupe tous vos AddHandler par convention.

Un handler slim est-il compatible avec les contrôles qui n'ont pas de vue native ?

Oui, .NET MAUI 10 introduit les slim handlers (via l'interface IElementHandler) qui peuvent omettre entièrement la PlatformView. Ils sont utiles pour des décorateurs de layout, des behaviors avancés ou des contrôles purement visuels dessinés via GraphicsView. Le mapper fonctionne identiquement mais sans création de vue native.

Editorial Team
À propos de l'auteur Editorial Team

Our team of expert writers and editors.