Compartir a través de


Migración de un representador personalizado de Xamarin.Forms a un controlador de .NET MAUI.

En Xamarin.Forms, los representadores personalizados se pueden usar para personalizar la apariencia y el comportamiento de un control y crear nuevos controles multiplataforma. Cada representador personalizado tiene una referencia al control multiplataforma y, a menudo, se basa en INotifyPropertyChanged para enviar notificaciones de cambio de propiedad. En lugar de usar representadores personalizados, .NET Multi-platform App UI (.NET MAUI) presenta un nuevo concepto denominado controlador.

Los controladores ofrecen muchas mejoras de rendimiento por encima de los representadores personalizados. En Xamarin.Forms, la clase ViewRenderer crea un elemento primario. Por ejemplo, en Android, se crea un ViewGroup objeto que se usa para tareas de posicionamiento auxiliar. En .NET MAUI, la clase ViewHandler no crea un elemento primario, lo que ayuda a reducir el tamaño de la jerarquía visual y mejorar el rendimiento de la aplicación. Los controladores también desacoplan los controles de plataforma del marco. El control de plataforma solo necesita controlar las necesidades del marco. Esto no solo es más eficaz, sino que es mucho más fácil extender o de invalidar cuando es necesario. Los controladores también son adecuados para su reutilización por parte de otros marcos como Comet y Fabulous. Para obtener más información sobre controladores, consultaHandlers.

En Xamarin.Forms, el método OnElementChanged de un representador personalizado crea el control de plataforma, inicializa los valores predeterminados, se suscribe a eventos y controla el elemento de Xamarin.Forms al que se adjuntó el representador (OldElement) y el elemento al que está asociado el representador (NewElement). Además, un único método OnElementPropertyChanged define las operaciones que se invocarán cuando se produce un cambio de propiedad en el control multiplataforma. .NET MAUI simplifica este enfoque, de modo que cada cambio de propiedad se controla mediante un método independiente y de modo que el código para crear el control de plataforma, realizar la configuración del control y la limpieza de controles se separa en métodos distintos.

El proceso para migrar un control personalizado de Xamarin.Forms respaldado por representadores personalizados en cada plataforma a un control personalizado de .NET MAUI respaldado por un controlador en cada plataforma es el siguiente:

  1. Crea una clase para el control multiplataforma, que proporciona la API pública del control. Para obtener más información, consulta Creación del control multiplataforma.
  2. Cree una clase de controlador partial. Para obtener más información, consultaCreación del controlador.
  3. En la clase de controlador, crea un diccionario PropertyMapper, que define las acciones que se realizarán cuando se produzcan cambios en las propiedades multiplataforma. Para obtener más información, consultaCreación del asignador de propiedades.
  4. Crea clases de controlador partial para cada plataforma que crea las vistas nativas que implementa el control multiplataforma. Para obtener más información, consulta Creación de los controles de plataforma.
  5. Registra el controla dor mediante los métodos ConfigureMauiHandlers y AddHandler en la clase MauiProgram de la aplicación. ara obtener más información, consulta Registro del controlador.

Después, se puede consumir el control multiplataforma. Para obtener más información, consulta Consumo del control multiplataforma.

Como alternativa, los representadores personalizados que personalizan controles de Xamarin.Forms se pueden convertir para que modifiquen los controladores .NET MAUI. Para obtener más información, consulta Personalización de controles con controladores.

Creación del control multiplataforma

Para crear un control multiplataforma, debes crear una clase que derive de View:

namespace MyMauiControl.Controls
{
    public class CustomEntry : View
    {
        public static readonly BindableProperty TextProperty =
            BindableProperty.Create(nameof(Text), typeof(string), typeof(CustomEntry), null);

        public static readonly BindableProperty TextColorProperty =
            BindableProperty.Create(nameof(TextColor), typeof(Color), typeof(CustomEntry), null);

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

