Borders personalizados en .NET MAUI

Aplicar bordes personalizados es una necesidad bastante común utilizada en el diseño de aplicaciones para crear estructuras visuales concretas, resaltar elementos o poder personalizar ciertos controles específicos.

En Xamarin.Forms el control Frame ha sido ampliamente utilizado con este fin. Sin embargo, el control Frame contaba con ciertas limitaciones:

  • No poder personalizar cada esquina del borde.
  • No poder personalizar el ancho del borde.
  • No poder utilizar brushes en el propio borde.
  • Etc.

Con la Preview 9 de .NET MAUI llega un nuevo control, Border, con bastantes más posibilidades y con el objetivo de resolver todas las limitaciones que teníamos hasta ahora. ¿Lo revisamos en este artículo?.

Nuevo control Border

El nuevo control Border, similar en concepto al Frame, es un control que permite un único elemento como contenido pero que puede ser cualquier cosa. Es decir, podremos aplicar un Border desde a un sencillo Label a un Grid con un layout complejo.

<Border Stroke="Red" StrokeThickness="2">
    <Border.ShapeBorder>
        <RoundRectangle CornerRadius="12, 0, 0, 24" />
    </Border.ShapeBorder>
    <Label Text="Border" />
</Border>

El control Border cuenta con diferentes propiedades para personalizar el borde:

  • Stroke: Brush que define el color o gradiente a utilizar en el borde.
  • StrokeThickness: Grosor del borde.
  • StrokeDashArray: Se utiliza una colección de valores Double que indican el patrón Dash y los espacios que se usan para delinear la figura a utilizar en el borde.
  • StrokeDashOffset: Es un valor de tipo Double que indica la distancia a usar en el patrón Dash desde donde comienza.
  • StrokeLineJoin: Especifica como se realizará la unión entre los vértices de la figura a utilizar en el borde.
  • StrokeLineCap: Especifica como es el trazo del inicio y fin de la figura a utilizar en el borde.

Y entre las propiedades disponibles me gustaría destacar, StrokeShape. Espera un Shape que define la forma del borde. Por defecto se usa un Rectángulo pero puede ser desde un Rectángulo con bordes redondeados a cualquier figura.

Podemos pasar de un borde con forma rectangular y bordes redondeados personalizados:

<Border Stroke="Red" StrokeThickness="2">
    <Border.ShapeBorder>
        <RoundRectangle CornerRadius="12, 0, 0, 24" />
    </Border.ShapeBorder>
    <Label Text="Border" />
</Border>

A una elipse:

<Border Stroke="Red" StrokeThickness="2">
    <Border.ShapeBorder>
        <Ellipse />
    </Border.ShapeBorder>
    <Label Text="Border" />
</Border>

A continuación, veamos como funcionan estas propiedades en conjunto en un ejemplo de forma visual.

.NET MAUI Border

¿Qué te parece esta novedad en .NET MAUI?. Recuerda, puedes usar los comentarios de la entrada para dejar tu feedback.

Más información

Sombras en .NET MAUI

La sombra es una de las formas en que un usuario percibe la elevación. Este efecto de elevación transmite en qué debe centrarse el usuario. Y esta es la razón por la que los diseñadores móviles prefieren incorporar sombras en sus diseños.

Con la llegada de la Preview 9 de .NET MAUI obtenemos novedades a nivel de UI como el nuevo control Border o una nueva API para poder crear sombras. En este artículo, vamos a centrarnos en conocer la nueva API de sombras.

Sombras

Sombras

A nivel de View, es decir, en cualquier vista disponible en .NET MAUI, se pueden añadir sombras. Esto quiere decir que se pueden añadir sombras desde en imágenes a cualquier Shape y por supuesto en un Button o Label.

Contamos con una nueva propiedad llamada Shadow de tipo Shadow. Shadow es un Element que cuenta con las siguientes propiedades:

  • Radius: Es el radio del Gaussian blur usado para generar la sombra.
  • Color: El color de la sombra.
  • Offset: Offset de la sombra en relación al elemento visual donde se adjunta la misma.
  • Opacity: La opacidad de la sombra.

La forma más sencilla de añadir una sombra es:

<Label Text="Label">
    <Label.Shadow>
        <Shadow 
            Color="Blue"
            Offset="10, 20"
            Offset="0.5"
            Radius="12">
    </Label.Shadow>
</Label>

NOTA: Cada propiedad que tenemos disponible en la clase Shadow es una BindableProperty por lo tanto, podemos actualizar cada propiedad con bindings.

Podemos crear sombras como recursos compartidos y utilizar la misma sombra en diferentes elementos visuales:

<ContentPage.Resources>
    <ResourceDictionary>

        <Shadow x:Key="Shadow" Brush="Red" Offset="12, 12" Radius="12" />

    </ResourceDictionary>
