次の方法で共有


Xamarin.Forms カスタム レンダラーを .NET MAUI ハンドラーに移行する

Xamarin.Forms では、カスタム レンダラーを使用してコントロールの外観と動作をカスタマイズし、新しいクロスプラットフォーム コントロールを作成できます。 各カスタム レンダラーにはクロスプラットフォーム コントロールへの参照があり、多くの場合、INotifyPropertyChanged に依存してプロパティ変更通知を送信します。 .NET マルチプラットフォーム アプリ UI (.NET MAUI) では、カスタム レンダラーを使用するのではなく、ハンドラーと呼ばれる新しい概念が導入されています。

ハンドラーを使用すると、カスタム レンダラーよりもパフォーマンスがさらに向上します。 Xamarin.Forms では、ViewRenderer クラスが親の要素を作成します。 たとえば、Android では、補助的な位置決めタスクに使用される ViewGroup が作成されます。 .NET MAUI では、ViewHandler クラスは親の要素を作成しないため、ビジュアル階層のサイズを小さくし、アプリのパフォーマンスを向上させることができます。 また、ハンドラーはプラットフォーム コントロールをフレームワークから切り離します。 プラットフォーム コントロールは、フレームワークのニーズを処理するだけで済みます。 これはより効率的であるだけでなく、必要に応じて簡単に拡張またはオーバーライドすることができます。 ハンドラーは、CometFabulous などの他のフレームワークによる再利用にも適しています。 ハンドラーの詳細については、「ハンドラー」をご覧ください。

Xamarin.Forms では、カスタム レンダラーの OnElementChanged メソッドによってプラットフォーム コントロールが作成され、既定値が初期化され、イベントがサブスクライブされ、レンダラーがアタッチされていた Xamarin.Forms 要素 (OldElement) とレンダラーがアタッチされている要素 (NewElement) が処理されます。 さらに、単一の OnElementPropertyChanged メソッドは、クロスプラットフォーム コントロールでプロパティの変更が発生したときに呼び出す操作を定義します。 .NET MAUI では、このアプローチを簡略化し、すべてのプロパティ変更が個別のメソッドによって処理されるようにし、プラットフォーム コントロールを作成し、コントロールのセットアップを実行し、コントロール クリーンアップを実行するコードを個別のメソッドに分離します。

各プラットフォームのカスタム レンダラーによってサポートされる Xamarin.Forms カスタム コントロールを、各プラットフォームのハンドラーによってサポートされる .NET MAUI カスタム コントロールに移行するプロセスは次のとおりです。

  1. コントロールのパブリック API を提供するクロスプラットフォーム コントロールのクラスを作成します。 詳細については、「クロスプラットフォームのコントロールを作成する」をご覧ください。
  2. partial ハンドラー クラスを作成します。 詳細については、「ハンドラーを作成する」をご覧ください。
  3. ハンドラー クラスで、PropertyMapper ディクショナリを作成します。これは、クロスプラットフォームのプロパティ変更が発生したときに実行するアクションを定義します。 詳細については、「プロパティ マッパーを作成する」をご覧ください。
  4. クロスプラットフォーム コントロールを実装するネイティブ ビューを作成する各プラットフォームの partial ハンドラー クラスを作成します。 詳細については、「プラットフォーム コントロールを作成する」をご覧ください。
  5. アプリの MauiProgram クラスの ConfigureMauiHandlers メソッドと AddHandler メソッドを使用してハンドラーを登録します。 詳細については、「ハンドラーを登録する」をご覧ください。

その後、クロスプラットフォーム コントロールを使用できます。 詳細については、「クロスプラットフォーム コントロールを使用する」をご覧ください。

あるいは、Xamarin.Forms コントロールをカスタマイズするカスタム レンダラーを変換して、.NET MAUI ハンドラーを変更することもできます。 詳細については、「ハンドラーを使用してコントロールをカスタマイズする」をご覧ください。

クロスプラットフォーム コントロールを作成する

クロスプラットフォーム コントロールを作成するには、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); }
        }
    }
}

