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

[XCT] Sombras en Xamarin.Forms

Las sombras consisten en un efecto visual que ayuda al cerebro humano a diferenciar ciertos elementos de la interfaz de usuario. Y esta es una de las razones por las que los diseñadores añaden sombras en sus diseños para aplicaciones móviles.

En este artículo vamos a aprender como añadir sombras, así como personalizar las mismas con diferentes opciones relacionadas con la dirección, tamaño o color de la sombra utilizando el Xamarin Community Toolkit.

Añadiendo Shadows usando el XCT

En el XCT se ha añadido un efecto sumamente útil y sencillo de utilizar que permite añadir sombras a cualquier View que añadimos a la UI.

El efecto ShadowEffect cuenta con una serie de propiedades para permitir personalizar las sombras:

  • Color: Especifica el color usado en la sombra.
  • OffsetX: Este valor nos permite definir el desplazamiento de la sombra. OffsetX especifica la distancia horizontal. Los valores negativos colocan las sombra a la izquierda de el elemento.
  • OffsetY: Este valor nos permite definir el desplazamiento de la sombra. OffsetY especifica la distancia vertical. Los valores negativos colocan las sombra en la parte superior de el elemento.
  • Opacity: Este valor permite especificar la opacidad de la sombra.
  • Radius: Cuando mayor sea este valor, mayor sera la difuminación, por consecuencia la sombra se vuelve más grande y ligera.

Utilizar sombras es sencillo, tras añadir la referencia al paquete NuGet del Xamarin Community Toolkit 1.1 o superior, añadimos el namespace necesario para trabajar con el XCT:

xmlns:xct="http://xamarin.com/schemas/2020/toolkit"

Y utilizando el efecto ShadowEffect con las propiedades que hemos visto previamente:

<Label 
    Text="Label With Shifted Red Shadow"
    xct:ShadowEffect.Color="Red"
    xct:ShadowEffect.OffsetX="10"
    xct:ShadowEffect.OffsetY="10" />

El resultado:

Sencillo, ¿verdad?. Recuerda, puedes usar los comentarios de la entrada para añadir tus dudas o preguntas o bien, si has usado este efecto y quieres compartir con todos tu resultado!.

Más información

Contribuir a .NET MAUI

.NET MAUI es un proyecto Open Source desde su inicio y desde etapas tempranas, se permite colaborar o contribuir de diferentes formas (eso sí, con unas bases a seguir bastante definidas).

.NET MAUI

Tras ver ya algunas contribuciones de la comunidad, y recibir un correo preguntando como se puede ayudar, en este artículo, voy a hacer un repaso en las diferentes formas en las que se puede contribuir en .NET MAUI, así como compilar el proyecto, etc.

¿Te interesa?. Vamos a por ello!.

Consejos antes de comenzar

Antes de empezar, colaborar en proyectos Open Source tiene partes positivas como:

  • Vas a ayudar a otros!.
  • Es una forma divertida de aprender.
  • Etc.

Sin embargo, si nunca has colaborado antes probablemente tengas algunas dudas. La primera de ellas será como hacerlo. Para eso espero que este artículo sea de ayuda. El segundo de los posibles problemas esta en el conocido “síndrome del impostor”. Dudas si lo que haces es correcto,  etc. En este punto, no te preocupes!. Si hay algo que se pueda mejorar, lo verás en el feedback. Es parte de la “gracia” de colaborar así, aprenderás posiblemente cosas nuevas.

Tan solo recuerda:

  • No tengas dudas. Aprenderás cosas nuevas como yo y todos lo hacemos a diario.
  • Puedes preguntar al crear la PR lo que necesites!.

Formas de contribuir

Lo primero de todo, contribuir tiene varias definiciones aunque en este caso:

“se trata de ayudar y concurrir con otros al logro de un cierto fin”

Se puede ayudar de muchas formas!. No tienes porque implementar la espectacular funcionalidad X para poder contribuir.

Vamos a repasar las diferentes opciones disponibles:

  • Probar las Previews.
  • Dar tu feedback en las Specs de nueva funcionalidad.
  • Correcciones en la wiki.
  • Tomar una de las issues disponibles (implementar una propiedad en un Handler).

Preparar el entorno

Antes de comenzar a trabajar con .NET MAUI, para trabajar con .NET 6, debemos instalar .NET 6 y otros requisitos.

Comenzamos instalando la Preview de .NET 6:

Para trabajar con .NET para Android y iOS necesitarás instalar también los workloads:

Android:

iOS:

En el caso de querer probar la implementación de macOS usando Catalyst, necesitas instalar:

Para compilar el proyecto debemos comenzar por tener el mismo. Es sencillo:

git clone https://github.com/dotnet/maui.git C:\maui

Usando Visual Studio (Windows o macOS), tras obtener el proyecto no deberás hacer nada especial para compilarlo. Abre la solución Microsoft.Maui.sln y pulsa F5.

Estructura del proyecto

A continuación, vamos a ver la estructura básica de la solución. Con respecto a Xamarin.Forms, se simplifica considerablemente el número de proyectos (gracias al uso de Multitargeting) donde:

  • En la carpeta Controls puedes encontrar los Renderers de Xamarin.Forms y otras partes usadas por el paquete Compatibility.
  • En Core, esta disponible el proyecto Core de .NET MAUI. En este proyecto puedes encontrar los Handlers en la carpeta Handlers y es el proyecto principal que editarás. Dentro de esta carpeta también puedes encontrar los proyectos de tests relacionados con el Core de .NET MAUI.
  • En la carpeta Essentials se encuentra la evolución de Xamarin.Essentials.
  • Todo lo que permite funcionar a al proyecto único o Single Project se encuentra en la carpeta SingleProject.

Implementar una propiedad de un handler

Has revisado las issues de .NET MAUI y te has decidido a colaborar implementando una propiedad de un Handler. Fantástico!. A continuación, algunos detalles que debes tener en cuenta:

  • Como la propia issue indica, añade una sola propiedad para un control.
  • Debes implementar el método del Handler para iOS y Android.
  • Crea los métodos de extensión necesarios o reutiliza lo existentes cuando sea apropiado / posible.
  • Mueve el código del Renderer correspondiente que encontrarás en la carpeta Compatbility. Marca el código portado con el atributo [PortHandler].
  • Añade tests tanto para iOS como para Android (DeviceTests).
  • Evitar cualquier cambio que no sea esencial para la propiedad del Handler (incluidos los espacios en blanco).

Crea una nueva rama partiendo de lo último disponible en main.

No se requiere cumplir un patrón específico a la hora de crear la rama. Mis recomendaciones serían:

  • Puedes crear una rama “fix-{id}” donde el id es el identificador de la Issue.
  • Puedes usar un nombre descriptivo. Por ejemplo, si estas corrigiendo un error al seleccionar un elemento en el TabView podría ser algo como “implement-text-button”.
  • También puedes usar una combinación de las ideas anteriores. Por ejemplo: “fix-1234-text-button”.

El momento de enviar la PR

Añade un título descriptivo junto con una breve descripción. Añade el enlace a la issue asociada.

Todo listo para enviar!. Una vez enviado, miembros del equipo comenzarán a revisar todo. Una vez pasadas revisiones, builds y otras validaciones. Enhorabuena, tus cambios estarán incluidos en .NET MAUI!.

Hasta aquí. Creo que hemos realizado un buen resumen de como contribuir en .NET MAUI. Además de todo lo expuesto, si puedo ayudar en algo tan solo avísame. Cualquier feedback es bienvenido en los comentarios de la entrada!.

Más información