</ContentPage.Resources>

<Ellipse HeightRequest="100" WidthRequest="100" Fill="Blue" Shadow="{StaticResource Shadow}" />
<Button Text="Button" BackgroundColor="Blue" Shadow="{StaticResource Shadow}" />

Sombras en .NET MAUI

¿Qué te parece esta novedad en .NET MAUI?. Recuerda, puedes usar los comentarios de la entrada para dejar tu feedback.

Más información

Convertir Triggers de Xamarin.Forms a .NET MAUI

Los Triggers permiten expresar acciones de forma declarativa en XAML que cambian la apariencia de los controles en función de eventos o cambios de propiedad. Además, los state triggers, que son un grupo especializado de triggers, definen cuándo se debe aplicar un VisualState.

Como regla general, todos los conceptos relacionados con XAML en Xamarin.Forms funcionarán sin requerir cambios en .NET MAUI.

Xamarin.Forms

<Entry 
    x:Name="Entry"
    Text=""
    Placeholder="Required field" />
<!-- Referenced below in DataTrigger-->
<Button 
    x:Name="Button"
    Text="Save"
    FontSize="Large"
    HorizontalOptions="Center">
    <Button.Triggers>
        <DataTrigger
            TargetType="Button"
            Binding="{Binding Source={x:Reference Entry},
                            Path=Text.Length}"
            Value="0">
            <Setter Property="IsEnabled" Value="False" />
        </DataTrigger>
    </Button.Triggers>
</Button>

.NET MAUI

<Entry 
    x:Name="Entry"
    Text=""
    Placeholder="Required field" />
<!-- Referenced below in DataTrigger-->
<Button 
    x:Name="Button"
    Text="Save"
    FontSize="Large"
    HorizontalOptions="Center">
    <Button.Triggers>
        <DataTrigger
            TargetType="Button"
            Binding="{Binding Source={x:Reference Entry},
                            Path=Text.Length}"
            Value="0">
            <Setter Property="IsEnabled" Value="False" />
        </DataTrigger>
    </Button.Triggers>
</Button>

Convertir Behaviors de Xamarin.Forms a .NET MAUI

Los Behaviors permiten agregar funciones a los controles de la interfaz de usuario sin tener que incluirlos en subclases. En su lugar, la función se implementa en una clase Behavior y se asocia al control como si fuera parte de este.

En .NET MAUI existe exactamente el mismo concepto, permitiendo reutilizar código de forma sencilla.

Veamos un ejemplo.

Xamarin.Forms

using Xamarin.Forms;

namespace Behaviors
{
    public class NumericValidationBehavior : Behavior
    {
        protected override void OnAttachedTo(Entry entry)
        {
            entry.TextChanged += OnEntryTextChanged;
            base.OnAttachedTo(entry);
        }

        protected override void OnDetachingFrom(Entry entry)
        {
            entry.TextChanged -= OnEntryTextChanged;
            base.OnDetachingFrom(entry);
        }

        void OnEntryTextChanged(object sender, TextChangedEventArgs args)
        {
            bool isValid = double.TryParse(args.NewTextValue, out double result);
            ((Entry)sender).TextColor = isValid ? Color.Default : Color.Red;
        }
    }
}

.NET MAUI

using Microsoft.Maui.Controls;
using Microsoft.Maui.Graphics;

namespace Behaviors
{
    public class NumericValidationBehavior : Behavior
    {
        protected override void OnAttachedTo(Entry entry)
        {
            entry.TextChanged += OnEntryTextChanged;
            base.OnAttachedTo(entry);
        }

        protected override void OnDetachingFrom(Entry entry)
        {
            entry.TextChanged -= OnEntryTextChanged;
            base.OnDetachingFrom(entry);
        }

        void OnEntryTextChanged(object sender, TextChangedEventArgs args)
        {
            bool isValid = double.TryParse(args.NewTextValue, out double result);
            ((Entry)sender).TextColor = isValid ? Colors.Black : Colors.Red;
        }
    }
}

¿Cuál es la diferencia?. El código es exactamente el mismo excepto por un detalle, namespaces.

los namespace de Xamarin.Forms cambian por el n amespace Microsoft.Maui.Controls.

Por otro lado, todos los tipos básicos como: Color, Rectangle o Point ahora estan dispoinles en Microsoft.Maui.Graphics. Por esa razón, y porque usamos colores en este Behavior, también incluimos como cambio el nuevo namespace de Graphics.

Convertir Converters de Xamarin.Forms a .NET MAUI

Los enlaces de datos generalmente transfieren datos de una propiedad de origen a una propiedad de destino y, en algunos casos, de la propiedad de destino a la propiedad de origen. Esta transferencia es sencilla cuando las propiedades de origen y destino son del mismo tipo, o cuando un tipo se puede convertir al otro tipo mediante una conversión implícita. Cuando ese no es el caso, se debe realizar un Converter.

