Los comportamientos (Behaviors) en .NET MAUI permiten adjuntar funcionalidades a los controles sin necesidad de extender o modificar las clases de los propios controles anfitriones. Este enfoque ofrece una gran flexibilidad para reutilizar la lógica y desacoplar la funcionalidad de la interfaz de usuario. En MAUI, existen dos métodos principales para implementar comportamientos: los comportamientos adjuntos (Attached Behaviors) y los comportamientos integrados (Built-in Behaviors), que se basan en la clase Behavior<t></t>.
- Comportamientos Adjuntos
Los comportamientos adjuntos se implementan a través de propiedades adjuntas, que son propiedades vinculables especiales que se pueden definir en una clase pero adjuntar a cualquier objeto BindableObject. Cuando el valor de una propiedad adjunta cambia, se invoca una función de devolución de llamada propertyChanged. Esta devolución de llamada es el lugar ideal para ejecutar la lógica de negocio que añade o elimina funcionalidad al control anfitrión.
1.1. Definición de una Clase de Comportamiento Adjunto
A continuación, se define una clase para validar entradas numéricas, que cambiará el color del texto de un control Entry si el valor no es un número válido.
namespace MiApp.Comportamientos
{
public static class ValidadorNumericoAdjunto
{
// Define la propiedad adjunta que habilita o deshabilita el comportamiento.
// Cuando su valor cambia, se invoca el método OnCambioPropiedadAdjunta.
public static readonly BindableProperty HabilitarValidacionNumericaProperty =
BindableProperty.CreateAttached("HabilitarValidacionNumerica",
typeof(bool),
typeof(ValidadorNumericoAdjunto),
false,
propertyChanged: OnCambioPropiedadAdjunta);
public static bool ObtenerHabilitacionValidacion(BindableObject vista)
{
return (bool)vista.GetValue(HabilitarValidacionNumericaProperty);
}
public static void EstablecerHabilitacionValidacion(BindableObject vista, bool valor)
{
vista.SetValue(HabilitarValidacionNumericaProperty, valor);
}
// Esta devolución de llamada se ejecuta cuando el valor de la propiedad adjunta cambia.
static void OnCambioPropiedadAdjunta(BindableObject vista, object valorAntiguo, object valorNuevo)
{
if (vista is not Entry entrada) return;
bool habilitar = (bool)valorNuevo;
if (habilitar)
{
// Suscribe el manejador de eventos cuando el comportamiento está habilitado.
entrada.TextChanged += AlCambiarTextoEntrada;
}
else
{
// Desuscribe el manejador de eventos cuando el comportamiento está deshabilitado.
entrada.TextChanged -= AlCambiarTextoEntrada;
}
}
// Manejador del evento TextChanged: valida si el texto es un número.
static void AlCambiarTextoEntrada(object? remitente, TextChangedEventArgs args)
{
if (remitente is not Entry entrada) return;
double resultado;
bool esValido = double.TryParse(args.NewTextValue, out resultado);
// Cambia el color del texto según la validez de la entrada.
entrada.TextColor = esValido ? Colors.Black : Colors.Red;
}
}
}
1.2. Uso de un Comportamiento Adjunto en XAML
Para aplicar el comportamiento, simplemente se establece la propiedad adjunta en el control Entry en XAML:
<ContentPage
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="MiApp.PaginaPrincipal"
xmlns:comp="clr-namespace:MiApp.Comportamientos">
<VerticalStackLayout Padding="20" Spacing="10">
<Label Text="Validación de entrada numérica con Comportamiento Adjunto:" FontSize="Medium" />
<Entry Placeholder="Por favor, introduce un valor numérico"
Keyboard="Numeric"
comp:ValidadorNumericoAdjunto.HabilitarValidacionNumerica="True"/>
</VerticalStackLayout>
</ContentPage>
1.3. Eliminación o Deshabilitación de un Comportamiento Adjunto
Para deshabilitar o "eliminar" el comportamiento, simplemente se establece la propiedad adjunta a false. Esto activará la devolución de llamada OnCambioPropiedadAdjunta, que desuscribirá el manejador de eventos, eliminando la funcionalidad del comportamiento.
<Entry Placeholder="Entrada sin validación"
comp:ValidadorNumericoAdjunto.HabilitarValidacionNumerica="False" />
- Comportamientos Integrados (Built-in Behaviors)
Los comportamientos integrados son objetos que derivan de la clase Behavior o Behavior<t></t> y se añaden a la propiedad Behaviors de un control, que es una colección de objetos Behavior. La clase base Behavior<t></t> proporciona dos métodos clave que pueden sobrescribirse: OnAttachedTo(T bindable) y OnDetachingFrom(T bindable). Estos métodos son llamados cuando el comportamiento se añade o se elimina del control anfitrión, respectivamente, permitiendo adjuntar o limpiar la lógica necesaria.
2.1. Definición de una Clase de Comportamiento Integrado
Definamos una clase similar a la anterior, pero usando el enfoque de comportamiento integrado, derivando de Behavior<entry></entry> para especificar que este comportamiento está diseñado para controles Entry.
namespace MiApp.Comportamientos
{
public class ValidadorNumericoBasico : Behavior<Entry>
{
// Se llama cuando el comportamiento se adjunta a un Entry.
protected override void OnAttachedTo(Entry entrada)
{
base.OnAttachedTo(entrada);
// Suscribe el manejador de eventos del Entry.
entrada.TextChanged += AlCambiarTextoEntrada;
}
// Se llama cuando el comportamiento se desadjunta de un Entry.
protected override void OnDetachingFrom(Entry entrada)
{
base.OnDetachingFrom(entrada);
// Desuscribe el manejador de eventos del Entry.
entrada.TextChanged -= AlCambiarTextoEntrada;
}
// Manejador del evento TextChanged: valida si el texto es un número.
private void AlCambiarTextoEntrada(object? remitente, TextChangedEventArgs args)
{
if (remitente is not Entry entrada) return;
double resultado;
bool esValido = double.TryParse(args.NewTextValue, out resultado);
// Cambia el color del texto según la validez de la entrada.
entrada.TextColor = esValido ? Colors.Black : Colors.Red;
}
}
}
2.2. Uso de un Comportamiento Integrado en XAML
Para utilizar este comportamiento, se añade una instancia de ValidadorNumericoBasico a la colección Behaviors del control Entry:
<ContentPage
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="MiApp.PaginaPrincipal"
xmlns:comp="clr-namespace:MiApp.Comportamientos">
<VerticalStackLayout Padding="20" Spacing="10">
<Label Text="Validación de entrada numérica con Comportamiento Integrado:" FontSize="Medium" />
<Entry Placeholder="Por favor, introduce un valor numérico" Keyboard="Numeric">
<Entry.Behaviors>
<comp:ValidadorNumericoBasico/>
</Entry.Behaviors>
</Entry>
</VerticalStackLayout>
</ContentPage>
2.3. Eliminación de un Comportamiento Integrado
Dado que la propiedad Behaviors es una colección, los comportamientos integrados se pueden añadir o eliminar dinámicamente en tiempo de ejecución desde el código subyacente (code-behind), utilizando métodos como Add, Remove o Clear en la colección Behaviors del control.
- Caso de Estudio: EventToCommandBehavior
En el patrón de diseño MVVM, los comandos son fundamentales para desacoplar la lógica de negocio de la interfaz de usuario. Sin embargo, no todos los controles de UI exponen una propiedad Command para sus eventos. Por ejemplo, un Button tiene un Command para su evento Clicked, pero otros eventos o controles carecen de esta funcionalidad directa.
Aquí es donde un comportamiento como EventToCommandBehavior resulta invaluable. Este comportamiento actúa como un puente, permitiendo que un evento específico de un control dispare un comando definido en el ViewModel, llevando consigo parámetros del evento si es necesario.
3.1. Uso del EventToCommandBehavior
La biblioteca CommunityToolkit.Maui ofrece un EventToCommandBehavior listo para usar. Primero, necesitas instalar el paquete NuGet:
Install-Package CommunityToolkit.Maui
Luego, puedes utilizarlo directamente en tu XAML:
<ContentPage
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="MiApp.PaginaPrincipal"
xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit">
<VerticalStackLayout Padding="20" Spacing="10">
<Button Text="Haga clic para ejecutar comando">
<Button.Behaviors>
<toolkit:EventToCommandBehavior
EventName="Clicked"
Command="{Binding MiComandoPersonalizado}"
CommandParameter="{Binding Source={RelativeSource Self}, Path=Text}" />
</Button.Behaviors>
</Button>
</VerticalStackLayout>
</ContentPage>
3.2. Análisis del Código Fuente de EventToCommandBehavior
Comprender cómo funciona EventToCommandBehavior internamente revela la ingeniosidad detrás de los comportamientos y la reflexión de .NET. A continuación, se presenta un fragmento simplificado del código con comentarios para ilustrar su mecánica:
public class EventToCommandBehavior : BaseBehavior<VisualElement>
{
// Campos privados para almacenar información de reflexión y delegados.
private readonly MethodInfo _metodoManejadorEvento;
private EventInfo? _informacionEvento;
private Delegate? _delegadoManejadorEvento;
public EventToCommandBehavior()
{
// Se inicializa _metodoManejadorEvento para referenciar el método OnTriggerHandled.
// Este método será invocado cuando ocurra el evento del control.
_metodoManejadorEvento = typeof(EventToCommandBehavior)
.GetTypeInfo()
.GetDeclaredMethod(nameof(OnTriggerHandled))
?? throw new InvalidOperationException($"No se encontró el método {nameof(OnTriggerHandled)}");
}
// Propiedades vinculables para configurar el comportamiento desde XAML.
public static readonly BindableProperty EventNameProperty =
BindableProperty.Create(nameof(EventName), typeof(string), typeof(EventToCommandBehavior), propertyChanged: OnEventNamePropertyChanged);
public string? EventName
{
get => (string?)GetValue(EventNameProperty);
set => SetValue(EventNameProperty, value);
}
public static readonly BindableProperty CommandProperty =
BindableProperty.Create(nameof(Command), typeof(ICommand), typeof(EventToCommandBehavior));
public ICommand? Command
{
get => (ICommand?)GetValue(CommandProperty);
set => SetValue(CommandProperty, value);
}
public static readonly BindableProperty CommandParameterProperty = /* ... definición ... */;
public object? CommandParameter { get; set; } // Simplificado para el ejemplo
public static readonly BindableProperty EventArgsConverterProperty = /* ... definición ... */;
public IValueConverter? EventArgsConverter { get; set; } // Simplificado para el ejemplo
// Métodos OnAttachedTo y OnDetachingFrom de la clase base Behavior<t>.
// Gestionan la suscripción y desuscripción del evento.
protected override void OnAttachedTo(VisualElement bindable)
{
base.OnAttachedTo(bindable);
RegistrarEvento(); // Suscribe el evento cuando el comportamiento se adjunta.
}
protected override void OnDetachingFrom(VisualElement bindable)
{
DesregistrarEvento(); // Desuscribe el evento cuando el comportamiento se desadjunta.
base.OnDetachingFrom(bindable);
}
// Se llama cuando la propiedad EventName cambia, lo que podría requerir
// volver a registrar el evento con un nuevo nombre.
private static void OnEventNamePropertyChanged(BindableObject bindable, object oldValue, object newValue)
=> ((EventToCommandBehavior)bindable).RegistrarEvento();
// Lógica para registrar el evento dinámicamente.
private void RegistrarEvento()
{
DesregistrarEvento(); // Asegura que no haya suscripciones duplicadas.
if (View is null || string.IsNullOrWhiteSpace(EventName))
{
return;
}
// Utiliza reflexión para encontrar el evento por su nombre en el control anfitrión.
_informacionEvento = View.GetType()?.GetRuntimeEvent(EventName)
?? throw new ArgumentException($"EventToCommandBehavior: No se pudo resolver el evento '{EventName}'.", nameof(EventName));
// Crea un delegado que apunta a OnTriggerHandled y tiene la firma del evento.
_delegadoManejadorEvento = _metodoManejadorEvento.CreateDelegate(_informacionEvento.EventHandlerType!, this)
?? throw new ArgumentException($"EventToCommandBehavior: No se pudo crear el manejador de eventos para '{EventName}'.", nameof(EventName));
// Suscribe el manejador de eventos al evento del control anfitrión.
_informacionEvento.AddEventHandler(View, _delegadoManejadorEvento);
}
// Lógica para desregistrar el evento.
private void DesregistrarEvento()
{
if (_informacionEvento is not null && _delegadoManejadorEvento is not null)
{
_informacionEvento.RemoveEventHandler(View, _delegadoManejadorEvento);
}
_informacionEvento = null;
_delegadoManejadorEvento = null;
}
// Método invocado cuando el evento del control anfitrión se dispara.
// Este método es el que realmente ejecuta el comando.
[Microsoft.Maui.Controls.Internals.Preserve(Conditional = true)]
protected virtual void OnTriggerHandled(object? sender = null, object? eventArgs = null)
{
// Determina el parámetro del comando, usando el CommandParameter directo
// o convirtiendo los argumentos del evento si hay un EventArgsConverter.
var parametro = CommandParameter
?? EventArgsConverter?.Convert(eventArgs, typeof(object), null, null);
var comando = Command;
// Si el comando está disponible y puede ejecutarse, lo ejecuta.
if (comando?.CanExecute(parametro) ?? false)
{
comando.Execute(parametro);
}
}
}
</t>