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.
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.
Aspect
Custom Renderer (Xamarin.Forms)
Handler (.NET MAUI 10)
Relation à la vue native
Hérite de la vue native
Compose la vue native (propriété PlatformView)
Point d'initialisation
OnElementChanged
ConnectHandler(PlatformView)
Point de nettoyage
Aucun explicite (dispose manuel)
DisconnectHandler(PlatformView)
Réaction aux propriétés
OnElementPropertyChanged + switch
Entrées dans PropertyMapper
Modification sans sous-classer
Impossible
Mapper.AppendToMapping(...)
Enregistrement
[assembly: ExportRenderer]
handlers.AddHandler<T, THandler>()
Testabilité
Difficile (couplage natif)
Handler mockable via interface IViewHandler
Performance
Réflexion sur propriétés
Dispatch 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 :
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).
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.
Point d'entrée : OnElementChanged devient ConnectHandler. Le paramètre est directement la vue native typée, plus besoin d'accéder à Control.
Enregistrement : l'attribut [assembly: ExportRenderer] disparaît, remplacé par handlers.AddHandler<Entry, BorderlessEntryHandler>() dans MauiProgram.cs.
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.
Choisir entre SQLite, Preferences et SecureStorage dans .NET MAUI 10 selon la nature des données. Repository pattern, migrations, chiffrement SQLCipher, pièges de production et bonnes pratiques 2026 avec exemples de code testés.
Tutoriel complet pour ajouter les notifications push dans .NET MAUI 10 : configuration FCM Android, APNs iOS avec clé .p8, deep linking Shell et gestion des états premier plan, arrière-plan et arrêté. Avec code C# prêt à copier.
Guide complet de la navigation Shell dans .NET MAUI 10. Routes URI, deep links iOS/Android, injection de dépendances et passage de paramètres typés, avec exemples testés en production et pièges courants à éviter.