Suponga que desea definir un enlace de datos donde la propiedad de origen es de tipo int pero la propiedad de destino es bool. Desea que este enlace de datos produzca un valor falso cuando la fuente entera sea igual a 0 y verdadero en caso contrario.

Puede hacer esto con una clase que implemente la interfaz IValueConverter.

Al igual que otros conceptos de Xamarin.Forms, los Converters se pueden reutilizar en .NET MAUI sin requerir cambios de código.

Veamos un ejemplo.

Xamarin.Forms

using System;
using System.Globalization;
using Xamarin.Forms;

namespace Converters
{
    public class IntToBoolConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            return (int)value != 0;
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            return (bool)value ? 1 : 0;
        }
    }
}

.NET MAUI

using Microsoft.Maui.Controls;
using System;
using System.Globalization;

namespace Converters
{
    public class IntToBoolConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            return (int)value != 0;
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            return (bool)value ? 1 : 0;
        }
    }
}

¿Cuál es la diferencia?. El código es exactamente el mismo excepto por un detalle, los espacios de nombres.

El espacio de nombres de Xamarin.Forms cambia al espacio de nombres Microsoft.Maui.Controls.

Convertir un Custom Renderer de Xamarin.Forms a un Custom Handler de .NET MAUI

Las interfaces de usuario de Xamarin.Forms y .NET MAUI utilizan los controles nativos de la plataforma de destino, lo que permite que las aplicaciones conserven la apariencia adecuada para cada plataforma. El uso de Custom Renderers en Xamarin.Forms permite a los desarrolladores personalizar la apariencia y el comportamiento de los controles en cada plataforma.

Por diferentes motivos, entre los que podemos destacar mejoras en el rendimiento, más posibilidades de extensibilidad, en .NET MAUI tenemos el concepto de Custom Handler. Es similar al concepto de Custom Renderer, pero diferente en diferentes puntos.

¡Veamos paso a paso cómo convertir un Renderer a un Handler!. Para centrarnos en los conceptos clave de Renderers y Handlers, vamos a crear una Entry personalizado, un control bien conocido. Por otro lado, vamos a usar Android simplemente porque tanto desde Windows como desde macOS puedes lanzar las demos asociadas.

Custom Renderer

Cada control de Xamarin.Forms tiene un Renderer adjunto para cada plataforma que crea una instancia de un control nativo. El proceso para crear un Renderer es el siguiente:

  • Crea un control personalizado de Xamarin.Forms.
  • Consume el control personalizado de Xamarin.Forms.
  • Crea el Custom Renderer para el control en cada plataforma.

Creación del control Entry personalizado

Se puede crear un control personalizado creando una clase que herede de la clase View:

public class CustomEntry : View
{

}

El control CustomEntry se crea en el proyecto de la librería .NET Standard y es simplemente un control para capturar texto. Para personalizar la apariencia y el comportamiento del control podemos agregar BindableProperties y eventos.

public static readonly BindableProperty TextProperty =
    BindableProperty.Create(nameof(Text), typeof(string), typeof(Entry), string.Empty);


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

Consumir el control personalizado

Se puede hacer referencia al control CustomEntry en XAML en el proyecto de la librería .NET Standard declarando un espacio de nombres y usando el prefijo del espacio de nombres en el elemento de control.


    ...
    
    ...

Creación del Custom Renderer en cada plataforma

El proceso para crear la clase del Custom Renderer es el siguiente:

  1. Crea una clase que herede de ViewRenderer que representa el control nativo.
  2. Sobreescribe el método OnElementChanged que representa el control nativo y la lógica para personalizar el control. Este método se llama cuando se crea el control de Xamarin.Forms correspondiente.
  3. Sobreescribe el método OnElementPropertyChanged que responde a cualquier cambio de una BindableProperty.
  4. Agrega el atributo ExportRenderer a la clase del Custom Renderer para especificar que se usará para representar el control Xamarin.Forms. Este atributo se usa para registrar el representador personalizado con Xamarin.Forms (usando Assembly Scanning).

La clase ViewRenderer expone el método OnElementChanged, al que se llama cuando se crea el control Xamarin.Forms para representar el control nativo correspondiente. Este método toma un parámetro ElementChangedEventArgs que contiene las propiedades OldElement y NewElement. Estas propiedades representan el elemento Xamarin.Forms al que se adjuntó el representador y el elemento Xamarin.Forms al que está adjunto el representador, respectivamente. En la aplicación de ejemplo, la propiedad OldElement será nula y la propiedad NewElement contendrá una referencia al control CustomEntry.

