2using System.Collections.Concurrent;
4using System.Threading.Tasks;
5using Microsoft.Extensions.DependencyInjection;
6using Microsoft.Maui.ApplicationModel;
7using Microsoft.Maui.Controls;
18 private readonly ConcurrentQueue<Func<Task>> taskQueue =
new();
19 private readonly Stack<PopupSession> popupStack =
new();
20 private bool isExecutingQueue;
32 public bool TopIsBlocking => this.popupStack.Count > 0 && this.popupStack.Peek().Options.IsBlocking;
41 foreach (PopupSession Session
in this.popupStack)
42 if (Session.Options.IsBlocking)
52 PopupSession session = await this.Enqueue(async () =>
55 return await this.PrepareAndShowPopupAsync(popupView, popupView.BindingContext, effectiveOptions);
57 await session.Completion.Task;
62 where TPopup : ContentView
63 where TViewModel : class
66 PopupSession session = await this.Enqueue(async () =>
70 popupView.BindingContext = viewModel;
71 return await this.PrepareAndShowPopupAsync(popupView, viewModel, effectiveOptions);
73 await session.Completion.Task;
77 where TPopup : ContentView
78 where TViewModel : class
81 PopupSession session = await this.Enqueue(async () =>
84 popupView.BindingContext = ViewModel;
85 return await this.PrepareAndShowPopupAsync(popupView, ViewModel, effectiveOptions);
87 await session.Completion.Task;
92 where TPopup : ContentView
97 Task<TReturn?> resultTask = viewModel.Result;
98 await this.Enqueue(async () =>
101 popupView.BindingContext = viewModel;
102 await this.PrepareAndShowPopupAsync(popupView, viewModel, effectiveOptions);
104 return await resultTask;
109 where TPopup : ContentView
112 if (ViewModel is
null)
113 throw new ArgumentNullException(nameof(ViewModel));
115 Task<TReturn?> resultTask = ViewModel.Result;
116 await this.Enqueue(async () =>
119 popupView.BindingContext = ViewModel;
120 await this.PrepareAndShowPopupAsync(popupView, ViewModel, effectiveOptions);
122 return await resultTask;
129 throw new ArgumentNullException(nameof(Popup));
131 PopupSession session = await this.Enqueue(async () => await this.PrepareAndShowPopupAsync(Popup, Popup.BindingContext, effectiveOptions));
132 await session.Completion.Task;
138 return this.Enqueue(this.PopTopAsync);
141 private async Task<PopupSession> PrepareAndShowPopupAsync(ContentView popupView,
object? viewModel,
PopupOptions options)
144 await EnsureInitializedAsync(popupView);
145 await EnsureInitializedAsync(viewModel);
150 while (this.popupStack.Count > 0)
151 await this.PopTopAsync();
154 PopupSession session =
new PopupSession(popupView, viewModel, options);
155 ApplyOptionsToPopup(popupView, options);
156 this.popupStack.Push(session);
159 await presenter.ShowPopup(popupView, options.
ShowTransition, CreateVisualState(options));
160 await InvokeAppearingAsync(popupView);
161 await InvokeAppearingAsync(viewModel);
162 this.RaisePopupStackChanged();
166 private async Task PopTopAsync()
168 if (this.popupStack.Count == 0)
170 PopupSession session = this.popupStack.Pop();
171 ContentView popupView = session.View;
172 object? bindingContext = session.BindingContext;
175 await InvokeDisappearingAsync(popupView);
176 await InvokeDisappearingAsync(bindingContext);
178 await basePopupViewModel.NotifyPoppedAsync();
179 IShellPresenter presenter = this.GetPresenter();
180 PopupVisualState? nextVisualState = this.popupStack.Count > 0 ? CreateVisualState(this.popupStack.Peek().Options) : null;
181 await presenter.HidePopup(popupView, session.Options.HideTransition, nextVisualState);
182 await InvokeDisposeAsync(popupView);
183 await InvokeDisposeAsync(bindingContext);
184 if (bindingContext is IAsyncDisposable asyncDisposable)
185 await asyncDisposable.DisposeAsync();
186 else if (bindingContext is IDisposable disposable)
187 disposable.Dispose();
191 ServiceRef.LogService.LogException(ex);
195 session.Completion.TrySetResult(
true);
196 this.RaisePopupStackChanged();
200 private Task<TResult> Enqueue<TResult>(Func<Task<TResult>> action)
202 TaskCompletionSource<TResult> completion =
new(TaskCreationOptions.RunContinuationsAsynchronously);
203 this.taskQueue.Enqueue(async () =>
207 TResult result = await action();
208 completion.TrySetResult(result);
212 completion.TrySetException(ex);
215 this.StartQueueProcessing();
216 return completion.Task;
219 private Task Enqueue(Func<Task> action)
221 TaskCompletionSource<bool> completion =
new(TaskCreationOptions.RunContinuationsAsynchronously);
222 this.taskQueue.Enqueue(async () =>
227 completion.TrySetResult(
true);
231 completion.TrySetException(ex);
234 this.StartQueueProcessing();
235 return completion.Task;
238 private void StartQueueProcessing()
240 if (this.isExecutingQueue)
242 this.isExecutingQueue =
true;
243 MainThread.BeginInvokeOnMainThread(async () => await this.ProcessQueueAsync());
246 private async Task ProcessQueueAsync()
250 while (this.taskQueue.TryDequeue(out Func<Task>? action))
255 ServiceRef.LogService.LogException(ex);
259 this.isExecutingQueue =
false;
263 private IShellPresenter GetPresenter()
265 Page? mainPage = Application.Current?.MainPage;
266 if (mainPage is
null)
267 throw new InvalidOperationException(
"No main page is available for popup presentation.");
268 IShellPresenter? presenter = (mainPage as NavigationPage)?.CurrentPage as IShellPresenter ?? mainPage as IShellPresenter;
269 if (presenter is
null)
270 throw new InvalidOperationException(
"CustomShell presenter not found.");
271 if (!ReferenceEquals(this.attachedPresenter, presenter))
273 this.DetachPresenter();
274 this.attachedPresenter = presenter;
275 presenter.PopupBackgroundTapped += this.OnPresenterPopupBackgroundTapped;
276 presenter.PopupBackRequested += this.OnPresenterPopupBackRequested;
281 private void DetachPresenter()
283 if (this.attachedPresenter is
null)
285 this.attachedPresenter.PopupBackgroundTapped -= this.OnPresenterPopupBackgroundTapped;
286 this.attachedPresenter.PopupBackRequested -= this.OnPresenterPopupBackRequested;
287 this.attachedPresenter =
null;
290 private void OnPresenterPopupBackgroundTapped(
object? sender, EventArgs args)
292 _ = this.Enqueue(async () =>
294 if (this.popupStack.Count == 0)
296 PopupSession session =
this.popupStack.Peek();
297 if (!session.Options.CloseOnBackgroundTap)
299 await
this.PopTopAsync();
303 private void OnPresenterPopupBackRequested(
object? sender, EventArgs args)
305 _ = this.Enqueue(async () =>
307 if (this.popupStack.Count == 0)
309 PopupSession session =
this.popupStack.Peek();
310 if (!session.Options.CloseOnBackButton)
312 await
this.PopTopAsync();
316 private void RaisePopupStackChanged() => this.PopupStackChanged?.Invoke(
this, EventArgs.Empty);
318 private static PopupVisualState CreateVisualState(PopupOptions options)
320 return new PopupVisualState(
321 options.OverlayOpacity,
323 options.CloseOnBackgroundTap,
330 private static void ApplyOptionsToPopup(ContentView popupView, PopupOptions options)
334 basePopup.Placement = options.Placement;
335 basePopup.AnchorPoint = options.AnchorPoint;
336 basePopup.PopupMargin = options.Margin;
337 basePopup.PopupPadding = options.Padding;
338 basePopup.CloseOnBackgroundTap = options.CloseOnBackgroundTap && !options.DisableBackgroundTap;
342 private static async Task EnsureInitializedAsync(
object? target)
348 System.Reflection.PropertyInfo?
property = target.GetType().GetProperty(
"IsInitialized",
System.Reflection.BindingFlags.Instance |
System.Reflection.BindingFlags.Public |
System.Reflection.BindingFlags.NonPublic);
349 bool isInitialized =
false;
350 if (property?.CanRead ==
true)
351 isInitialized = (bool)(property.GetValue(target) ??
false);
354 await lifeCycle.OnInitializeAsync();
355 if (property?.CanWrite ==
true)
356 property.SetValue(target,
true);
361 private static async Task InvokeAppearingAsync(
object? target)
364 await lifeCycle.OnAppearingAsync();
367 private static async Task InvokeDisappearingAsync(
object? target)
370 await lifeCycle.OnDisappearingAsync();
373 private static async Task InvokeDisposeAsync(
object? target)
376 await lifeCycle.OnDisposeAsync();
379 private sealed
class PopupSession
381 public PopupSession(ContentView view,
object? bindingContext, PopupOptions options)
384 this.BindingContext = bindingContext;
385 this.Options = options;
386 this.Completion =
new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
388 public ContentView View {
get; }
389 public object? BindingContext {
get; }
390 public PopupOptions Options {
get; }
391 public TaskCompletionSource<bool> Completion {
get; }
Base class that references services in the app.
static IServiceProvider Provider
The service provider for the app. This is set before the app is started, and will be used to resolve ...
Presenter abstraction host for page, popup and toast transitions.
Interface for views who need to react to life-cycle events.