Custom Handlers no .NET MAUI: Guia Completo para Personalizar Controles Nativos em 2026
Guia completo de Custom Handlers no .NET MAUI 2026: PropertyMapper, CommandMapper, ciclo de vida ConnectHandler/DisconnectHandler, exemplos praticos em iOS e Android e como migrar do Xamarin.Forms sem dores.
Custom Handlers no .NET MAUI são a arquitetura de renderização que substitui os antigos Custom Renderers do Xamarin.Forms, expondo mapeadores desacoplados (PropertyMapper, CommandMapper) que traduzem propriedades cross-platform em chamadas diretas para UIView no iOS, android.view.View no Android e FrameworkElement no Windows. Se você veio do Xamarin.Forms esperando herdar de ViewRenderer, esqueça. Em 2026, com .NET 9 já estável e .NET 10 no horizonte, a Microsoft moveu tudo para uma pipeline mais leve, mais testável e (importante) sem acoplamento à árvore visual do controle base.
Honestamente, essa mudança confunde bastante quem chega vindo do Xamarin. Eu mesmo passei semanas tentando "portar" renderers antes de aceitar que o modelo mental era outro. Vamos ao que interessa.
Handlers substituem Renderers usando o padrão Mapper, eliminando herança e reduzindo overhead de instanciação em cerca de 30% segundo benchmarks da equipe .NET MAUI.
Você personaliza controles existentes sem criar subclasses. Basta modificar Entry.PropertyMapper em MauiProgram.cs.
Para controles totalmente novos, herde de ViewHandler<TVirtualView, TPlatformView> e implemente CreatePlatformView().
O acesso à view nativa (PlatformView) é sempre tipado: UITextField no iOS, AppCompatEditText no Android, sem casts arriscados.
Handlers rodam por plataforma via pastas Platforms/iOS, Platforms/Android e diretivas #if IOS, mantendo o código compartilhado limpo.
O ciclo de vida é explícito com ConnectHandler e DisconnectHandler. Não gerenciar corretamente vaza memória em iOS mais rápido do que em Android.
O que são Handlers no .NET MAUI?
Handlers são classes intermediárias que ligam um controle cross-platform (chamado no jargão da equipe MAUI de virtual view) à sua contraparte nativa em cada plataforma. Quando você escreve <Entry Text="Olá" /> em XAML, o runtime não instancia diretamente um UITextField. Ele instancia um Microsoft.Maui.Controls.Entry, resolve o EntryHandler registrado, e o handler chama CreatePlatformView() para materializar o controle nativo. Depois, o PropertyMapper (um dicionário estático) é iterado e cada propriedade cross-platform (Text, TextColor, Placeholder...) é aplicada no controle nativo.
Na prática, isso significa três coisas concretas que impactam seu código. Primeiro: não existe mais uma classe base "peso" para herdar. O handler é composto, não herdado. Segundo: o custo de criar um controle caiu porque a árvore de tipos ficou rasa. Segundo os benchmarks oficiais do .NET 9, a instanciação de Entry ficou cerca de 30% mais rápida vs. Xamarin.Forms. Terceiro: você pode modificar o comportamento de qualquer controle globalmente adicionando uma linha em MauiProgram.cs, sem subclasses e sem attached properties auxiliares.
Qual a diferença entre Handlers e Renderers do Xamarin.Forms?
Se você migrou (ou está migrando) do Xamarin.Forms, essa é a mudança arquitetural que mais dói no começo. Renderers eram subclasses concretas. Você herdava de EntryRenderer, sobrescrevia OnElementChanged e mexia no Control. Handlers invertem o modelo: você não herda para customizar, você mapeia. Isso resolve três problemas velhos de uma vez: acoplamento com a classe base, dificuldade de testar (renderers exigiam mock de Context Android e UIViewController iOS), e overhead de reflection na resolução.
Aspecto
Renderer (Xamarin.Forms)
Handler (.NET MAUI)
Padrão de extensão
Herança (subclasse do renderer base)
Composição (modificar o Mapper)
Acesso ao nativo
this.Control (fracamente tipado)
PlatformView (fortemente tipado)
Registro
Atributo [assembly: ExportRenderer]
Fluent API em MauiProgram.cs
Ciclo de vida
OnElementChanged
ConnectHandler / DisconnectHandler
Testabilidade
Baixa (depende do runtime)
Alta (Mapper é dicionário puro)
Overhead
Reflection em cada resolução
Lookup estático em dicionário
Cross-platform
Um renderer por plataforma
Handler compartilhado + partial per-platform
O guia oficial da Microsoft para migração de Renderer para Handler lista as equivalências API por API. Mas a lição prática que aprendi migrando três apps de produção em 2025 é essa: você raramente precisa de um handler novo. 80% dos casos que exigiam um Custom Renderer no Xamarin.Forms são resolvidos hoje modificando o PropertyMapper do controle existente. Se você está começando a portar código, dê uma olhada também no nosso guia prático de migração do Xamarin.Forms para .NET MAUI para ver o quadro maior.
Arquitetura do padrão Mapper: PropertyMapper e CommandMapper
Todo handler expõe dois dicionários estáticos: PropertyMapper e CommandMapper. O primeiro mapeia nomes de propriedades cross-platform para ações que aplicam o valor no controle nativo. O segundo mapeia comandos (ações imperativas como "focar", "rolar até") para métodos nativos. Ambos são IPropertyMapper (na verdade um PropertyMapper<TVirtualView, THandler>) e são iterados após ConnectHandler para semear o estado inicial e novamente sempre que a propriedade muda no lado managed.
A forma canônica de estender é chamar .AppendToMapping ou .PrependToMapping. Isso preserva o comportamento default (o mapper original ainda roda) e permite injetar sua lógica antes ou depois. Substituir uma entrada com .ReplaceMapping é a opção nuclear. Só use quando o default está simplesmente errado para o seu app.
// MauiProgram.cs: remove a borda inferior do Entry no Android globalmente
using Microsoft.Maui.Handlers;
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder.UseMauiApp<App>();
#if ANDROID
EntryHandler.Mapper.AppendToMapping("SemBordaInferior", (handler, view) =>
{
// PlatformView aqui é AppCompatEditText
handler.PlatformView.BackgroundTintList =
Android.Content.Res.ColorStateList.ValueOf(
Android.Graphics.Color.Transparent);
});
#endif
return builder.Build();
}
Como personalizar um controle existente com Handler?
Este é o caso mais comum, provavelmente 80% do que você vai fazer. Vamos ao exemplo prático: remover a borda padrão do Entry em iOS e Android, algo que designers pedem em todo app que já toquei. No Xamarin.Forms isso exigia um Custom Renderer por plataforma. Com Handlers, é uma configuração no MauiProgram.cs.
using Microsoft.Maui.Handlers;
public static class EntrySemBordaExtensions
{
public static MauiAppBuilder ConfigurarEntrySemBorda(this MauiAppBuilder builder)
{
EntryHandler.Mapper.AppendToMapping("EntrySemBorda", (handler, view) =>
{
#if ANDROID
// Android: remover o underline nativo do AppCompatEditText
handler.PlatformView.Background = null;
#elif IOS || MACCATALYST
// iOS: BorderStyle None e remover leftView spacing
handler.PlatformView.BorderStyle = UIKit.UITextBorderStyle.None;
handler.PlatformView.Layer.BorderWidth = 0;
#elif WINDOWS
// Windows: escondendo a borda do TextBox
handler.PlatformView.BorderThickness = new Microsoft.UI.Xaml.Thickness(0);
#endif
});
return builder;
}
}
Aqui, uma coisa que a documentação não deixa clara: PlatformView é fortemente tipado pela plataforma. No iOS é UIKit.UITextField, no Android é AndroidX.AppCompat.Widget.AppCompatEditText (não a classe abstrata EditText que o Xamarin.Forms usava). Isso importa porque AppCompatEditText tem BackgroundTintList como propriedade separada de Background, e você precisa zerar ambos em alguns temas Material 3 para eliminar todos os artefatos visuais. Já perdi uma manhã inteira nesse detalhe.
Outra vantagem prática: como o mapper roda toda vez que a propriedade muda, se você vincular IsEnabled a um ViewModel e alternar dinamicamente, o handler reaplica seus overrides automaticamente. No mundo Renderer você precisava sobrescrever OnElementPropertyChanged e comparar strings. Dor bem específica que ninguém sente falta.
Como criar um Custom Handler do zero?
Quando você precisa expor um controle nativo que o .NET MAUI não oferece (digamos, um UIVisualEffectView blur no iOS ou um MaterialCardView específico no Android), o caminho é criar um View cross-platform mais um ViewHandler. Vamos construir um controle de badge simples que renderiza um círculo com contador, algo que você quer no ícone de notificações.
// Controls/BadgeView.cs: a virtual view
using Microsoft.Maui.Controls;
public class BadgeView : View
{
public static readonly BindableProperty CountProperty =
BindableProperty.Create(nameof(Count), typeof(int), typeof(BadgeView), 0,
propertyChanged: (b, o, n) => ((BadgeView)b).Handler?.UpdateValue(nameof(Count)));
public int Count
{
get => (int)GetValue(CountProperty);
set => SetValue(CountProperty, value);
}
}
// Handlers/BadgeViewHandler.cs: handler compartilhado (partial)
using Microsoft.Maui.Handlers;
public partial class BadgeViewHandler : ViewHandler<BadgeView, PlatformBadgeView>
{
public static IPropertyMapper<BadgeView, BadgeViewHandler> PropertyMapper =
new PropertyMapper<BadgeView, BadgeViewHandler>(ViewHandler.ViewMapper)
{
[nameof(BadgeView.Count)] = MapCount,
};
public BadgeViewHandler() : base(PropertyMapper) { }
static void MapCount(BadgeViewHandler handler, BadgeView view)
=> handler.PlatformView.SetCount(view.Count);
}
// Platforms/iOS/PlatformBadgeView.cs
using UIKit;
using CoreGraphics;
public class PlatformBadgeView : UIView
{
private readonly UILabel _label = new()
{
TextColor = UIColor.White,
Font = UIFont.BoldSystemFontOfSize(11),
TextAlignment = UITextAlignment.Center,
};
public PlatformBadgeView()
{
BackgroundColor = UIColor.SystemRed;
Layer.CornerRadius = 10;
AddSubview(_label);
}
public void SetCount(int count)
{
_label.Text = count > 99 ? "99+" : count.ToString();
Hidden = count == 0;
}
public override void LayoutSubviews()
{
base.LayoutSubviews();
_label.Frame = Bounds;
}
}
// Platforms/iOS/BadgeViewHandler.cs
public partial class BadgeViewHandler
{
protected override PlatformBadgeView CreatePlatformView() => new PlatformBadgeView();
}
Para Android, você repete o padrão com Platforms/Android/PlatformBadgeView.cs herdando de Android.Views.View (ou TextView), e para Windows com Border do WinUI. Finalmente, registre o handler no MauiProgram.cs:
Aqui é onde a maioria dos apps vaza memória, especialmente em iOS. Quando o handler é conectado a uma virtual view, ConnectHandler(PlatformView platformView) é chamado. Quando é desligado (por remoção da árvore visual ou navegação), DisconnectHandler deve limpar assinaturas de evento, timers, observadores KVO no iOS e listeners no Android.
Um detalhe que a Apple documenta de forma sutil: em UIGestureRecognizer, o alvo é uma referência forte a partir do recognizer, mas o recognizer é retido pela view. Se o alvo for o handler e você não remover o recognizer no DisconnectHandler, você tem uma retention cycle clássica. O app não crasha, mas cada tela reprodutora do controle vaza. Peguei esse exato bug num app de finanças que abria e fechava telas de detalhe centenas de vezes por sessão. No Android o GC te salva mais rápido, mas isso não é desculpa para não desanexar listeners.
Importante em .NET MAUI 9+: por padrão, DisconnectHandlernão é chamado automaticamente em todos os cenários por razões de compatibilidade retroativa. A recomendação oficial é opt-in via Microsoft.Maui.Controls.Handler.HandlerProperties.SetDisconnectPolicy ou chamando handler.DisconnectHandler() manualmente ao sair da página. Testes em memória com o profiler do Visual Studio 2026 mostram diferenças perceptíveis em navegação pesada.
Truques específicos de iOS e Android que a documentação não mostra
Depois de anos com Xamarin e agora MAUI em produção, alguns aprendizados nascem só de bater a cara na parede. Vou pinçar os que mais me poupam tempo hoje.
iOS: use Layer, não subviews, para efeitos visuais
Efeitos como borda arredondada, sombra e gradient são infinitamente mais baratos aplicados na CALayer da view do que empilhando subviews. Um Layer.CornerRadius = 8 mais Layer.MasksToBounds = true renderiza no compositor GPU e não força relayout. Adicionar uma UIView filha para "moldura" força o autolayout a recalcular a cada scroll.
Android: cuidado com temas Material 3
Se seu projeto usa Theme.Material3.DayNight (padrão em novos templates MAUI 10), controles como AppCompatEditText vêm com tint states aplicados via ThemeOverlay. Setar Background = null às vezes não é suficiente. Você precisa também limpar BackgroundTintList. Consulte o guia oficial do Android sobre temas e estilos para o modelo de resolução.
Compartilhar código entre plataformas com partial classes
A convenção da equipe MAUI, herdada do Multi-Targeting do net9.0-ios/net9.0-android, é usar partial class declarada no diretório compartilhado e implementações específicas em Platforms/iOS e Platforms/Android. Isso é mais limpo que #if em massa e o IntelliSense funciona corretamente ao alternar o TFM ativo no Visual Studio.
Como depurar Custom Handlers no .NET MAUI
Debug de handler é diferente de debug de código managed puro por uma razão simples: parte da execução acontece no runtime nativo. Três técnicas que uso todo dia:
Breakpoint dentro do lambda do mapper. Funciona normalmente e permite inspecionar PlatformView tipado. É a forma mais rápida de confirmar que a propriedade está sendo mapeada.
View Hierarchy Debugger. No iOS use "Debug View Hierarchy" do Xcode conectado ao processo do app. No Android, "Layout Inspector" do Android Studio. Ambos mostram o controle nativo real, não a virtual view, o que revela quando um PlatformView não foi adicionado à árvore.
MAUI_DIAGNOSTICS_ENABLED. Variável de ambiente que ativa logs verbose do handler. Adicione em launchSettings.json para ver cada Connect/Disconnect.
Para casos onde o problema é performance, meu fluxo é: perfil no dotnet-trace, filtrar por eventos do MAUI, e correlacionar com o Instruments (iOS) ou Perfetto (Android). Se você precisa ir mais fundo em performance geral do app, temos um guia completo de otimização de performance no .NET MAUI que cobre startup, layout e listas.
Erros comuns e como evitá-los
Depois de revisar código de várias equipes migrando para MAUI, os mesmos erros aparecem repetidamente. Anote:
Esquecer de chamar base.ConnectHandler. Pula a inicialização do handler base e o controle não responde a mudanças de propriedade. Sempre chame o base primeiro.
Registrar handler no Assembly errado. Em multi-targeting, o handler precisa ser visível no TFM da plataforma. Se você colocar em Platforms/iOS mas registrar em código compartilhado sem #if IOS, o build Android quebra.
Modificar PropertyMapper em tempo de execução após inicialização. O mapper é estático. Modificar depois que instâncias já existem não reaplica em views existentes. Faça no MauiProgram.cs.
Confundir VirtualView com Element. Herança nova. Use VirtualView quando precisar do modelo cross-platform.
Depender do Handler antes da view ser conectada à árvore.view.Handler é null até o controle ser materializado. Se você chama métodos dele no construtor da página, vai receber NullReferenceException.
Para conectar o padrão de handlers com uma arquitetura MVVM bem estruturada, veja também o nosso material sobre MVVM com CommunityToolkit no .NET MAUI. Handlers são o "como" da UI; MVVM é o "por quê". A combinação é o que separa apps de brinquedo de apps que escalam.
Perguntas frequentes
Custom Renderers do Xamarin.Forms ainda funcionam no .NET MAUI?
Sim, via o modo de compatibilidade (Microsoft.Maui.Controls.Compatibility), mas a Microsoft marcou como caminho de migração temporário e não recebe mais melhorias de performance. A recomendação oficial é reescrever para Handlers durante o esforço de migração para .NET MAUI 9+.
Preciso de um Custom Handler para simplesmente mudar a cor de um botão?
Não. Mudanças cosméticas suportadas pelas propriedades cross-platform (BackgroundColor, TextColor, CornerRadius) devem ser feitas em XAML e estilos. Handlers são para comportamento ou propriedades nativas que não têm equivalente cross-platform.
Onde registro um Custom Handler no .NET MAUI?
Em MauiProgram.cs, dentro de ConfigureMauiHandlers, usando handlers.AddHandler<MinhaView, MeuHandler>(). Handlers registrados só entram em vigor após o próximo build, não são hot-reload em tempo real.
Como acesso a view nativa dentro de um handler?
Pela propriedade PlatformView, que é tipada segundo o segundo genérico de ViewHandler<TVirtualView, TPlatformView>. No iOS será um UIView ou subclasse. No Android, Android.Views.View ou subclasse. Você acessa métodos nativos diretamente sem casts.
O que acontece se eu esquecer de implementar DisconnectHandler?
Você não terá crash imediato, mas vazamentos de memória se acumulam, especialmente em iOS onde UIGestureRecognizer forma ciclos de retenção. Em apps com navegação intensa, isso aparece rapidamente no profiler como UIViews órfãos que nunca foram desalocados.
Aprenda Shell Navigation no .NET MAUI 9: estrutura da AppShell, rotas absolutas e relativas, ShellNavigationQueryParameters, deep linking universal e integração com MVVM. Guia com exemplos reais e armadilhas comuns em produção.
Aprenda a usar MVVM no .NET MAUI com CommunityToolkit.Mvvm. Guia prático com source generators, ObservableProperty, RelayCommand, Messenger, validação de dados e exemplos completos de código.