El método OnElementChanged en la clase CustomEntryRenderer es el lugar para realizar la personalización del control nativo. Se puede acceder a una referencia del control nativo que se usa en la plataforma a través de la propiedad Control. Además, se puede obtener una referencia al control Xamarin.Forms que se está representando a través de la propiedad Element.

protected override void OnElementChanged(ElementChangedEventArgs e)
{
    base.OnElementChanged(e);

    if (e.OldElement == null)
    {
        EditText editText = new EditText(Context);

        _defaultTextColors = editText.TextColors;
        _defaultPlaceholderColors = editText.HintTextColors;

        SetNativeControl(editText);
    }

    UpdateText();
    UpdateTextColor();
    UpdatePlaceholder();
    UpdatePlaceholderColor();
    UpdateCharacterSpacing();
    UpdateHorizontalTextAlignment();
    UpdateVerticalTextAlignment();
}

La invalidación OnElementPropertyChanged responde a los cambios de BindableProperties enlazadas al control Xamarin.Forms.

protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
    if (e.PropertyName == CustomEntry.TextProperty.PropertyName)
        UpdateText();
    else if (e.PropertyName == CustomEntry.TextColorProperty.PropertyName)
        UpdateTextColor();
    if (e.PropertyName == CustomEntry.PlaceholderProperty.PropertyName)
        UpdatePlaceholder();
    else if (e.PropertyName == Entry.PlaceholderColorProperty.PropertyName)
        UpdatePlaceholderColor();
    else if (e.PropertyName == Entry.CharacterSpacingProperty.PropertyName)
        UpdateCharacterSpacing();
    else if (e.PropertyName == Entry.HorizontalTextAlignmentProperty.PropertyName)
        UpdateHorizontalTextAlignment();
    else if (e.PropertyName == Entry.VerticalTextAlignmentProperty.PropertyName)
        UpdateVerticalTextAlignment();

    base.OnElementPropertyChanged(sender, e);
}

Nada realmente nuevo hasta ahora, solo una pequeña revisión sobre cómo trabajar con Custom Renderers en Xamarin.Forms, ¿vamos a .NET MAUI?

Pero primero, ¿por qué cambiar de la arqquitectura de Renderers a la de Handlers en .NET MAUI?

Para comprender las motivaciones que impulsan el cambio, incluso conociendo las implicaciones en la cantidad de cambios necesarios, necesitamos saber qué es lo que está mal o es mejorable en los Renderers.

¿Recuerdas el atributo ExportRenderer que usa para registrar el Renderer? Esto le dice a Xamarin.Forms que en el arranque, haciendo uso del escaneo de ensamblados, debe buscar todas las bibliotecas referenciadas y usando este atributo, y si lo encuentra, registra el Renderer. Es fácil de usar, pero… el escaneo del ensamblados es lento y penaliza el inicio.

El método OnElementChanged generalmente causa confusión. ¿Cuándo usar OldElement , y NewElement?, ¿cuándo creo valores por defecto o me suscribo a eventos?. Causar confusión es un problema, pero es un problema aún mayor si no tener una forma fácil de suscribirse/desuscribirse hace que a veces no se anule la suscripción (por ejemplo) y… penaliza el rendimiento.

Todos esos métodos privados para actualizar las propiedades del control nativo son un gran problema. Es posible que debas hacer un pequeño cambio y debido a la falta de acceso (nuevamente, ¡métodos privados aquí y allá!), terminas creando un Custom Renderer más grande de lo necesario, etc.

Resolver estos problemas, y otros menores, es el objetivo fundamental de los Handlers.

Custom Handlers

El proceso para crear la clase del Custom Handler es el siguiente:

  1. Crea una clase que implementa la clase ViewHandler que representa el control nativo.
  2. Sobrecarga el método CreateNativeView que representa el control nativo.
  3. Crea el diccionario Mapper que responda a los cambios de propiedad.
  4. Registra el controlador usando el método AddHandler en la clase Startup.

Creación del control Entry personalizado

Los Handlers utilizan interfaces que derivan de la interfaz IView. Esto evita que el control multiplataforma tenga que hacer referencia a su Handler y que el Handler tenga que hacer referencia al control multiplataforma. El Mapper se encarga de mapear las propiedades multiplataforma a lo nativo en cada plataforma.

De esta forma comenzamos a crear la interfaz que define nuestro control:

public interface ICustomEntry : IView
{
    public string Text { get; }
    public Color TextColor { get; }
    public string Placeholder { get; }
    public Color PlaceholderColor { get; }
    public double CharacterSpacing { get; }
    public TextAlignment HorizontalTextAlignment { get; }
    public TextAlignment VerticalTextAlignment { get; }

    void Completed();
}

Se puede crear un control personalizado heredando de la clase View e implementando la interfaz del control:

public class CustomEntry : View, ICustomEntry
{

}

Crear el Custom Handler cada plataforma