        public Color TextColor
        {
            get { return (Color)GetValue(TextColorProperty); }
            set { SetValue(TextColorProperty, value); }
        }
    }
}

El control debe proporcionar una API pública a la que accederán sus controladores y controlará a los consumidores. Los controles multiplataforma deben derivar de View, que representa un elemento visual que se usa para colocar diseños y vistas en la pantalla.

Crea el controlador.

Después de crear el control multiplataforma, debes crear una clase partial para el controlador:

#if IOS || MACCATALYST
using PlatformView = Microsoft.Maui.Platform.MauiTextField;
#elif ANDROID
using PlatformView = AndroidX.AppCompat.Widget.AppCompatEditText;
#elif WINDOWS
using PlatformView = Microsoft.UI.Xaml.Controls.TextBox;
#elif (NETSTANDARD || !PLATFORM) || (NET6_0_OR_GREATER && !IOS && !ANDROID)
using PlatformView = System.Object;
#endif
using MyMauiControl.Controls;
using Microsoft.Maui.Handlers;

namespace MyMauiControl.Handlers
{
    public partial class CustomEntryHandler
    {
    }
}

La clase de controlador es una clase parcial cuya implementación se completará en cada plataforma con una clase parcial adicional.

Las instrucciones using condicionales definen el tipo PlatformView en cada plataforma. La instrucción condicional using final define PlatformView que es igual a System.Object. Esto es necesario para que el tipo PlatformView se pueda usar en el controlador para su uso en todas las plataformas. La alternativa sería tener que definir la propiedad PlatformView una vez por plataforma mediante la compilación condicional.

Creación del asignador de propiedades

Cada controlador normalmente proporciona un asignador de propiedades, que define qué acciones realizar cuando se produce un cambio de propiedad en el control multiplataforma. El tipo PropertyMapper es un objeto Dictionary que asigna las propiedades del control multiplataforma a sus acciones asociadas.

Nota:

El asignador de propiedades es el reemplazo del método OnElementPropertyChanged en un representador personalizado de Xamarin.Forms.

PropertyMapper se define en la clase genérica ViewHandler de .NET MAUI y requiere que se proporcionen dos argumentos genéricos:

  • La clase para el control multiplataforma, que deriva de View.
  • La clase para el controlador.

En el ejemplo de código siguiente se muestra la clase CustomEntryHandler ampliada con la definición de PropertyMapper:

public partial class CustomEntryHandler
{
    public static PropertyMapper<CustomEntry, CustomEntryHandler> PropertyMapper = new PropertyMapper<CustomEntry, CustomEntryHandler>(ViewHandler.ViewMapper)
    {
        [nameof(CustomEntry.Text)] = MapText,
        [nameof(CustomEntry.TextColor)] = MapTextColor
    };

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

PropertyMapper es un Dictionary cuya clave es string y cuyo valor es un Action genérico. string representa el nombre de propiedad del control multiplataforma y Action representa un método static que requiere el controlador y el control multiplataforma como argumentos. Por ejemplo, la firma del método MapText es public static void MapText(CustomEntryHandler handler, CustomEntry view).

Cada controlador de plataforma debe proporcionar implementaciones de las acciones, que manipulan las API de vista nativa. Esto garantiza que, cuando se establece una propiedad en un control multiplataforma, la vista nativa subyacente se actualizará según sea necesario. La ventaja de este enfoque es que permite una fácil personalización de control multiplataforma, ya que el asignador de propiedades se puede modificar mediante consumidores de control multiplataforma sin subclases. Para obtener más información, consulta Personalización de controles con controladores.

Creación de los controles de plataforma

Después de crear los asignadores para el controlador, debes proporcionar implementaciones de controlador en todas las plataformas. Esto se puede lograr agregando implementaciones de controlador de clases parciales en las carpetas secundarias de la carpeta Plataformas. Como alternativa, puedes configurar el proyecto para admitir el destino múltiple basado en nombre de archivo, o destino múltiple basado en carpetas, o ambos.

El destino múltiple basado en nombre de archivo se configura agregando el siguiente XML al archivo de proyecto, como elementos secundarios del nodo <Project>:

<!-- Android -->
<ItemGroup Condition="$(TargetFramework.StartsWith('net8.0-android')) != true">
  <Compile Remove="**\*.Android.cs" />
  <None Include="**\*.Android.cs" Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder)" />
