Handlers personalizados en .NET MAUI 10: customizar controles nativos iOS y Android (2026)

Los handlers en .NET MAUI 10 sustituyen a los Custom Renderers de Xamarin.Forms. Aprende a modificar controles nativos iOS y Android con PropertyMapper, ConnectHandler y ejemplos reales de un proyecto en producción.

Handlers Personalizados .NET MAUI 10 (2026)
On this page
  1. ¿Qué son los handlers en .NET MAUI 10?
  2. Handler vs Renderer: ¿cuál es la diferencia?
  3. Cómo modificar un handler existente sin crear una subclase
  4. Crear un handler personalizado paso a paso
  5. PlatformView y VirtualView: acceso a la vista nativa
  6. Ciclo de vida: ConnectHandler, DisconnectHandler y fugas de memoria
  7. Registrar handlers en MauiProgram.cs
  8. Errores comunes al usar handlers personalizados

Actualizado: 18 de septiembre de 2026

Los handlers personalizados en .NET MAUI 10 son las clases puente que conectan un control abstracto (por ejemplo, Button) con su vista nativa real (UIButton en iOS, AppCompatButton en Android) y permiten modificar cualquier propiedad de esa vista nativa sin subclasear. Se registran una sola vez en MauiProgram.cs y trabajan sobre dos diccionarios estáticos (PropertyMapper y CommandMapper) que reciben la señal cuando cambia una propiedad enlazada. Todo lo que aquí explico está verificado sobre .NET 10 GA (noviembre 2025) y Xcode 17, y sobre un par de apps que llevé a producción este verano.

¿Qué son los handlers en .NET MAUI 10?

Un handler es la clase intermedia que .NET MAUI usa para materializar un control cross-platform en su equivalente nativo. Cuando declaras <Button Text="Hola" /> en XAML, el runtime instancia un Microsoft.Maui.Controls.Button (la virtual view) y le asigna un ButtonHandler que, por debajo, crea un UIButton en iOS o un MaterialButton en Android (la platform view). El handler no hereda del control, ni del nativo: solo actúa como pegamento.

La lista completa de handlers integrados vive en el ensamblado Microsoft.Maui y sigue el patrón I{Control}Handler: IButtonHandler, IEntryHandler, ILabelHandler, etc. La documentación oficial de Microsoft sobre handlers mantiene una tabla actualizada por versión. En .NET 10 el equipo eliminó tres handlers heredados de Xamarin (WebViewLegacyHandler, entre otros) y añadió IHybridWebViewHandler para el nuevo control híbrido de Blazor.

Lo importante: cada handler expone dos diccionarios estáticos que rigen su comportamiento. Mapper (o PropertyMapper) reacciona a cambios de propiedades enlazadas, y CommandMapper reacciona a llamadas explícitas al método Invoke. Modificar esos diccionarios es lo que te permite personalizar un control globalmente sin escribir una sola subclase.

Handler vs Renderer: ¿cuál es la diferencia?

Los Custom Renderers de Xamarin.Forms exigían herencia: creabas una clase MyButtonRenderer : ButtonRenderer, sobrescribías OnElementChanged, y luego decorabas con [assembly: ExportRenderer]. Cualquier cambio implicaba reescribir la cadena de herencia entera, y compartir código entre plataformas era doloroso porque cada renderer vivía en su carpeta específica.

Los handlers atacan el mismo problema desde otro ángulo. No hay herencia, no hay atributos globales, no hay OnElementChanged. Solo dos operaciones: registrar un mapping o registrar una acción sobre un comando. Esta tabla resume las diferencias que más impactan al día a día:

AspectoCustom Renderer (Xamarin.Forms)Handler (.NET MAUI 10)
Modelo de extensiónHerencia obligatoriaComposición sobre Mapper
Registro[assembly: ExportRenderer]ConfigureMauiHandlers en MauiProgram
Acceso al nativoControl (tipado según renderer)PlatformView (tipo nativo real)
Ciclo de vidaOnElementChangedConnectHandler / DisconnectHandler
Modificación global sin subclaseNo soportadaSí, editando el mapper estático
Rendimiento de arranqueAlto coste por reflexiónResolución directa vía DI