Cree una subclase de la clase ViewHandler que representa el control nativo.

public partial class CustomEntryHandler : ViewHandler
{

}

Parece un cambio trivial, pasamos de heredar de ViewRenderer a ViewHandler, ¡pero es mucho más!.

ViewRenderer en Xamarin.Forms crea un elemento padre, en el caso de Android un ViewGroup, que se usaba para tareas de posicionamiento auxiliares. ViewHandler NO crea ningún elemento padre que ayudea a reducir la jerarquía visual y, por lo tanto, mejora el rendimiento.

Heredando de ViewHandler, tenemos que implementar el método CreateNativeView.

protected override EditText CreateNativeView()
{
    return new EditText(Context);
}

¿Recuerdas que anteriormente revisamos cómo se usó OnElementChanged en Xamarin.Forms?. En este método creamos el control nativo, inicializamos valores predeterminados, nos suscribimos a eventos, etc. Sin embargo, requiere una gran diversidad de conocimientos: qué es OldElement y NewElement, etc.

.NET MAUI simplifica y distribuye todo lo que hicimos anteriormente en el método OnElementChanged en diferentes métodos de una manera más sencilla.

Creamos el control nativo en el método CreateNativeView. Por otro lado, tenemos otros métodos como ConnectHandler y DisconnectHandler.

protected override void ConnectHandler(EditText nativeView)
{
    _defaultTextColors = nativeView.TextColors;
    _defaultPlaceholderColors = nativeView.HintTextColors;

    _watcher.Handler = this;
    nativeView.AddTextChangedListener(_watcher);

    base.ConnectHandler(nativeView);
}

protected override void DisconnectHandler(EditText nativeView)
{
    nativeView.RemoveTextChangedListener(_watcher);
    _watcher.Handler = null;

    base.DisconnectHandler(nativeView);
}

ConnectHandler es el lugar ideal para inicializar, suscribir eventos, etc. y de la misma manera podemos eliminar, cancelar la suscripción de eventos, etc. en DisconnectHandler.

El Mapper

** Mapper ** es un nuevo concepto introducido por los Handlers en .NET MAUI. No es más que un diccionario con las propiedades (y acciones) definidas en la interfaz de nuestro control (recuerda, usamos interfaces en el Handler). Reemplaza todo lo que se hacía en el método OnElementPropertyChanged en Xamarin.Forms.

public static PropertyMapper CustomEntryMapper = new PropertyMapper(ViewHandler.ViewMapper)
{
    [nameof(ICustomEntry.Text)] = MapText,
    [nameof(ICustomEntry.TextColor)] = MapTextColor,
    [nameof(ICustomEntry.Placeholder)] = MapPlaceholder,
    [nameof(ICustomEntry.PlaceholderColor)] = MapPlaceholderColor,
    [nameof(ICustomEntry.CharacterSpacing)] = MapCharacterSpacing,
    [nameof(ICustomEntry.HorizontalLayoutAlignment)] = MapHorizontalLayoutAlignment,
    [nameof(ICustomEntry.VerticalLayoutAlignment)] = MapVerticalLayoutAlignment
};

Mapper mapea propiedades a métodos estáticos.

public static void MapText(CustomEntryHandler handler, ICustomEntry entry)
{
    handler.NativeView?.UpdateText(entry);
}

El Mapper, además de simplificar la gestión de cambios de propiedad (notifica las propiedades al inicializar el control y también, cada vez que cambia una propiedad) nos permite más opciones de extensibilidad.

Por ejemplo:

using Microsoft.Maui; using Microsoft.Maui.Controls;

public partial class App : Application
{
    public App()
    {
        InitializeComponent();

#if __ANDROID__
        CustomEntryMapper[nameof(ICustomEntry.Text)] = (handler, view) =&gt;
        {
            (handler.NativeView as Android.Widget.EditText).Text = view.text + "custom";
        };
#endif
    }
}

Registrar el handler

A diferencia de Xamarin.Forms que usa el atributo ExportRenderer que a su vez hace uso del escaneo de ensamblado, en .NET MAUI el registro del handler es ligeramente diferente.

appBuilder
    .UseMauiApp()
    .ConfigureMauiHandlers(handlers =&gt;
    {
#if __ANDROID__
        handlers.AddHandler(typeof(CustomEntry), typeof(CustomEntryHandler));
#endif
    });

Hacemos uso de AppHostBuilder y el método AddHandler para registrar el Handler. Al final requiere, como en Xamarin.Forms, una línea para indicar que queremos dar de alta el Handler, pero se evita el uso de Assembly Scanning, que es lento y costoso, penalizando el inicio.

.NET MAUI Compatibility: Reutilizar tus Renderers sin cambios!

¿Qué es el paquete de Compatibilidad?