コントロールは、ハンドラーによってアクセスされ、コンシューマーを制御するパブリック API を提供する必要があります。 クロスプラットフォーム コントロールは、画面にレイアウトとビューを配置するために使用されるビジュアル要素を表す View から派生する必要があります。

ハンドラーを作成する

クロスプラットフォーム コントロールを作成したら、ハンドラーの partial クラスを作成する必要があります。

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

ハンドラー クラスは部分クラスであり、その実装は追加の部分クラスを使用して各プラットフォームで完了します。

条件付き using ステートメントは、各プラットフォームの PlatformView タイプを定義します。 最後の条件付き using ステートメントは、PlatformViewSystem.Object に等しいと定義します。 これは、PlatformView 型をハンドラー内で使用してすべてのプラットフォームで使用できるようにするために必要です。 別の方法としては、条件付きコンパイルを使用して、プラットフォームごとに 1 回 PlatformView プロパティを定義する必要があります。

プロパティ マッパーを作成する

通常、各ハンドラーはプロパティ マッパーを提供します。これは、クロスプラットフォーム コントロールでプロパティの変更が発生したときに実行するアクションを定義します。 PropertyMapper 型は、クロスプラットフォーム コントロールのプロパティを関連するアクションにマッピングする Dictionary です。

Note

プロパティ マッパーは、Xamarin.Forms カスタム レンダラーの OnElementPropertyChanged メソッドに代わるものです。

PropertyMapper は .NET MAUI の汎用 ViewHandler クラスで定義されており、次の 2 つの汎用引数を指定する必要があります。

  • View から派生するクロスプラットフォーム コントロールのクラス。
  • ハンドラーのクラス。

次のコード例は、PropertyMapper 定義により拡張された CustomEntryHandler クラスを示しています。

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

PropertyMapperDictionary で、そのキーは string で、その値はジェネリック Action です。 string はクロスプラットフォーム コントロールのプロパティ名を表し、Action は、引数としてハンドラーとクロスプラットフォーム コントロールを必要とする static メソッドを表します。 例えば、MapText メソッドのシグネチャは public static void MapText(CustomEntryHandler handler, CustomEntry view) です。

各プラットフォーム ハンドラーは、ネイティブ ビュー API を操作するアクションの実装を提供する必要があります。 これにより、クロスプラットフォーム コントロールでプロパティが設定されると、基になるネイティブ ビューが必要に応じて更新されます。 このアプローチの利点は、クロスプラットフォーム コントロール コンシューマーがサブクラス化せずにプロパティ マッパーを変更できるため、クロスプラットフォーム コントロールのカスタマイズを簡単にできることです。 詳細については、「ハンドラーを使用してコントロールをカスタマイズする」をご覧ください。

プラットフォーム コントロールの作成

ハンドラーのマッパーを作成した後、すべてのプラットフォームでハンドラーの実装を提供する必要があります。 これを実現するには、プラットフォーム フォルダーの子フォルダーに部分クラス ハンドラーの実装を追加します。 または、ファイル名ベースのマルチターゲット、フォルダーベースのマルチターゲット、またはその両方をサポートするようにプロジェクトを構成することもできます。

ファイル名ベースのマルチターゲットは、次の XML を <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>

マルチターゲットの構成の詳細については、「マルチターゲットの構成」をご覧ください。

各プラットフォーム ハンドラー クラスは部分クラスで、ジェネリック ViewHandler クラスから派生する必要があります。これには、次の 2 つの型の引数が必要です。

  • View から派生するクロスプラットフォーム コントロールのクラス。
  • プラットフォームでクロスプラットフォーム コントロールを実装するネイティブ ビューの型。 これは、ハンドラー内の PlatformView プロパティの型と同じである必要があります。

重要

ViewHandler クラスは、VirtualView プロパティと PlatformView プロパティを提供します。 VirtualView プロパティは、ハンドラーからクロスプラットフォーム コントロールにアクセスするために使用されます。 PlatformView プロパティは、クロスプラットフォーム コントロールを実装する各プラットフォームのネイティブ ビューにアクセスするために使用されます。