La consecuencia práctica: en MAUI puedes modificar el aspecto de todos los Entry de tu app (quitar el subrayado en Android, cambiar el color del cursor en iOS) con seis líneas dentro de MauiProgram.cs, sin crear ni una subclase. Eso era literalmente imposible en Xamarin.Forms.

Cómo modificar un handler existente sin crear una subclase

Honestamente, este es el patrón que uso el 80 % del tiempo. Imagina que quieres eliminar el subrayado que Android pinta debajo de todo Entry (viene del tema Material) y ocultar el borde en iOS. En lugar de subclasear EntryHandler, basta con inyectar dos entradas en el mapper:

// En MauiProgram.cs, antes de builder.Build()
Microsoft.Maui.Handlers.EntryHandler.Mapper.AppendToMapping("SinBordes", (handler, view) =>
{
#if ANDROID
    handler.PlatformView.BackgroundTintList =
        Android.Content.Res.ColorStateList.ValueOf(
            Android.Graphics.Color.Transparent);
#elif IOS
    handler.PlatformView.BorderStyle = UIKit.UITextBorderStyle.None;
#endif
});

AppendToMapping registra una acción que se ejecutará después de todos los mappings por defecto, así que tus cambios sobrescriben lo que MAUI habría hecho normalmente. Si necesitas ejecutar antes, existe PrependToMapping; si necesitas reemplazar por completo la lógica de una propiedad concreta, usa ModifyMapping.

Un detalle que la documentación menciona en letra pequeña: el mapping se dispara una vez por cada instancia del control cuando el handler se conecta. No se ejecuta cada vez que cambia el valor de la propiedad; para eso hay que suscribirse dentro de ConnectHandler. Si vienes de MVVM con CommunityToolkit.Mvvm, piensa en el mapping como un partial ejecutado en la construcción del control nativo, no como un observador.

Crear un handler personalizado paso a paso

Cuando lo que necesitas es un control completamente nuevo (por ejemplo, un VideoPlayer que envuelva AVPlayer en iOS y ExoPlayer en Android), sí que hay que crear tu propio handler. Los pasos son cinco y siempre son los mismos:

  1. Definir la vista abstracta. Una clase que herede de View (o Element) con las propiedades bindables que quieres exponer.
  2. Crear la clase parcial del handler con PropertyMapper y CommandMapper.
  3. Implementar CreatePlatformView en cada plataforma (archivo con sufijo .iOS.cs, .Android.cs, etc.).
  4. Implementar ConnectHandler y DisconnectHandler para gestionar suscripciones y liberar recursos.
  5. Registrar el handler en MauiProgram.cs.

Un ejemplo mínimo de un control PulsingButton que hace pulsar un botón cada segundo:

// PulsingButton.cs - vista abstracta compartida
public class PulsingButton : View
{
    public static readonly BindableProperty TextProperty =
        BindableProperty.Create(nameof(Text), typeof(string), typeof(PulsingButton), string.Empty);

    public string Text
    {
        get => (string)GetValue(TextProperty);
        set => SetValue(TextProperty, value);
    }
}

// PulsingButtonHandler.cs - mapper compartido
public partial class PulsingButtonHandler
{
    public static IPropertyMapper<PulsingButton, PulsingButtonHandler> PropertyMapper =
        new PropertyMapper<PulsingButton, PulsingButtonHandler>(ViewMapper)
        {
            [nameof(PulsingButton.Text)] = MapText,
        };

    public PulsingButtonHandler() : base(PropertyMapper) { }
}

Después, un archivo parcial por plataforma se encarga de crear la vista nativa. La guía oficial para crear handlers tiene el ejemplo completo iOS + Android + Windows. En un proyecto real yo suelo empezar copiando ese esqueleto y borrar Windows si no lo necesito: reduce el ruido del proyecto y compila un 20 % más rápido.

PlatformView y VirtualView: acceso a la vista nativa