Para hacer transición de Xamarin.Forms a .NET MAUI lo más fluida posible, se agrega el paquete Compatability que agrega la funcionalidad de Xamarin.Forms que permite reutilizar código como Custom Renderers sin necesidad de realizar cambios.

¿Custom Renderers de Xamarin.Forms?

Las interfaces de usuario de Xamarin.Forms se crean mediante controles nativos de la plataforma de destino, lo que permite que las aplicaciones de Xamarin.Forms conserven la apariencia adecuada para cada plataforma. Los Custom Renderers permiten a los desarrolladores utilizar este proceso para personalizar la apariencia y el comportamiento de los controles de Xamarin.Forms en cada plataforma.

Veamos un ejemplo. Vamos a crear un Entry personalizada.

using Xamarin.Forms;

namespace Renderers
{
    public class CustomEntry : Entry
    {

    }
}

La implementación en Android

using Android.Content;
using Renderers;
using Renderers.Droid.Renderers;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;

[assembly: ExportRenderer(typeof(CustomEntry), typeof(CustomEntryRenderer))]
namespace Renderers.Droid.Renderers
{
    public class CustomEntryRenderer : EntryRenderer
    {
        public CustomEntryRenderer(Context context) : base(context)
        {
        }

        protected override void OnElementChanged(ElementChangedEventArgs<Entry> e)
        {
            base.OnElementChanged(e);

            if (Control != null)
            {
                Control.SetBackgroundColor(global::Android.Graphics.Color.LightGreen);
            }
        }
    }
}

Como podemos ver, solo estamos modificando el color de fondo del control nativo. Algo realmente simple, pero suficiente para el propósito de este documento, para aprender a reutilizar Renderers sin cambiar el código de los mismos.

Reutilizar el Renderer

El Renderer en .NET MAUI usa exactamente el mismo código:

using Android.Content;
using Compatibility;
using Compatibility.Droid.Renderers;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Controls.Compatibility;
using Microsoft.Maui.Controls.Compatibility.Platform.Android;

[assembly: ExportRenderer(typeof(CustomEntry), typeof(CustomEntryRenderer))]
namespace Compatibility.Droid.Renderers
{
    public class CustomEntryRenderer : EntryRenderer
    {
        public CustomEntryRenderer(Context context) : base(context)
        {
        }

        protected override void OnElementChanged(ElementChangedEventArgs<Entry> e)
        {
            base.OnElementChanged(e);

            if (Control != null)
            {
                Control.SetBackgroundColor(global::Android.Graphics.Color.LightGreen);
            }
        }
    }
}

Los únicos cambios son reemplazar algunos espacios de nombres para usar Microsoft.Maui.Controls.Compatibility.

¿Y ya está todo listo?

No exactamente, en lugar de usar Assembly Scanning, .NET MAUI usa la clase Startup para realizar tareas como registrar Handlers o Renderers.

public class Startup : IStartup
{
    public void Configure(IAppHostBuilder appBuilder)
    {
        appBuilder
            .UseMauiApp<App>()
            .ConfigureMauiHandlers(handlers =>
            {
#if __ANDROID__
                handlers.AddCompatibilityRenderer(typeof(CustomEntry), typeof(Droid.Renderers.CustomEntryRenderer));
#endif
            });
    }
}

.NET MAUI Preview 5

Vaya, no hace tanto del //Build donde se lanzo la Preview 4 y ya tenemos disponible la nueva Preview 5. Se continua con progreso release a release en todas las áreas (implementaciones en el framework, novedades en el framework, proyecto único, tooling, etc). En esta versión tenemos novedades jugosas cómo la llegada de Shell, animaciones o el proyecto único.

En este artículo, vamos a hacer un repaso a todas las novedades principales.

Novedades en controles

Se sigue el proceso de creación de Handlers (recuerda, es la nueva arquitectura para la creación de los controles nativos desde la abstracción) con más controles 100% completados (ActivityIndicator, CheckBox, Image, Slider, Stepper) y varios más completos.

Más y más controles!

Disponible transformaciones y animaciones

Se ha añadido implementación para todas las propiedades que permiten aplicar transformaciones a un control (rotar, escalar, etc). De igual forma, se añade soporte a animaciones en controles de .NET MAUI.

Animaciones!

Tanto el aplicar transformaciones, como el uso de animaciones mantiene la misma API de Xamarin.Forms:

view.FadeTo(1, 800);

Así que, si conocías la API o tenías código relacionado con animaciones, podrás portar código así como reutilizar tanto código como conocimientos en .NET MAUI.

Novedades en Proyecto único

Si habías probado el proyecto único, verías que todo estaba unificado en un único proyecto para Android, iOS y macOS pero teníamos otros dos proyectos para WinUI. Ahora, con la Preview 5, al realizar:

dotnet new maui

