更那堪冷落清秋节 發表於 2026-1-3 17:28:00

[MAUI]简单可食用的Popup<TResult>

<h1 id="缘起">缘起</h1>
<p>2025-12-24 21:35:30 星期三 🎄<br>
MAUI没有Popup,百度也找不到大佬的现成轮子。<br>
CommunityToolkits 实现的 Popup 有严重的内存泄露问题,本想仿写 CommunityToolkits 源码实现,未果。<br>
问了下通义,发现轮子雏形挺简洁,根本不需要 CommunityToolkits 那一套。<br>
以下是重新封装成 可传出<code>&lt;TResult&gt;</code>的轮子。</p>
<p>Popup包装<br>
PopupPage.xaml.cs<br>
PopupPage.xaml</p>
<p>Popup<br>
Popup.cs</p>
<p>成果<br>
使用示例</p>
<h1 id="popup">Popup</h1>
<h5 id="PopupPage.xaml.cs">PopupPage.xaml.cs </h5>
<pre><code>static class PopupPageExtension
{
        public static Task&lt;Popup&lt;TResult&gt;.PopupResult&gt; ShowAsync&lt;TResult&gt;(this Popup&lt;TResult&gt; popup, Page invoker)
        {
                var page = new PopupPage(popup);
                invoker.Navigation.PushModalAsync(page);
                return page.Popup.WaitResultTask as Task&lt;Popup&lt;TResult&gt;.PopupResult&gt;;
        }
}
/// &lt;summary&gt;
/// 这是包装,请勿食用
/// &lt;/summary&gt;
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 =&gt; 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();
        }
}
</code></pre>
<h5 id="PopupPage.xaml">PopupPage.xaml</h5>
<pre><code>&lt;?xml version="1.0" encoding="utf-8" ?&gt;
&lt;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"
             &gt;
    &lt;Grid
      x:Name="mOverlay"
      BackgroundColor="#80000000"
      Opacity="1"
      &gt;
      &lt;!-- 遮罩层(点击关闭) --&gt;
      &lt;Grid.GestureRecognizers&gt;
            &lt;TapGestureRecognizer Tapped="OnOverlayTapped" /&gt;
      &lt;/Grid.GestureRecognizers&gt;

      &lt;!-- Popup 内容容器 --&gt;
      &lt;Border
            x:Name="mBorder"
            HorizontalOptions="Center"
            VerticalOptions="Center"
            &gt;
            &lt;Label Text="找我有啥事?"/&gt;
      &lt;/Border&gt;
    &lt;/Grid&gt;
&lt;/ContentPage&gt;
</code></pre>
<h5 id="Popup.cs">Popup.cs</h5>
<pre><code>/// &lt;summary&gt;
/// 这是容器,请继承使用。
/// &lt;/summary&gt;
abstract class Popup&lt;TResult&gt; : ContentView, PopupPage.IPopup
{
        public record class PopupResult(bool IsCanceled, TResult Result);
        public TResult Result { get; protected set; }
        /// &lt;summary&gt;
        /// 这个是给 用户 等待用的。
        /// 等待结束时,Popup 大概率已经销毁了。
        /// 所以 ResultTask.Set 只能在 NavigatedFrom 调用。
        /// &lt;/summary&gt;
        readonly TaskCompletionSource&lt;PopupResult&gt; ResultTask = new();
        Task&lt;PopupResult&gt; WaitResult =&gt; ResultTask.Task;

        #region IPopup
        object PopupPage.IPopup.WaitResultTask =&gt; 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);
        }
        /// &lt;summary&gt;
        /// 用户取消
        /// &lt;/summary&gt;
        public async Task CloseAsync() =&gt; await CloseAsync(true);
}
</code></pre>
<h5 id="使用示例">使用示例</h5>
<pre><code>partial class LoginPopup : Popup&lt;UserInfo&gt;
{
        public LoginPopup()
        {
                InitializeComponent();
                // TODO
        }
        public override void OnClose()
        {
                // TODO
        }
}
</code></pre>
<p><img src="https://img2024.cnblogs.com/blog/3530262/202512/3530262-20251224214500825-1024188369.png"></p>
<p>有一个小小的操作bug,很容易解决。😏解决不了问通义</p>


</div>
<div id="MySignature" role="contentinfo">
    <p>本文来自博客园,作者:TomChaos,转载请注明原文链接:https://www.cnblogs.com/TomChaos/p/19394657</p><br><br>
来源:https://www.cnblogs.com/TomChaos/p/19394657
頁: [1]
查看完整版本: [MAUI]简单可食用的Popup<TResult>