</ItemGroup>

<!-- iOS and Mac Catalyst -->
<ItemGroup Condition="$(TargetFramework.StartsWith('net8.0-ios')) != true AND $(TargetFramework.StartsWith('net8.0-maccatalyst')) != true">
  <Compile Remove="**\*.MaciOS.cs" />
  <None Include="**\*.MaciOS.cs" Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder)" />
</ItemGroup>

<!-- Windows -->
<ItemGroup Condition="$(TargetFramework.Contains('-windows')) != true ">
  <Compile Remove="**\*.Windows.cs" />
  <None Include="**\*.Windows.cs" Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder)" />
</ItemGroup>

Para obtener más información sobre cómo configurar varios destinos, consulta Configuración de varios destinos.

Cada clase de controlador de plataforma debe ser una clase parcial y derivar de la clase ViewHandler genérica, que requiere dos argumentos de tipo:

  • La clase para el control multiplataforma, que deriva de View.
  • El tipo de la vista nativa que implementa el control multiplataforma en la plataforma. Este debe ser idéntico al tipo de la propiedad PlatformView en el controlador.

Importante

La clase ViewHandler proporciona propiedades VirtualView y PlatformView. La propiedad VirtualView se usa para acceder al control multiplataforma desde su controlador. La propiedad PlatformView, se usa para acceder a la vista nativa en cada plataforma que implementa el control multiplataforma.

Cada una de las implementaciones del controlador de plataforma debe invalidar los métodos siguientes:

  • CreatePlatformView, que debe crear y devolver la vista nativa que implementa el control multiplataforma.
  • ConnectHandler, que debe realizar cualquier configuración de vista nativa, como inicializar la vista nativa y realizar suscripciones de eventos.
  • DisconnectHandler, que debe realizar cualquier limpieza de vista nativa, como anular la suscripción de eventos y eliminar objetos. .NET MAUI no invoca intencionadamente este método. En su lugar, debes invocarla por tu cuenta desde una ubicación adecuada en el ciclo de vida de la aplicación. Para obtener más información, consulta Limpieza de la vista nativa.

Nota:

Las invalidaciones CreatePlatformView, ConnectHandler y DisconnectHandler son los reemplazos del método OnElementChanged en un representador personalizado de Xamarin.Forms.

Cada controlador de plataforma también debe implementar las acciones definidas en los diccionarios del asignador. Además, cada controlador de plataforma también debe proporcionar código, según sea necesario, para implementar la funcionalidad del control multiplataforma en la plataforma. Como alternativa, para controles más complejos, esto se puede proporcionar mediante un tipo adicional.

En el siguiente ejemplo se muestra una implementación CustomEntryHandler en Android:

#nullable enable
using AndroidX.AppCompat.Widget;
using Microsoft.Maui.Handlers;
using Microsoft.Maui.Platform;
using MyMauiControl.Controls;

namespace MyMauiControl.Handlers
{
    public partial class CustomEntryHandler : ViewHandler<CustomEntry, AppCompatEditText>
    {
        protected override AppCompatEditText CreatePlatformView() => new AppCompatEditText(Context);

        protected override void ConnectHandler(AppCompatEditText platformView)
        {
            base.ConnectHandler(platformView);

            // Perform any control setup here
        }

        protected override void DisconnectHandler(AppCompatEditText platformView)
        {
            // Perform any native view cleanup here
            platformView.Dispose();
            base.DisconnectHandler(platformView);
        }

        public static void MapText(CustomEntryHandler handler, CustomEntry view)
        {
            handler.PlatformView.Text = view.Text;
            handler.PlatformView?.SetSelection(handler.PlatformView?.Text?.Length ?? 0);
        }