プラットフォーム ハンドラーの実装はそれぞれ、次のメソッドをオーバーライドする必要があります。

  • CreatePlatformView は、クロスプラットフォーム コントロールを実装するネイティブ ビューを作成して返す必要があります。
  • ConnectHandler は、ネイティブ ビューの初期化やイベント サブスクリプションの実行など、ネイティブ ビューのセットアップを実行する必要があります。
  • DisconnectHandler は、イベントからのサブスクライブ解除やオブジェクトの破棄など、ネイティブ ビューのクリーンアップを実行する必要があります。 このメソッドは、.NET MAUI によって意図的に呼び出されません。 代わりに、アプリのライフサイクル内の適切な場所から自分で呼び出す必要があります。 詳細については、「ネイティブ ビューのクリーンアップ」をご覧ください。

Note

CreatePlatformViewConnectHandlerDisconnectHandler オーバーライドは、Xamarin.Forms カスタム レンダラーの OnElementChanged メソッドに代わるものです。

各プラットフォーム ハンドラーは、マッパー ディクショナリで定義されているアクションも実装する必要があります。 さらに、各プラットフォーム ハンドラーは、プラットフォームにクロスプラットフォーム コントロールの機能を実装するために、必要に応じてコードを提供する必要もあります。 または、より複雑なコントロールの場合は、追加の型で提供できます。

次の例は Android での CustomEntryHandler の実装を示しています。

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

CustomEntryHandlerViewHandler クラスから派生し、ジェネリック CustomEntry 引数はクロスプラットフォーム コントロール型を指定し、AppCompatEditText 引数はネイティブ コントロールの型を指定します。

CreatePlatformView オーバーライドによって AppCompatEditText オブジェクトが作成され、返されます。 ConnectHandler オーバーライドは、必要なネイティブ ビューのセットアップを実行する場所です。 DisconnectHandler オーバーライドは、ネイティブ ビューのクリーンアップを実行する場所であるため、 AppCompatEditText インスタンスで Dispose メソッドを呼び出します。

ハンドラーは、プロパティ マッパー ディクショナリで定義されているアクションも実装します。 各アクションは、クロスプラットフォーム コントロールでのプロパティの変更に応答して実行され、引数としてハンドラーとクロスプラットフォーム コントロール インスタンスを必要とする static メソッドです。 いずれの場合も、アクションはネイティブ コントロールで定義されているメソッドを呼び出します。

ハンドラーの登録

カスタム コントロールとそのハンドラーは、アプリを使用する前に登録する必要があります。 これは、アプリ プロジェクトの MauiProgram クラス内の CreateMauiApp メソッド (アプリのクロスプラットフォーム エントリ ポイント) で発生する必要があります。

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

ハンドラは ConfigureMauiHandlersAddHandler メソッドで登録されます。 AddHandler メソッドの最初の引数はクロスプラットフォーム コントロール型で、2 番目の引数はそのハンドラー型です。

Note

この登録方法により、低速でコストがかかる Xamarin.Forms のアセンブリ スキャンを回避できます。

クロスプラットフォーム コントロールを使用する

ハンドラーをアプリに登録したら、クロスプラットフォーム コントロールを使用できるようになります。

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

ネイティブ ビューのクリーンアップ

各プラットフォームのハンドラー実装は、DisconnectHandler 実装をオーバーライドします。この実装は、イベントのサブスクライブ解除やオブジェクトの破棄など、ネイティブ ビューのクリーンアップを実行するために使用されます。 ただし、このオーバーライドは、.NET MAUI によって意図的に呼び出されません。 代わりに、アプリのライフサイクル内の適切な位置から自分で呼び出す必要があります。 コントロールを含むページから移動したときなどがこれに該当し、その結果、ページの Unloaded イベントが発生します。

ページの Unloaded イベントのイベント ハンドラーを XAML に登録できます。

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

その結果、Unloaded イベントのイベント ハンドラーは、その Handler インスタンスに対して DisconnectHandler メソッドを呼び出すことができます。

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

関連項目