Compartilhar via


Migrar um renderizador personalizado do Xamarin.Forms para um manipulador MAUI do .NET

No Xamarin.Forms, renderizadores personalizados podem ser usados para personalizar a aparência e o comportamento de um controle e criar novos controles multiplataforma. Cada renderizador personalizado tem uma referência ao controle multiplataforma e geralmente depende do INotifyPropertyChanged envio de notificações de alteração de propriedade. Em vez de usar renderizadores personalizados, a interface do usuário do aplicativo .NET multiplataforma (.NET MAUI) apresenta um novo conceito chamado manipulador.

Os manipuladores oferecem muitas melhorias de desempenho em relação aos renderizadores personalizados. No Xamarin.Forms, a ViewRenderer classe cria um elemento pai. Por exemplo, no Android, é criado um ViewGroup que é usado para tarefas de posicionamento auxiliar. No .NET MAUI, a ViewHandler<TVirtualView,TPlatformView> classe não cria um elemento pai, o que ajuda a reduzir o tamanho da hierarquia visual e melhorar o desempenho do aplicativo. Os manipuladores também desacoplam os controles de plataforma da estrutura. O controle da plataforma só precisa lidar com as necessidades da estrutura. Isso não é apenas mais eficiente, mas é muito mais fácil de estender ou substituir quando necessário. Os manipuladores também são adequados para reutilização por outras estruturas, como Comet e Fabulous. Para obter mais informações sobre manipuladores, confira Manipuladores.

No Xamarin.Forms, o OnElementChanged método em um renderizador personalizado cria o controle de plataforma, inicializa valores padrão, assina eventos e manipula o elemento Xamarin.Forms ao qual o renderizador foi anexado (OldElement) e o elemento ao qual o renderizador está anexado (NewElement). Além disso, um único OnElementPropertyChanged método define as operações a serem invocadas quando ocorre uma alteração de propriedade no controle multiplataforma. O .NET MAUI simplifica essa abordagem, para que cada alteração de propriedade seja tratada por um método separado e para que o código para criar o controle de plataforma, executar a configuração de controle e executar a limpeza de controle seja separado em métodos distintos.

O processo de migração de um controle personalizado do Xamarin.Forms com suporte de renderizadores personalizados em cada plataforma para um controle personalizado do .NET MAUI com suporte de um manipulador em cada plataforma é o seguinte:

  1. Crie uma classe para o controle multiplataforma, que fornece a API pública do controle. Para obter mais informações, consulte Criar o controle multiplataforma.
  2. Crie uma partial classe de manipulador. Para obter mais informações, consulte Criar o manipulador.
  3. Na classe do manipulador, crie um PropertyMapper dicionário, que define as ações a serem executadas quando ocorrerem alterações de propriedade entre plataformas. Para obter mais informações, consulte Criar o mapeador de propriedades.
  4. Crie partial classes de manipulador para cada plataforma que criam as exibições nativas que implementam o controle multiplataforma. Para obter mais informações, consulte Criar os controles da plataforma.
  5. Registre o manipulador usando os ConfigureMauiHandlers métodos and AddHandler na classe do MauiProgram aplicativo. Para obter mais informações, consulte Registrar o manipulador.

Em seguida, o controle multiplataforma pode ser consumido. Para obter mais informações, consulte Consumir o controle multiplataforma.

Como alternativa, os renderizadores personalizados que personalizam os controles do Xamarin.Forms podem ser convertidos para que modifiquem os manipuladores do .NET MAUI. Para obter mais informações, confira Personalizar controles com manipuladores.

Criar o controle multiplataforma

Para criar um controle multiplataforma, você deve criar uma classe 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); }
        }
    }
}

O controle deve fornecer uma API pública que será acessada por seu manipulador e controlar os consumidores. Os controles multiplataforma devem derivar de View, que representa um elemento visual usado para colocar layouts e exibições na tela.

Criar o manipulador

Depois de criar seu controle multiplataforma, você deve criar uma partial classe para seu manipulador:

#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
    {
    }
}

A classe handler é uma classe parcial cuja implementação será concluída em cada plataforma com uma classe parcial adicional.

As instruções condicionais using definem o PlatformView tipo em cada plataforma. A instrução condicional using final é definida PlatformView como igual a System.Object. Isso é necessário para que o PlatformView tipo possa ser usado dentro do manipulador para uso em todas as plataformas. A alternativa seria ter que definir a propriedade uma vez por plataforma, usando compilação PlatformView condicional.

Criar o mapeador de propriedades

