緣起
2025-12-24 21:35:30 星期三 🎄
MAUI沒有Popup,百度也找不到大佬的現成輪子。
CommunityToolkits 實現的 Popup 有嚴重的內存泄露問題,本想仿寫 CommunityToolkits 源碼實現,未果。
問了下通義,發現輪子雛形挺簡潔,根本不需要 CommunityToolkits 那一套。
以下是重新封裝成 可傳出<TResult>的輪子。
Popup包裝
PopupPage.xaml.cs
PopupPage.xaml
Popup
Popup.cs
成果
使用示例
Popup
PopupPage.xaml.cs
static class PopupPageExtension
{
public static Task<Popup<TResult>.PopupResult> ShowAsync<TResult>(this Popup<TResult> popup, Page invoker)
{
var page = new PopupPage(popup);
invoker.Navigation.PushModalAsync(page);
return page.Popup.WaitResultTask as Task<Popup<TResult>.PopupResult>;
}
}
/// <summary>
/// 這是包裝,請勿食用
/// </summary>
partial class PopupPage : ContentPage
{
public PopupPage(ContentView popup)
{
InitializeComponent();
mOverlay.Opacity = 0;
mBorder.Content = popup;
mBorder.Scale = 0.01;
}
protected override void OnAppearing()
{
base.OnAppearing();
Montages(false);
}
async Task Montages(bool onDismiss)
{
if (!onDismiss)
{
var t1 = mOverlay.FadeTo(1, 150);
var t2 = mBorder.ScaleTo(1, 200, Easing.CubicOut);
await Task.WhenAny(t1, t2);
}
else
{
var t1 = mOverlay.FadeTo(0, 150, Easing.CubicOut);
var t2 = mBorder.ScaleTo(0.01, 150);
await Task.WhenAny(t1, t2);
}
}
protected override void OnNavigatedTo(NavigatedToEventArgs args)
{
base.OnNavigatedTo(args);
}
private async void OnOverlayTapped(object sender, EventArgs e)
{
(sender as VisualElement).IsEnabled = false;
try
{
await CloseAsync(true);
}
finally
{
(sender as VisualElement).IsEnabled = true;
}
}
public interface IPopup
{
bool TrySetResult();
bool TrySetCancel();
object WaitResultTask { get; }
void OnClose();
}
public IPopup Popup => mBorder.Content as IPopup;
public async Task CloseAsync(bool isCanceled)
{
NavigatedFrom += isCanceled
? IsCanceled_NavigatedFrom
: SetResult_NavigatedFrom;
await Montages(true);
await Navigation.PopModalAsync();
}
static void IsCanceled_NavigatedFrom(object sender, NavigatedFromEventArgs e)
{
var This = sender as PopupPage;
This.NavigatedFrom -= IsCanceled_NavigatedFrom;
This.Popup.TrySetCancel();
This.Popup.OnClose();
}
static void SetResult_NavigatedFrom(object sender, NavigatedFromEventArgs e)
{
var This = sender as PopupPage;
This.NavigatedFrom -= SetResult_NavigatedFrom;
This.Popup.TrySetResult();
This.Popup.OnClose();
}
}
PopupPage.xaml
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="PopupPage"
x:ClassModifier="internal"
BackgroundColor="#50000000"
>
<Grid
x:Name="mOverlay"
BackgroundColor="#80000000"
Opacity="1"
>
<!-- 遮罩層(點擊關閉) -->
<Grid.GestureRecognizers>
<TapGestureRecognizer Tapped="OnOverlayTapped" />
</Grid.GestureRecognizers>
<!-- Popup 內容容器 -->
<Border
x:Name="mBorder"
HorizontalOptions="Center"
VerticalOptions="Center"
>
<Label Text="找我有啥事?"/>
</Border>
</Grid>
</ContentPage>
Popup.cs
/// <summary>
/// 這是容器,請繼承使用。
/// </summary>
abstract class Popup<TResult> : ContentView, PopupPage.IPopup
{
public record class PopupResult(bool IsCanceled, TResult Result);
public TResult Result { get; protected set; }
/// <summary>
/// 這個是給 用户 等待用的。
/// 等待結束時,Popup 大概率已經銷燬了。
/// 所以 ResultTask.Set 只能在 NavigatedFrom 調用。
/// </summary>
readonly TaskCompletionSource<PopupResult> ResultTask = new();
Task<PopupResult> WaitResult => ResultTask.Task;
#region IPopup
object PopupPage.IPopup.WaitResultTask => WaitResult;
bool PopupPage.IPopup.TrySetResult()
{
return ResultTask.TrySetResult(new PopupResult(false, Result));
}
bool PopupPage.IPopup.TrySetCancel()
{
return ResultTask.TrySetResult(new PopupResult(true, Result));
}
public abstract void OnClose();
#endregion
PopupPage GetPopupPage()
{
var parent = Parent;
while(parent is not null)
{
if (parent is PopupPage page)
return page;
parent = parent.Parent;
}
return null;
}
protected async Task CloseAsync(bool isCanceled)
{
var page = GetPopupPage();
await page.CloseAsync(isCanceled);
}
/// <summary>
/// 用户取消
/// </summary>
public async Task CloseAsync() => await CloseAsync(true);
}
使用示例
partial class LoginPopup : Popup<UserInfo>
{
public LoginPopup()
{
InitializeComponent();
// TODO
}
public override void OnClose()
{
// TODO
}
}

有一個小小的操作bug,很容易解決。😏解決不了問通義