Dentro de un handler tienes dos propiedades siempre disponibles: VirtualView es la vista MAUI (por ejemplo, Button) y PlatformView es la vista nativa real. El tipo de PlatformView se resuelve por plataforma en compile-time:

  • En iOS es UIKit.UIButton (o el UIView concreto para tu control).
  • En Android es Google.Android.Material.Button.MaterialButton.
  • En Mac Catalyst se comparte con iOS.
  • En Windows (WinUI 3) es Microsoft.UI.Xaml.Controls.Button.

Aquí es donde cualquier desarrollador que venga de Swift o Kotlin empieza a agradecer al equipo de MAUI. Como PlatformView es el tipo nativo real, tienes acceso a toda la API de Apple o Google. Si abres la referencia de UIButton en developer.apple.com, cualquier método de esa página es alcanzable dentro de #if IOS. Nada de wrappers artificiales, nada de propiedades a medias.

La contrapartida es que tus archivos platform-specific tienen que compilar contra los SDKs nativos. Por eso .NET MAUI genera un target framework por plataforma (net10.0-ios, net10.0-android) y por eso el hot reload a veces se rompe cuando editas ficheros .iOS.cs: el compilador tiene que regenerar los bindings.

Ciclo de vida: ConnectHandler, DisconnectHandler y fugas de memoria

ConnectHandler(platformView) se ejecuta una vez, cuando la vista nativa acaba de crearse y está a punto de añadirse al árbol visual. Es el lugar donde enganchas eventos nativos, gestores de gestos, delegates de UITextField, listeners de Android, etc. DisconnectHandler(platformView) se ejecuta cuando el control se desconecta del árbol, y es donde tienes que desenganchar todo.

Este es el error número uno que veo en revisiones de código: los desarrolladores suscriben eventos en ConnectHandler pero olvidan desuscribirlos en DisconnectHandler. En Android el garbage collector es lo suficientemente agresivo para disimularlo, pero en iOS el UIViewController queda retenido en memoria hasta que Xcode Instruments te avisa. En una app con navegación intensiva eso equivale a decenas de MB perdidos por hora. Me pilló en el primer proyecto MAUI que llevé a la App Store, y desde entonces reviso este par en cada pull request.

protected override void ConnectHandler(UIButton platformView)
{
    base.ConnectHandler(platformView);
    platformView.TouchUpInside += OnClicked;
}

protected override void DisconnectHandler(UIButton platformView)
{
    platformView.TouchUpInside -= OnClicked; // imprescindible
    base.DisconnectHandler(platformView);
}

private void OnClicked(object? sender, EventArgs e) => VirtualView?.SendClicked();

Registrar handlers en MauiProgram.cs

Todo handler personalizado (o toda modificación del mapper de un handler existente) pasa por MauiProgram.cs. La API es ConfigureMauiHandlers y admite tanto registro puntual como registros por convención:

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

    builder
        .UseMauiApp<App>()
        .ConfigureMauiHandlers(handlers =>
        {
            // Handler completamente nuevo
            handlers.AddHandler<PulsingButton, PulsingButtonHandler>();

            // Reemplazar handler integrado
            handlers.AddHandler(typeof(Entry), typeof(MiEntryHandlerPersonalizado));
        });

    // Modificar el mapper del handler integrado sin subclasear
    Microsoft.Maui.Handlers.LabelHandler.Mapper.AppendToMapping("SombraTexto", (h, v) =>
    {
#if IOS
        h.PlatformView.Layer.ShadowOpacity = 0.4f;
        h.PlatformView.Layer.ShadowRadius = 2f;
#elif ANDROID
        h.PlatformView.SetShadowLayer(2f, 1f, 1f, Android.Graphics.Color.Gray);
#endif
    });

    return builder.Build();
}

La sección migrar de Xamarin.Forms a .NET MAUI incluye un mapa completo de conversión: dónde antes tenías [assembly: ExportRenderer], ahora usarás handlers.AddHandler; dónde tenías OnElementChanged, ahora usarás ConnectHandler. La regla es directa: todo lo que estaba disperso en atributos globales queda centralizado en MauiProgram.cs.