Cada manipulador normalmente fornece um mapeador de propriedades, que define quais ações devem ser executadas quando ocorre uma alteração de propriedade no controle multiplataforma. O PropertyMapper tipo é um Dictionary que mapeia as propriedades do controle multiplataforma para suas ações associadas.

Observação

O mapeador de propriedades é a substituição do OnElementPropertyChanged método em um renderizador personalizado do Xamarin.Forms.

PropertyMapper é definido na classe do .NET MAUI e requer que dois argumentos genéricos ViewHandler<TVirtualView,TPlatformView> sejam fornecidos:

  • A classe para o controle multiplataforma, que deriva de View.
  • A classe para o manipulador.

O exemplo de código a seguir mostra a CustomEntryHandler classe estendida com a PropertyMapper definição:

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)
    {
    }
}

O PropertyMapper é um Dictionary cuja chave é um string e cujo valor é um genérico Action. O string representa o nome da propriedade do controle multiplataforma e o Action representa um static método que requer o manipulador e o controle multiplataforma como argumentos. Por exemplo, a assinatura do MapText método é public static void MapText(CustomEntryHandler handler, CustomEntry view).

Cada manipulador de plataforma deve fornecer implementações das Ações, que manipulam as APIs de exibição nativas. Isso garante que, quando uma propriedade for definida em um controle multiplataforma, a exibição nativa subjacente será atualizada conforme necessário. A vantagem dessa abordagem é que ela permite fácil personalização de controle entre plataformas, pois o mapeador de propriedades pode ser modificado por consumidores de controle entre plataformas sem subclasses. Para obter mais informações, confira Personalizar controles com manipuladores.

Criar os controles da plataforma

Depois de criar os mapeadores para o manipulador, você deve fornecer implementações de manipulador em todas as plataformas. Isso pode ser feito adicionando implementações parciais de manipulador de classe nas pastas filho da pasta Plataformas . Como alternativa, você pode configurar seu projeto para dar suporte a vários destinos baseados em nome de arquivo ou vários destinos baseados em pasta, ou ambos.

A multiplataforma baseada em nome de arquivo é configurada adicionando o seguinte XML ao arquivo de projeto, como filhos do <Project> nó:

<!-- 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 obter mais informações sobre como configurar vários destinos, consulte Configurar vários destinos.

Cada classe de manipulador de plataforma deve ser uma classe parcial e derivar da classe, o ViewHandler<TVirtualView,TPlatformView> que requer dois argumentos de tipo:

  • A classe para o controle multiplataforma, que deriva de View.
  • O tipo de exibição nativa que implementa o controle multiplataforma na plataforma. Isso deve ser idêntico ao tipo da PlatformView propriedade no manipulador.

Importante

A ViewHandler<TVirtualView,TPlatformView> classe fornece VirtualView e PlatformView propriedades. A VirtualView propriedade é usada para acessar o controle multiplataforma de seu manipulador. A PlatformView propriedade é usada para acessar a exibição nativa em cada plataforma que implementa o controle multiplataforma.

Cada uma das implementações do manipulador de plataforma deve substituir os seguintes métodos:

  • CreatePlatformView, que deve criar e retornar a exibição nativa que implementa o controle multiplataforma.
  • ConnectHandler, que deve executar qualquer configuração de exibição nativa, como inicializar a exibição nativa e executar assinaturas de eventos.
  • DisconnectHandler, que deve executar qualquer limpeza de exibição nativa, como cancelar a assinatura de eventos e descartar objetos. Esse método não é invocado intencionalmente pelo .NET MAUI. Em vez disso, você deve invocá-lo por conta própria de um local adequado no ciclo de vida do seu aplicativo. Para obter mais informações, consulte Limpeza de exibição nativa.
  • CreatePlatformView, que deve criar e retornar a exibição nativa que implementa o controle multiplataforma.
  • ConnectHandler, que deve executar qualquer configuração de exibição nativa, como inicializar a exibição nativa e executar assinaturas de eventos.
  • DisconnectHandler, que deve executar qualquer limpeza de exibição nativa, como cancelar a assinatura de eventos e descartar objetos. Esse método é invocado automaticamente pelo .NET MAUI por padrão, embora esse comportamento possa ser alterado. Para obter mais informações, consulte Desconexão do manipulador de controle.

Observação

As CreatePlatformViewsubstituições , ConnectHandlere DisconnectHandler são as substituições para o OnElementChanged método em um renderizador personalizado do Xamarin.Forms.