        public static void MapTextColor(CustomEntryHandler handler, CustomEntry view)
        {
            handler.PlatformView?.SetTextColor(view.TextColor.ToPlatform());
        }
    }
}

CustomEntryHandler deriva de la clase ViewHandler, con el argumento CustomEntry genérico que especifica el tipo de control multiplataforma y el argumento AppCompatEditText que especifica el tipo de control nativo.

La invalidación CreatePlatformView crea y devuelve un objeto AppCompatEditText. La invalidación ConnectHandler es la ubicación para realizar cualquier configuración de vista nativa necesaria. La invalidación DisconnectHandler es la ubicación para realizar cualquier limpieza de vista nativa y, por tanto, llama al método Dispose en la instancia AppCompatEditText.

El controlador también implementa las acciones definidas en el diccionario del asignador de propiedades. Cada acción se ejecuta en respuesta a un cambio de propiedad en el control multiplataforma y es un método static que requiere instancias de control multiplataforma y controlador como argumentos. En cada caso, la acción llama a los métodos definidos en el control nativo.

Registro del controlador

Un control personalizado y su controlador deben registrarse con una aplicación para poder consumirlo. Esto debe ocurrir en el método CreateMauiApp de la clase MauiProgram del proyecto de aplicación, que es el punto de entrada multiplataforma de la aplicación:

using Microsoft.Extensions.Logging;
using MyMauiControl.Controls;
using MyMauiControl.Handlers;

namespace MyMauiControl;

public static class MauiProgram
{
  public static MauiApp CreateMauiApp()
  {
    var builder = MauiApp.CreateBuilder();
    builder
      .UseMauiApp<App>()
      .ConfigureFonts(fonts =>
      {
        fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
        fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
      })
      .ConfigureMauiHandlers(handlers =>
      {
        handlers.AddHandler(typeof(CustomEntry), typeof(CustomEntryHandler));
      });

#if DEBUG
    builder.Logging.AddDebug();
#endif

    return builder.Build();
  }
}

El controlador se registra con el método ConfigureMauiHandlers y AddHandler. El primer argumento para el método AddHandler es el tipo de control multiplataforma, siendo el segundo argumento su tipo de controlador.

Nota:

Este enfoque de registro evita la detección de ensamblados de Xamarin.Forms, que es lenta y costosa.

Consumo del control multiplataforma

Después de registrar el controlador con la aplicación, se puede consumir el control multiplataforma:

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:controls="clr-namespace:MyMauiControl.Controls"
             x:Class="MyMauiControl.MainPage">
    <Grid>
        <controls:CustomEntry Text="Hello world"
                              TextColor="Blue" />
    </Grid>
</ContentPage>

Limpieza de vistas nativas

La implementación del controlador de cada plataforma invalida la implementación DisconnectHandler, que se usa para realizar la limpieza de vistas nativas, como la anulación de la suscripción a eventos y la eliminación de objetos. Pero .NET MAUI no invoca intencionadamente esta invalidación. En su lugar, debes invocarla por tu cuenta desde una ubicación adecuada en el ciclo de vida de la aplicación. Esto podría suceder cuando la página que contiene el control se aleja, lo que hace que se genere el evento Unloaded de la página.

Un controlador de eventos para el evento Unloaded de la página se puede registrar en XAML:

<ContentPage ...
             xmlns:controls="clr-namespace:MyMauiControl.Controls"
             Unloaded="ContentPage_Unloaded">
    <Grid>
        <controls:CustomEntry x:Name="customEntry"
                              ... />
    </Grid>
</ContentPage>

Después, el controlador de eventos del evento Unloaded puede invocar el método DisconnectHandler en su instancia Handler:

void ContentPage_Unloaded(object sender, EventArgs e)
{
    customEntry.Handler?.DisconnectHandler();
}

Consulta también