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.
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 :
<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 :
- Point d'entrée :
OnElementChangeddevientConnectHandler. Le paramètre est directement la vue native typée, plus besoin d'accéder àControl. - Enregistrement : l'attribut
[assembly: ExportRenderer]disparaît, remplacé parhandlers.AddHandler<Entry, BorderlessEntryHandler>()dansMauiProgram.cs. - Nettoyage : ajoutez un
DisconnectHandlerpour 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
DisconnectHandlerpour 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
AppendToMappingsuffirait, ce qui augmente la surface de code et le couplage. - Accéder à
PlatformViewdans le constructeur du handler. À ce stade, la vue native n'est pas encore créée, il faut attendreConnectHandler. - Manipuler la vue native depuis un thread non-UI. Utilisez
MainThread.BeginInvokeOnMainThreadouDispatcher.Dispatchdepuis 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.




