MAUI標準で提供されているDisplayAlertではなく、コミュニティ主導で開発されているCommunityToolkitの方のポップアップ。 カスタムUIでポップアップを簡単に実装できる利便性の高いもので、最近書き直しされてV2に。

リリースノート
移行ガイド
ドキュメント

V2の主な変更点

2025/06/18時点のメモなので注意。最新の情報はドキュメントおよびリポジトリを参照。

同一インスタンスで複数回表示できるように

一度作成したPopupインスタンスを使い回せるようになった。この変更に伴い、SingletonやScoped等のライフサイクルでDI登録して、IPopupServiceから 必要なタイミングで呼び出せるように。また、IPopupServiceの名前空間はCommunityToolkit.Maui.CoreからCommunityToolkit.Mauiに移動。

builder.Services.AddTransientPopup<SimplePopup, SimplePopupViewModel>();
builder.Services.AddScopedPopup<AppPopup, AppPopupViewModel>();
builder.Services.AddSingletonPopup<SettingPopup>();

popupService.ShowPopupAsync<SimplePopupViewModel>(Shell.Current,options: null, queryAttributes: null);

Popupを継承せずに、コンテンツとなるViewを直接渡して表示できるように

ポップアップ終了時の結果が不要な場合、CommunityToolkit.Maui.Views.Popupを継承せずに直接Viewを渡して表示することが可能になった。Popup自体はただのContentViewなので、 従来通りPopupを継承して渡すことも可能。

public static async Task<IPopupResult<TResult>> ShowPopupAsync<TResult>(this Page page, View view, IPopupOptions? options = null, CancellationToken token = default);
public static async Task<IPopupResult<TResult>> ShowPopupAsync<TResult>(this INavigation navigation, View view, IPopupOptions? options = null, CancellationToken token = default);
public static async Task<IPopupResult<TResult>> ShowPopupAsync<TResult>(this Shell shell, View view, IPopupOptions? options = null, IDictionary<string, object>? shellParameters = null, CancellationToken token = default);

IQueryAttributableでパラメータを渡すように

ポップアップのビューモデルにパラメータを渡したい場合、従来はonPresenting引数のオーバーロードで設定していた。V2からは Shellナビゲーション間での受け渡しと同様に、IQueryAttributableを実装して辞書で渡せるようになった。

// https://github.com/CommunityToolkit/Maui/wiki/Migrating-to-Popup-v2#passing-data-to-a-popup-has-changed
public class NamePopupViewModel : ObservableObject, IQueryAttributable
{
    [ObservableProperty]
    public partial string Name { get; set; } = "";

    public void ApplyQueryAttributes(IDictionary<string, object> query)
    {
        Name = query[nameof(NamePopupViewModel.Name)] as string;
    }
}

public class MyViewModel : INotifyPropertyChanged
{
    private readonly IPopupService popupService;

    public MyViewModel(IPopupService popupService)
    {
        this.popupService = popupService;
    }

    public async Task DisplayPopup()
    {
        var queryAttributes = new Dictionary<string, object>
        {
            [nameof(NamePopupViewModel.Name)] = "Shaun"
        };

        await this.popupService.ShowPopupAsync<NamePopupViewModel>(
            Shell.Current,
            options: null,
            queryAttributes);
    }
}

結果をIPopupResult,IPopupResult<T>で受け取るように

object?からIPopupResultに変更されて型安全に。また、従来ではポップアップの外をタップして閉じられたかどうかを判別するにはOnClosedもしくは OnDismissedByTappingOutsideOfPopupメソッドをオーバーライドする必要があったが、V2からはIPopupResultに含まれるようになった。 これにより、V2のPopupではOnClosedメソッドがOnClosedイベントに置き換わるなど、APIの変更がある。

public interface IPopupResult<out TResult> : IPopupResult
{
	TResult? Result { get; }
}

public interface IPopupResult
{
	bool WasDismissedByTappingOutsideOfPopup { get; }
}

ここで注意が必要なのが、Null非許容型を結果にしている時の挙動である。結果がboolのポップアップを外側タップで終了させた場合、 ResultにアクセスするとCommunityToolkit.Maui.PopupResultExceptionが発生する。 そのため、結果無しで終了すると想定される場合、先にWasDismissedByTappingOutsideOfPopupをチェックする必要がある。

# タプルが結果のポップアップの場合の例
CommunityToolkit.Maui.PopupResultException:
'Result is null, but PopupResult type, System.ValueTuple`2[System.Boolean,System.String], cannot be converted to null. 
Every time WasDismissedByTappingOutsideOfPopup is True, Result is always null. When using a non-nullable type (e.g. bool) be sure to first check if 
WasDismissedByTappingOutsideOfPopup is False before getting the value of Result'

https://github.com/CommunityToolkit/Maui/blob/main/src/CommunityToolkit.Maui/Primitives/PopupResult.shared.cs

ポップアップの挙動や外観のオプション

V1にもあった、CanBeDismissedByTappingOutsideOfPopup(ポップアップの外をタップすることで閉じるようにするフラグ)はPopupクラスのメンバーから PopupOptionsで呼び出し時に設定するように変更。また、このオプションから背景オーバーレイや影、境界線のスタイルを指定可能になった。オプションは 呼び出しごとに適用され、ポップアップ終了後は維持されないため、共通の設定を適用したい場合はキャッシュする必要がある。

this.ShowPopupAsync(new SimplePopup(), new PopupOptions()
{
    CanBeDismissedByTappingOutsideOfPopup = true,
    Shape = new RoundRectangle()
    {
        CornerRadius = new CornerRadius(4),
        Stroke = Colors.Blue,
        StrokeThickness = 4
    },
    PageOverlayColor = Colors.White,
    Shadow = new Shadow()
    {
        Offset = new Point(20,20),
        Radius = 20f,
        Opacity = 0.3f,
    }
});

プロパティの置き換え

SizeとColorが置き換えられた。

<toolkit:Popup 
    xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
    x:Class="MyProject.SimplePopup"
-   Size="100,200"
+   WidthRequest="100"
+   HeightRequest="200">
    
</toolkit:Popup>
<toolkit:Popup 
    xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
    x:Class="MyProject.SimplePopup"
-   Color="Green"
+   BackgroundColor="Green">
    
</toolkit:Popup>

方針

DI登録とIPopupServiceでのポップアップ表示、IQueryAttributableによるパラメータ受け渡しは密結合を回避するには有用だが、抽象度の高さゆえの 認知負荷・管理負荷増大のリスクも生じる。 私の環境では、従来通り依存関係の多いポップアップのみDI登録し、パラメータの受け渡しに関してはできる限りインスタンス生成時に コンストラクタで渡す実装を継続する。