Se creará una solución con dos proyectos, uno haciendo uso de multi targeting para la mayoría de plataformas, y otro para WinUI. Esto es otro paso adelante en el concepto de proyecto único y sobretodo evitará muchas confusiones acerca de qué proyecto usar para lanzar la App en Windows haciendo uso de WinUI.

NOTA: Para poder tener soporte a esta nueva estructura de proyectos, necesitas tener instalado las extensiones de Visual Studio Project Reunion 0.8 (Preview).

Disponible en NuGet

Por primera vez no es necesario añadir unas fuentes de NuGet disponibles porque .NET MAUI ya esta disponible en nuget.org.

nuget.org

Primera documentación disponible!

En esta Preview es donde recibimos también la primera documentación oficial disponible en https://docs.microsoft.com/es-es/dotnet/maui/

Documentación

Visual Studio 2020 Preview 1

Junto con la Preview 5 de .NET 6 llega la primera Preview de Visual Studio 2022!.

Primera Preview de Visual Studio 2022

Con soporte a 64 bits, mejoras de rendimiento, mejoras en IntelliCode y bastantes más novedades, también añade soporte a desarrollo móvil.

NOTA: Si desarrollas con .NET MAUI, por ahora deshabilita XAML Hot Reload para evitar errores (en desarrollo).

Más información

.NET MAUI Preview 3

Con la Preview 3 de NET MAUI llegan más novedades como:

  • Añadidos más cambios relacionados con HostBuilder, clase Startup.
  • Nueva API para gestionar el ciclo de vida de la aplicación.
  • Añadidos mas controles (DatePicker, TimePicker, SearchBar, Stepper, etc).
  • Añadidos mas cambios en Layouts.
  • Añadidas nuevas APIs de accesibilidad.
  • Primeros cambios añadiendo soporte para Windows usando WinUI 3.

En este artículo, vamos a hacer un repaso a todo lo que incluye la Preview 3, además de repasar que nos esperar en próximas Previews.

.NET MAUI Preview 3

Startup

Las aplicaciones .NET MAUI van a utilizar una clase Startup que permitirá:

  • Incluye un método Configure para canalizar los procesos de registro de servicios, registro de handlers o personalización de la aplicación.
  • Poder crear un HostBuilder personalizado.

Por ejemplo:

public void Configure(IAppHostBuilder appBuilder)
{
    appBuilder = appBuilder
        .UseCompatibilityRenderers()
        .UseMauiApp<MyApp>();
}

Por defecto, si no quiere personalizar nada especial, o bien, quieres utilizar tu propio contenedor de dependencias, etc., podrás hacerlo.

Ciclo de vida

El ciclo de vida de una aplicación es sumamente importante. Hay muchas acciones que se deben realizar cuando la aplicación pasa a segundo plano, o bien cuando regresa de suspensión. Por lo tanto, tener un control detallado de cada paso es importante.

En .NET MAUI hay una nueva API para el ciclo de vida con el firme objetivo de cubrir todas las peticiones recibidas en Xamarin.Forms y mejorar las áreas donde las posibilidades necesitaban ser expandidas.

Por supuesto, al igual que en Xamarin.Forms podrás sobrecargar diferentes métodos para saber cuándo la aplicación pasa a segundo plano etc. Igualmente, se añaden mucho más control en el ciclo de vida de otros elementos como ventanas o Views.

Por otro lado, también se va a permitir conectar directamente con eventos nativos de cada plataforma. Veamos un ejemplo:

