Neuron®
The Neuron® is the basis for the creation of open and secure federated networks for smart societies.
Loading...
Searching...
No Matches
PopupService.cs
1using System;
2using System.Collections.Concurrent;
4using System.Threading.Tasks;
5using Microsoft.Extensions.DependencyInjection;
6using Microsoft.Maui.ApplicationModel;
7using Microsoft.Maui.Controls;
11
13{
15 [Singleton]
17 {
18 private readonly ConcurrentQueue<Func<Task>> taskQueue = new();
19 private readonly Stack<PopupSession> popupStack = new();
20 private bool isExecutingQueue;
21 private IShellPresenter? attachedPresenter;
22
24 public event EventHandler? PopupStackChanged;
25
27 public bool HasOpenPopups => this.popupStack.Count > 0;
28
32 public bool TopIsBlocking => this.popupStack.Count > 0 && this.popupStack.Peek().Options.IsBlocking;
33
38 {
39 get
40 {
41 foreach (PopupSession Session in this.popupStack)
42 if (Session.Options.IsBlocking)
43 return true;
44 return false;
45 }
46 }
47
49 public async Task PushAsync<TPopup>(PopupOptions? Options = null) where TPopup : ContentView
50 {
51 PopupOptions effectiveOptions = Options ?? new PopupOptions();
52 PopupSession session = await this.Enqueue(async () =>
53 {
54 TPopup popupView = ServiceRef.Provider.GetRequiredService<TPopup>();
55 return await this.PrepareAndShowPopupAsync(popupView, popupView.BindingContext, effectiveOptions);
56 });
57 await session.Completion.Task;
58 }
59
61 public async Task PushAsync<TPopup, TViewModel>(PopupOptions? Options = null)
62 where TPopup : ContentView
63 where TViewModel : class
64 {
65 PopupOptions effectiveOptions = Options ?? new PopupOptions();
66 PopupSession session = await this.Enqueue(async () =>
67 {
68 TPopup popupView = ServiceRef.Provider.GetRequiredService<TPopup>();
69 TViewModel viewModel = ServiceRef.Provider.GetRequiredService<TViewModel>();
70 popupView.BindingContext = viewModel;
71 return await this.PrepareAndShowPopupAsync(popupView, viewModel, effectiveOptions);
72 });
73 await session.Completion.Task;
74 }
75
76 public async Task PushAsync<TPopup, TViewModel>(TViewModel ViewModel, PopupOptions? Options = null)
77 where TPopup : ContentView
78 where TViewModel : class
79 {
80 PopupOptions effectiveOptions = Options ?? new PopupOptions();
81 PopupSession session = await this.Enqueue(async () =>
82 {
83 TPopup popupView = ServiceRef.Provider.GetRequiredService<TPopup>();
84 popupView.BindingContext = ViewModel;
85 return await this.PrepareAndShowPopupAsync(popupView, ViewModel, effectiveOptions);
86 });
87 await session.Completion.Task;
88 }
89
91 public async Task<TReturn?> PushAsync<TPopup, TViewModel, TReturn>(PopupOptions? Options = null)
92 where TPopup : ContentView
93 where TViewModel : ReturningPopupViewModel<TReturn>
94 {
95 PopupOptions effectiveOptions = Options ?? new PopupOptions();
96 TViewModel viewModel = ServiceRef.Provider.GetRequiredService<TViewModel>();
97 Task<TReturn?> resultTask = viewModel.Result;
98 await this.Enqueue(async () =>
99 {
100 TPopup popupView = ServiceRef.Provider.GetRequiredService<TPopup>();
101 popupView.BindingContext = viewModel;
102 await this.PrepareAndShowPopupAsync(popupView, viewModel, effectiveOptions);
103 });
104 return await resultTask;
105 }
106
108 public async Task<TReturn?> PushAsync<TPopup, TViewModel, TReturn>(TViewModel ViewModel, PopupOptions? Options = null)
109 where TPopup : ContentView
110 where TViewModel : ReturningPopupViewModel<TReturn>
111 {
112 if (ViewModel is null)
113 throw new ArgumentNullException(nameof(ViewModel));
114 PopupOptions effectiveOptions = Options ?? new PopupOptions();
115 Task<TReturn?> resultTask = ViewModel.Result;
116 await this.Enqueue(async () =>
117 {
118 TPopup popupView = ServiceRef.Provider.GetRequiredService<TPopup>();
119 popupView.BindingContext = ViewModel;
120 await this.PrepareAndShowPopupAsync(popupView, ViewModel, effectiveOptions);
121 });
122 return await resultTask;
123 }
124
126 public async Task PushAsync(ContentView Popup, PopupOptions? Options = null)
127 {
128 if (Popup is null)
129 throw new ArgumentNullException(nameof(Popup));
130 PopupOptions effectiveOptions = Options ?? new PopupOptions();
131 PopupSession session = await this.Enqueue(async () => await this.PrepareAndShowPopupAsync(Popup, Popup.BindingContext, effectiveOptions));
132 await session.Completion.Task;
133 }
134
136 public Task PopAsync()
137 {
138 return this.Enqueue(this.PopTopAsync);
139 }
140
141 private async Task<PopupSession> PrepareAndShowPopupAsync(ContentView popupView, object? viewModel, PopupOptions options)
142 {
143 options.Normalize();
144 await EnsureInitializedAsync(popupView);
145 await EnsureInitializedAsync(viewModel);
146
147 if (options.IsBlocking)
148 {
149 // Remove all non-blocking popups. If a blocking popup already exists, remove it to enforce single modal semantics.
150 while (this.popupStack.Count > 0)
151 await this.PopTopAsync();
152 }
153
154 PopupSession session = new PopupSession(popupView, viewModel, options);
155 ApplyOptionsToPopup(popupView, options);
156 this.popupStack.Push(session);
157
158 IShellPresenter presenter = this.GetPresenter();
159 await presenter.ShowPopup(popupView, options.ShowTransition, CreateVisualState(options));
160 await InvokeAppearingAsync(popupView);
161 await InvokeAppearingAsync(viewModel);
162 this.RaisePopupStackChanged();
163 return session;
164 }
165
166 private async Task PopTopAsync()
167 {
168 if (this.popupStack.Count == 0)
169 return;
170 PopupSession session = this.popupStack.Pop();
171 ContentView popupView = session.View;
172 object? bindingContext = session.BindingContext;
173 try
174 {
175 await InvokeDisappearingAsync(popupView);
176 await InvokeDisappearingAsync(bindingContext);
177 if (bindingContext is BasePopupViewModel basePopupViewModel)
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();
188 }
189 catch (Exception ex)
190 {
191 ServiceRef.LogService.LogException(ex);
192 }
193 finally
194 {
195 session.Completion.TrySetResult(true);
196 this.RaisePopupStackChanged();
197 }
198 }
199
200 private Task<TResult> Enqueue<TResult>(Func<Task<TResult>> action)
201 {
202 TaskCompletionSource<TResult> completion = new(TaskCreationOptions.RunContinuationsAsynchronously);
203 this.taskQueue.Enqueue(async () =>
204 {
205 try
206 {
207 TResult result = await action();
208 completion.TrySetResult(result);
209 }
210 catch (Exception ex)
211 {
212 completion.TrySetException(ex);
213 }
214 });
215 this.StartQueueProcessing();
216 return completion.Task;
217 }
218
219 private Task Enqueue(Func<Task> action)
220 {
221 TaskCompletionSource<bool> completion = new(TaskCreationOptions.RunContinuationsAsynchronously);
222 this.taskQueue.Enqueue(async () =>
223 {
224 try
225 {
226 await action();
227 completion.TrySetResult(true);
228 }
229 catch (Exception ex)
230 {
231 completion.TrySetException(ex);
232 }
233 });
234 this.StartQueueProcessing();
235 return completion.Task;
236 }
237
238 private void StartQueueProcessing()
239 {
240 if (this.isExecutingQueue)
241 return;
242 this.isExecutingQueue = true;
243 MainThread.BeginInvokeOnMainThread(async () => await this.ProcessQueueAsync());
244 }
245
246 private async Task ProcessQueueAsync()
247 {
248 try
249 {
250 while (this.taskQueue.TryDequeue(out Func<Task>? action))
251 await action();
252 }
253 catch (Exception ex)
254 {
255 ServiceRef.LogService.LogException(ex);
256 }
257 finally
258 {
259 this.isExecutingQueue = false;
260 }
261 }
262
263 private IShellPresenter GetPresenter()
264 {
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))
272 {
273 this.DetachPresenter();
274 this.attachedPresenter = presenter;
275 presenter.PopupBackgroundTapped += this.OnPresenterPopupBackgroundTapped;
276 presenter.PopupBackRequested += this.OnPresenterPopupBackRequested;
277 }
278 return presenter;
279 }
280
281 private void DetachPresenter()
282 {
283 if (this.attachedPresenter is null)
284 return;
285 this.attachedPresenter.PopupBackgroundTapped -= this.OnPresenterPopupBackgroundTapped;
286 this.attachedPresenter.PopupBackRequested -= this.OnPresenterPopupBackRequested;
287 this.attachedPresenter = null;
288 }
289
290 private void OnPresenterPopupBackgroundTapped(object? sender, EventArgs args)
291 {
292 _ = this.Enqueue(async () =>
293 {
294 if (this.popupStack.Count == 0)
295 return;
296 PopupSession session = this.popupStack.Peek();
297 if (!session.Options.CloseOnBackgroundTap)
298 return;
299 await this.PopTopAsync();
300 });
301 }
302
303 private void OnPresenterPopupBackRequested(object? sender, EventArgs args)
304 {
305 _ = this.Enqueue(async () =>
306 {
307 if (this.popupStack.Count == 0)
308 return;
309 PopupSession session = this.popupStack.Peek();
310 if (!session.Options.CloseOnBackButton)
311 return;
312 await this.PopTopAsync();
313 });
314 }
315
316 private void RaisePopupStackChanged() => this.PopupStackChanged?.Invoke(this, EventArgs.Empty);
317
318 private static PopupVisualState CreateVisualState(PopupOptions options)
319 {
320 return new PopupVisualState(
321 options.OverlayOpacity,
322 options.IsBlocking,
323 options.CloseOnBackgroundTap,
324 options.Placement,
325 options.AnchorPoint,
326 options.Margin,
327 options.Padding);
328 }
329
330 private static void ApplyOptionsToPopup(ContentView popupView, PopupOptions options)
331 {
332 if (popupView is BasePopupView basePopup)
333 {
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;
339 }
340 }
341
342 private static async Task EnsureInitializedAsync(object? target)
343 {
344 if (target is null)
345 return;
346 if (target is ILifeCycleView lifeCycle)
347 {
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);
352 if (!isInitialized)
353 {
354 await lifeCycle.OnInitializeAsync();
355 if (property?.CanWrite == true)
356 property.SetValue(target, true);
357 }
358 }
359 }
360
361 private static async Task InvokeAppearingAsync(object? target)
362 {
363 if (target is ILifeCycleView lifeCycle)
364 await lifeCycle.OnAppearingAsync();
365 }
366
367 private static async Task InvokeDisappearingAsync(object? target)
368 {
369 if (target is ILifeCycleView lifeCycle)
370 await lifeCycle.OnDisappearingAsync();
371 }
372
373 private static async Task InvokeDisposeAsync(object? target)
374 {
375 if (target is ILifeCycleView lifeCycle)
376 await lifeCycle.OnDisposeAsync();
377 }
378
379 private sealed class PopupSession
380 {
381 public PopupSession(ContentView view, object? bindingContext, PopupOptions options)
382 {
383 this.View = view;
384 this.BindingContext = bindingContext;
385 this.Options = options;
386 this.Completion = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
387 }
388 public ContentView View { get; }
389 public object? BindingContext { get; }
390 public PopupOptions Options { get; }
391 public TaskCompletionSource<bool> Completion { get; }
392 }
393 }
394}
Base class that references services in the app.
Definition: ServiceRef.cs:43
static IServiceProvider Provider
The service provider for the app. This is set before the app is started, and will be used to resolve ...
Definition: ServiceRef.cs:48
Options controlling popup presentation and behavior.
Definition: PopupOptions.cs:11
PopupTransition ShowTransition
Gets or sets the transition used when the popup is shown.
Definition: PopupOptions.cs:17
bool IsBlocking
If true, popup behaves as a blocking modal (single logical layer). Background taps are disabled unles...
Definition: PopupOptions.cs:57
void Normalize()
Normalizes option combinations enforcing blocking semantics.
Definition: PopupOptions.cs:87
Task PopAsync()
Dismisses the top-most popup if present.
bool HasOpenPopups
Returns true if at least one popup is currently presented.
Definition: PopupService.cs:27
bool ContainsBlockingPopup
Returns true if any blocking popup exists in the stack.
Definition: PopupService.cs:38
async Task PushAsync< TPopup, TViewModel >(PopupOptions? Options=null)
Pushes a popup view resolved from dependency injection together with its view model.
Definition: PopupService.cs:61
bool TopIsBlocking
Gets true if the top popup is blocking.
Definition: PopupService.cs:32
async Task PushAsync< TPopup >(PopupOptions? Options=null)
Pushes a popup view resolved from dependency injection.
Definition: PopupService.cs:49
async Task< TReturn?> PushAsync< TPopup, TViewModel, TReturn >(PopupOptions? Options=null)
Pushes a popup view resolved from dependency injection that returns a value when closed....
Definition: PopupService.cs:91
async Task PushAsync(ContentView Popup, PopupOptions? Options=null)
Pushes an already created popup instance.
Flexible popup surface capable of positioning its content in different regions of the screen.
Base class for popup view models/>.
Presenter abstraction host for page, popup and toast transitions.
Service responsible for presenting and dismissing application popups.
Interface for views who need to react to life-cycle events.
Definition: ImplTypes.g.cs:58