Cada manipulador de plataforma também deve implementar as ações definidas nos dicionários do mapeador. Além disso, cada manipulador de plataforma também deve fornecer código, conforme necessário, para implementar a funcionalidade do controle multiplataforma na plataforma. Como alternativa, para controles mais complexos, isso pode ser fornecido por um tipo adicional.

O exemplo a seguir mostra a CustomEntryHandler implementação no 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 da ViewHandler<TVirtualView,TPlatformView> classe, com o argumento genérico CustomEntry especificando o tipo de controle multiplataforma e o AppCompatEditText argumento especificando o tipo de controle nativo.

A CreatePlatformView substituição cria e retorna um AppCompatEditText objeto. A ConnectHandler substituição é o local para executar qualquer configuração de exibição nativa necessária. A DisconnectHandler substituição é o local para executar qualquer limpeza de exibição nativa e, portanto, chama o Dispose método na AppCompatEditText instância.

O manipulador também implementa as Ações definidas no dicionário do mapeador de propriedades. Cada ação é executada em resposta a uma alteração de propriedade no controle multiplataforma e é um static método que requer instâncias de controle de manipulador e plataforma cruzada como argumentos. Em cada caso, a ação chama métodos definidos no controle nativo.

Registrar o manipulador

Um controle personalizado e seu manipulador devem ser registrados em um aplicativo antes de serem consumidos. Isso deve ocorrer no método CreateMauiApp na classe MauiProgram em seu projeto de aplicativo, que é o ponto de entrada multiplataforma do aplicativo:

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();
  }
}

O manipulador é registrado com o ConfigureMauiHandlers método and AddHandler . O primeiro argumento para o AddHandler método é o tipo de controle multiplataforma, com o segundo argumento sendo seu tipo de manipulador.

Observação

Essa abordagem de registro evita a verificação de assembly do Xamarin.Forms, que é lenta e cara.

Consumir o controle multiplataforma

Depois de registrar o manipulador com seu aplicativo, o controle multiplataforma pode ser consumido:

<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>

Limpeza de exibição nativa

A implementação do manipulador de cada plataforma substitui a implementação, que é usada para executar a DisconnectHandler limpeza de exibição nativa, como cancelar a assinatura de eventos e descartar objetos. No entanto, essa substituição não é invocada intencionalmente pelo .NET MAUI. Em vez disso, você deve invocá-lo por conta própria de um local adequado no ciclo de vida do seu aplicativo. Isso pode ocorrer quando a página que contém o controle é navegada para fora, o que faz com que o evento da Unloaded página seja gerado.

Um manipulador de eventos para o evento da Unloaded página pode ser registrado em XAML:

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

O manipulador de eventos do Unloaded evento pode invocar o DisconnectHandler método em sua Handler instância:

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

Desconexão do manipulador de controle

A implementação do manipulador de cada plataforma substitui a implementação, que é usada para executar a DisconnectHandler limpeza de exibição nativa, como cancelar a assinatura de eventos e descartar objetos. Por padrão, os manipuladores se desconectam automaticamente de seus controles quando possível, como ao navegar para trás em um aplicativo.

Em alguns cenários, talvez você queira controlar quando um manipulador se desconecta de seu controle, o que pode ser obtido com a HandlerProperties.DisconnectPolicy propriedade anexada. Essa propriedade requer um HandlerDisconnectPolicy argumento, com a enumeração definindo os seguintes valores:

  • Automatic, o que indica que o manipulador será desconectado automaticamente. Esse é o valor padrão da propriedade anexada HandlerProperties.DisconnectPolicy.
  • Manual, o que indica que o manipulador terá que ser desconectado manualmente invocando a DisconnectHandler() implementação.

O exemplo a seguir mostra a configuração da propriedade anexada HandlerProperties.DisconnectPolicy:

<controls:CustomEntry x:Name="customEntry"
                      Text="Hello world"
                      TextColor="Blue"
                      HandlerProperties.DisconnectPolicy="Manual" />             

Ao definir a propriedade anexada HandlerProperties.DisconnectPolicy como Manual , você deve invocar a implementação do DisconnectHandler manipulador por conta própria de um local adequado no ciclo de vida do aplicativo. Isso pode ser feito invocando customEntry.Handler?.DisconnectHandler();.

Além disso, há um método de extensão DisconnectHandlers que desconecta os manipuladores de um determinado IView:

video.DisconnectHandlers();

Ao se desconectar, o método DisconnectHandlers se propagará para baixo na árvore de controle até que seja concluído ou chegue a um controle que tenha definido uma política manual.

Confira também