appBuilder
    .ConfigureLifecycleEvents(events =>
    {
        events.AddEvent<Action<string>>("CustomEventName", value => LogEvent("CustomEventName"));

#if __ANDROID__
        events.AddAndroid(android => android
            .OnActivityResult((a, b, c, d) => LogEvent(nameof(AndroidLifecycle.OnActivityResult), b.ToString()))
            .OnBackPressed((a) => LogEvent(nameof(AndroidLifecycle.OnBackPressed)))
            .OnConfigurationChanged((a, b) => LogEvent(nameof(AndroidLifecycle.OnConfigurationChanged)))
            .OnCreate((a, b) => LogEvent(nameof(AndroidLifecycle.OnCreate)))
            .OnDestroy((a) => LogEvent(nameof(AndroidLifecycle.OnDestroy)))
            .OnNewIntent((a, b) => LogEvent(nameof(AndroidLifecycle.OnNewIntent)))
            .OnPause((a) => LogEvent(nameof(AndroidLifecycle.OnPause)))
            .OnPostCreate((a, b) => LogEvent(nameof(AndroidLifecycle.OnPostCreate)))
            .OnPostResume((a) => LogEvent(nameof(AndroidLifecycle.OnPostResume)))
            .OnPressingBack((a) => LogEvent(nameof(AndroidLifecycle.OnPressingBack)) && false)
            .OnRequestPermissionsResult((a, b, c, d) => LogEvent(nameof(AndroidLifecycle.OnRequestPermissionsResult)))
            .OnRestart((a) => LogEvent(nameof(AndroidLifecycle.OnRestart)))
            .OnRestoreInstanceState((a, b) => LogEvent(nameof(AndroidLifecycle.OnRestoreInstanceState)))
            .OnResume((a) => LogEvent(nameof(AndroidLifecycle.OnResume)))
            .OnSaveInstanceState((a, b) => LogEvent(nameof(AndroidLifecycle.OnSaveInstanceState)))
            .OnStart((a) => LogEvent(nameof(AndroidLifecycle.OnStart)))
            .OnStop((a) => LogEvent(nameof(AndroidLifecycle.OnStop))));

        // Add some cool features/things
        var shouldPreventBack = 1;
        events.AddAndroid(android => android
            .OnResume(a =>
            {
                LogEvent(nameof(AndroidLifecycle.OnResume), "shortcut");
            })
            .OnPressingBack(a =>
            {
                LogEvent(nameof(AndroidLifecycle.OnPressingBack), "shortcut");

                return shouldPreventBack-- > 0;
            })
            .OnBackPressed(a => LogEvent(nameof(AndroidLifecycle.OnBackPressed), "shortcut"))
            .OnRestoreInstanceState((a, b) =>
            {
                LogEvent(nameof(AndroidLifecycle.OnRestoreInstanceState), "shortcut");

                Debug.WriteLine($"{b.GetString("test2", "fail")} == {b.GetBoolean("test", false)}");
            })
            .OnSaveInstanceState((a, b) =>
            {
                LogEvent(nameof(AndroidLifecycle.OnSaveInstanceState), "shortcut");

                b.PutBoolean("test", true);
                b.PutString("test2", "yay");
            }));
    }

Como puedes ver, recibimos información de cada evento nativo de una aplicación nativa Android. Si plugins se enlazan de esta forma con estos eventos, se notificará tanto al plugin como a tu propia subscripción. Esto hará que el uso de plugins dónde se requiere un control de ciclo de vida sea más sencillo sin necesidad de añadir código de inicialización, etc.

¿Qué te parece?

Novedades en accesibilidad

Añadir más control y mejorar la API de accesibilidad para evitar confusiones además de alinear el comportamiento en todos los casos y plataformas es una de las prioridades en unos de los apartados con tanta importancia como es la accesibilidad.

<Entry
    Text="Entry text TH"
    FontSize="14"
    SemanticProperties.Description="Description text"
    SemanticProperties.Hint="Hint text"/>

Se añade el concepto de SemanticProperties. Hablamos de una serie de propiedades que añaden información extra a Views para permitir interpretar correctamente que ocurre cuando se utiliza el lector de pantalla, o navegación por teclado.

Habrá más novedades en próximas Previews. Puedes ver mas información en la Spec.

Soporte a Windows

Con la llegada de la versión 0.5 de Project Reunion, incluimos soporte a Windows en .NET MAUI con WinUI 3:

.NET MAUI en Windows
.NET MAUI en Windows!

Hasta aquí el pequeño repaso a algunas de las novedades en la Preview 3 de .NET MAUI. En la próxima Preview se esperan mas controles, novedades a nivel de UI, Microsoft Graphics y mucho más!. Por supuesto, estaremos aquí en el blog repasando con detallada cada novedad.

Más información

.NET MAUI Check tool

.NET MAUI es un framework de desarrollo de aplicaciones multiplataforma disponible en .NET 6. Con las primeras Previews ya disponibles, es necesario descargar e instalar varios requisitos desde el propio .NET 6 a los workloads para Android, iOS o Catalyst.

¿Y si pudieras utilizar una herramienta que prepare todo el entorno e instale todo los requisitos automáticamente?.

.NET MAUI Check tool

Se trata de una herramienta de línea de comandos que valida los requisitos necesarios para poder lanzar aplicaciones .NET MAUI y permite automáticamente descargar e instalar los requisitos pendientes.

Para instalar la herramienta basta con abrir la linea de comandos y ejecutar el siguiente comando:

dotnet tool install -g Redth.Net.Maui.Check

Para lanzar la herramienta basta con ejecutar el siguiente comando:

maui-check

Otras opciones

La herramienta utiliza un archivo de manifiesto para obtener la última versión de todos los requisitos. Por defecto, se utiliza un archivo de manifiesto disponible en https://aka.ms/dotnet-maui-check-manifest, pero si quieres utilizar un manifiesto personalizado es posible:

maui-check --manifest /some/other/file

Si se utiliza la herramienta en CI, es posible ejecutarla evitando cualquier confirmación usando el argumento –non-interactive.

maui-check --non-interactive

Más información