Errores comunes al usar handlers personalizados

Sinceramente, tras meses depurando handlers en varias apps, estos son los cinco fallos que aparecen una y otra vez, en el orden en que muerden:

  1. Olvidar DisconnectHandler. Ya lo mencioné, pero merece repetirse: si suscribes eventos, desuscribe. Xcode Instruments > Leaks lo detecta en un minuto.
  2. Suponer que VirtualView nunca es null. Durante la desconexión puede serlo. Usa siempre el operador ?.: VirtualView?.SendClicked().
  3. Modificar el mapper varias veces por hot-reload. Si haces AppendToMapping en tiempo de ejecución (no en MauiProgram), cada hot-reload añade una entrada. La aplicación acaba ejecutando la misma acción diez veces por control.
  4. Confundir Handler con Handler.PlatformView. Cuando accedes fuera del handler, con myButton.Handler.PlatformView, tienes que castear al tipo nativo esperado en cada plataforma. Un cast fallido tira una InvalidCastException que solo se ve en Release.
  5. Registrar el handler después de Build(). Los handlers registrados fuera de ConfigureMauiHandlers no se aplican a controles ya instanciados. La regla es: todo antes de builder.Build().

Para depurar el ciclo de vida, activa el logging interno de MAUI con builder.Logging.AddDebug() y filtra por Microsoft.Maui.Handlers. Verás cada ConnectHandler, DisconnectHandler y cada llamada al mapper. Es más barato que abrir Instruments para un caso puntual.

Hay un sexto error que solo aparece en apps grandes: reordenar el diccionario del mapper. Cuando dos librerías (por ejemplo, un tema visual de terceros y tu propio código) llaman AppendToMapping sobre la misma clave, gana la última en registrarse. Si dependes de ese orden, documéntalo o convierte tu registro en un ModifyMapping explícito para dejar clara la intención. En una app que auditamos el año pasado, esa ambigüedad provocaba que el color del cursor cambiara al reordenar los paquetes NuGet, un bug imposible de rastrear sin este contexto.

Por último, si estás usando SQLite para almacenamiento local y guardas configuraciones visuales que aplicas mediante handlers, cachea el resultado en memoria. Cada AppendToMapping es rápido, pero ejecutarlo cientos de veces al pintar una lista virtualizada acaba notándose en dispositivos gama media.

Preguntas frecuentes

¿Puedo tener un handler distinto en iOS y otro en Android para el mismo control?

No es necesario. Un único handler partial con archivos .iOS.cs y .Android.cs resuelve cada método CreatePlatformView en la plataforma correcta. El registro es único en ConfigureMauiHandlers.

¿Los handlers funcionan en .NET MAUI Blazor Hybrid?

Sí, los controles nativos que aparecen fuera del BlazorWebView usan handlers estándar. Dentro del BlazorWebView, en cambio, los componentes se renderizan como HTML/CSS y no pasan por el sistema de handlers.

¿Es más rápido un handler que un renderer de Xamarin.Forms?

Sí. El equipo de MAUI publicó benchmarks en el repositorio de dotnet/maui mostrando entre un 20 % y un 40 % menos de tiempo en la resolución de controles, dependiendo del volumen de vistas por página. La diferencia proviene de haber eliminado la reflexión en tiempo de arranque.

¿Puedo modificar el handler de un control solo en una página, no globalmente?

Sí, aunque no es lo habitual. Puedes acceder al handler de una instancia concreta con miBoton.Handler tras el evento Loaded y modificar la PlatformView directamente. Recuerda revertir el cambio en el evento Unloaded si la vista se reutiliza.

¿Qué diferencia hay entre PropertyMapper y CommandMapper?

PropertyMapper reacciona a cambios de propiedades enlazadas (Text, Color, IsEnabled...). CommandMapper reacciona a llamadas explícitas invocadas por el propio handler cuando la vista virtual emite una acción, típicamente para métodos como Focus() o Unfocus().