Neuron®
The Neuron® is the basis for the creation of open and secure federated networks for smart societies.
Loading...
Searching...
No Matches
ViewSwitcher.cs
1using System;
4using System.Collections.ObjectModel;
5using System.Collections.Specialized;
6using System.Diagnostics;
7using System.Globalization;
8using System.Threading;
9using System.Threading.Tasks;
10using System.Windows.Input;
11using Microsoft.Maui.Controls;
12using Microsoft.Maui.Dispatching;
18
20{
21 [ContentProperty(nameof(Views))]
22 public partial class ViewSwitcher : Grid, IDisposable
23 {
24 public static readonly BindableProperty SelectedIndexProperty =
25 BindableProperty.Create(nameof(SelectedIndex), typeof(int), typeof(ViewSwitcher), -1, BindingMode.TwoWay, propertyChanged: OnSelectedIndexPropertyChanged);
26
27 public static readonly BindableProperty SelectedItemProperty =
28 BindableProperty.Create(nameof(SelectedItem), typeof(object), typeof(ViewSwitcher), default(object), BindingMode.TwoWay, propertyChanged: OnSelectedItemPropertyChanged);
29
30 public static readonly BindableProperty SelectedStateKeyProperty =
31 BindableProperty.Create(nameof(SelectedStateKey), typeof(string), typeof(ViewSwitcher), default(string), BindingMode.TwoWay, propertyChanged: OnSelectedStateKeyPropertyChanged);
32
33 public static readonly BindableProperty ItemsSourceProperty =
34 BindableProperty.Create(nameof(ItemsSource), typeof(IEnumerable), typeof(ViewSwitcher), default(IEnumerable), propertyChanged: OnItemsSourcePropertyChanged);
35
36 public static readonly BindableProperty ItemTemplateProperty =
37 BindableProperty.Create(nameof(ItemTemplate), typeof(DataTemplate), typeof(ViewSwitcher), default(DataTemplate), propertyChanged: OnItemTemplatePropertyChanged);
38
39 public static readonly BindableProperty ItemTemplateSelectorProperty =
40 BindableProperty.Create(nameof(ItemTemplateSelector), typeof(DataTemplateSelector), typeof(ViewSwitcher), default(DataTemplateSelector), propertyChanged: OnItemTemplateSelectorPropertyChanged);
41
42 public static readonly BindableProperty CacheViewsProperty =
43 BindableProperty.Create(nameof(CacheViews), typeof(bool), typeof(ViewSwitcher), true, propertyChanged: OnCacheViewsPropertyChanged);
44
45 public static readonly BindableProperty AnimateProperty =
46 BindableProperty.Create(nameof(Animate), typeof(bool), typeof(ViewSwitcher), true, propertyChanged: OnAnimatePropertyChanged);
47
48 public static readonly BindableProperty TransitionDurationProperty =
49 BindableProperty.Create(nameof(TransitionDuration), typeof(uint), typeof(ViewSwitcher), (uint)250, propertyChanged: OnTransitionDurationPropertyChanged);
50
51 public static readonly BindableProperty TransitionEasingProperty =
52 BindableProperty.Create(nameof(TransitionEasing), typeof(Easing), typeof(ViewSwitcher), Easing.Linear, propertyChanged: OnTransitionEasingPropertyChanged);
53
54 public static readonly BindableProperty TransitionProperty =
55 BindableProperty.Create(nameof(Transition), typeof(IViewTransition), typeof(ViewSwitcher), new CrossFadeViewTransition(), propertyChanged: OnTransitionPropertyChanged);
56
57 public static readonly BindableProperty SelectedIndexBehaviorProperty =
58 BindableProperty.Create(nameof(SelectedIndexBehavior), typeof(ViewSwitcherSelectedIndexBehavior), typeof(ViewSwitcher), ViewSwitcherSelectedIndexBehavior.Clamp, propertyChanged: OnSelectedIndexBehaviorPropertyChanged);
59
60 public static readonly BindableProperty AutomationDescriptionTemplateProperty =
61 BindableProperty.Create(nameof(AutomationDescriptionTemplate), typeof(string), typeof(ViewSwitcher), default(string));
62
63 private readonly ObservableCollection<View> views;
64 private readonly ObservableCollection<ViewSwitcherStateView> stateViews;
65 private readonly List<ViewSwitcherItemDescriptor> descriptors;
66 private readonly Dictionary<string, ViewSwitcherItemDescriptor> stateKeyLookup;
67 private readonly ViewSwitcherSelectionState selectionState;
68 private readonly ViewSwitcherViewFactory viewFactory;
69 private readonly ViewSwitcherViewCache viewCache;
70 private readonly Dictionary<View, ViewSwitcherItemDescriptor> viewDescriptorMap;
71 private readonly HashSet<object> initializedLifecycleTargets;
72 private readonly ViewSwitcherTransitionCoordinator transitionCoordinator;
73 private readonly Grid presenter;
74 private readonly Command nextCommand;
75 private readonly Command previousCommand;
76
77 private INotifyCollectionChanged? itemsSourceNotifier;
78 private readonly ObservableTask<int> selectionOperation;
79 private bool disposed;
80 private bool isInitialLoad;
81
82 public ViewSwitcher()
83 {
84 RowDefinition rootRow = new RowDefinition { Height = GridLength.Star };
85 this.RowDefinitions.Add(rootRow);
86
87 this.views = new ObservableCollection<View>();
88 this.views.CollectionChanged += this.OnViewsCollectionChanged;
89
90 this.stateViews = new ObservableCollection<ViewSwitcherStateView>();
91 this.stateViews.CollectionChanged += this.OnStateViewsCollectionChanged;
92
93 this.descriptors = new List<ViewSwitcherItemDescriptor>();
94 this.stateKeyLookup = new Dictionary<string, ViewSwitcherItemDescriptor>(StringComparer.Ordinal);
95
96 this.selectionState = new ViewSwitcherSelectionState
97 {
98 IndexBehavior = this.SelectedIndexBehavior
99 };
100
101 this.viewFactory = new ViewSwitcherViewFactory(this)
102 {
103 ItemTemplate = this.ItemTemplate,
104 ItemTemplateSelector = this.ItemTemplateSelector
105 };
106
107 this.viewCache = new ViewSwitcherViewCache
108 {
109 IsEnabled = this.CacheViews
110 };
111
112 this.viewDescriptorMap = new Dictionary<View, ViewSwitcherItemDescriptor>(ReferenceEqualityComparer.Instance);
113 this.initializedLifecycleTargets = new HashSet<object>(ReferenceEqualityComparer.Instance);
114
115 this.selectionOperation = new ObservableTask<int>();
116
117 this.presenter = new Grid();
119 try
120 {
122 }
123 catch (ArgumentException)
124 {
126 }
127 this.transitionCoordinator = new ViewSwitcherTransitionCoordinator(this.presenter, AnimationCoordinator)
128 {
129 Animate = this.Animate,
130 Duration = this.TransitionDuration,
131 Easing = this.TransitionEasing,
132 Transition = this.Transition
133 };
134 this.Children.Add(this.presenter);
135
136 this.nextCommand = new Command(this.ExecuteNextCommand, this.CanExecuteNextCommand);
137 this.previousCommand = new Command(this.ExecutePreviousCommand, this.CanExecutePreviousCommand);
138
139 this.Loaded += this.OnLoaded;
140 this.Unloaded += this.OnUnloaded;
141
142 this.isInitialLoad = true;
143 }
144
145 public event EventHandler<ViewSwitcherSelectionChangingEventArgs>? SelectionChanging;
146
147 public event EventHandler<ViewSwitcherSelectionChangedEventArgs>? SelectionChanged;
148
149 public int SelectedIndex
150 {
151 get => (int)this.GetValue(SelectedIndexProperty);
152 set => this.SetValue(SelectedIndexProperty, value);
153 }
154
155 public object? SelectedItem
156 {
157 get => this.GetValue(SelectedItemProperty);
158 set => this.SetValue(SelectedItemProperty, value);
159 }
160
161 public string? SelectedStateKey
162 {
163 get => (string?)this.GetValue(SelectedStateKeyProperty);
164 set => this.SetValue(SelectedStateKeyProperty, value);
165 }
166
167 public IEnumerable? ItemsSource
168 {
169 get => (IEnumerable?)this.GetValue(ItemsSourceProperty);
170 set => this.SetValue(ItemsSourceProperty, value);
171 }
172
173 public DataTemplate? ItemTemplate
174 {
175 get => (DataTemplate?)this.GetValue(ItemTemplateProperty);
176 set => this.SetValue(ItemTemplateProperty, value);
177 }
178
179 public DataTemplateSelector? ItemTemplateSelector
180 {
181 get => (DataTemplateSelector?)this.GetValue(ItemTemplateSelectorProperty);
182 set => this.SetValue(ItemTemplateSelectorProperty, value);
183 }
184
185 public bool CacheViews
186 {
187 get => (bool)this.GetValue(CacheViewsProperty);
188 set => this.SetValue(CacheViewsProperty, value);
189 }
190
191 public bool Animate
192 {
193 get => (bool)this.GetValue(AnimateProperty);
194 set => this.SetValue(AnimateProperty, value);
195 }
196
197 public uint TransitionDuration
198 {
199 get => (uint)this.GetValue(TransitionDurationProperty);
200 set => this.SetValue(TransitionDurationProperty, value);
201 }
202
203 public Easing? TransitionEasing
204 {
205 get => (Easing?)this.GetValue(TransitionEasingProperty);
206 set => this.SetValue(TransitionEasingProperty, value);
207 }
208
209 public IViewTransition Transition
210 {
211 get => (IViewTransition)this.GetValue(TransitionProperty);
212 set => this.SetValue(TransitionProperty, value);
213 }
214
215 public ViewSwitcherSelectedIndexBehavior SelectedIndexBehavior
216 {
217 get => (ViewSwitcherSelectedIndexBehavior)this.GetValue(SelectedIndexBehaviorProperty);
218 set => this.SetValue(SelectedIndexBehaviorProperty, value);
219 }
220
221 public string? AutomationDescriptionTemplate
222 {
223 get => (string?)this.GetValue(AutomationDescriptionTemplateProperty);
224 set => this.SetValue(AutomationDescriptionTemplateProperty, value);
225 }
226
227 public ObservableCollection<View> Views => this.views;
228
229 public ObservableCollection<ViewSwitcherStateView> StateViews => this.stateViews;
230
231 public ICommand NextCommand => this.nextCommand;
232
233 public ICommand PreviousCommand => this.previousCommand;
234
235 public ObservableTask<int> SelectionOperation => this.selectionOperation;
236
237 public Task SwitchToAsync(int index, bool animate = true, CancellationToken cancellationToken = default)
238 {
239 Task task = this.RunSelectionTaskAsync(
240 token => this.ApplySelectionAsync(index, null, null, ViewSwitcherSelectionChangeSource.Index, animate, token),
241 cancellationToken);
242 this.ObserveSelectionTask(task);
243 return task;
244 }
245
246 public Task SwitchToStateAsync(string stateKey, bool animate = true, CancellationToken cancellationToken = default)
247 {
248 Task task = this.RunSelectionTaskAsync(
249 token => this.ApplySelectionAsync(null, null, stateKey, ViewSwitcherSelectionChangeSource.StateKey, animate, token),
250 cancellationToken);
251 this.ObserveSelectionTask(task);
252 return task;
253 }
254
255 protected override void OnBindingContextChanged()
256 {
257 base.OnBindingContextChanged();
258
259 object? bindingContext = this.BindingContext;
260 foreach (View view in this.views)
261 {
262 if (!ReferenceEquals(view.BindingContext, bindingContext))
263 {
264 view.BindingContext = bindingContext;
265 }
266 }
267
268 foreach (ViewSwitcherStateView stateView in this.stateViews)
269 {
270 if (stateView.Content is not null && stateView.Content.BindingContext is null)
271 {
272 stateView.Content.BindingContext = bindingContext;
273 }
274 }
275 }
276
277 private static void OnSelectedIndexPropertyChanged(BindableObject bindable, object oldValue, object newValue)
278 {
279 if (bindable is ViewSwitcher viewSwitcher)
280 {
281 int requestedIndex = (int)newValue;
282 viewSwitcher.RequestSelection(ViewSwitcherSelectionChangeSource.Index, requestedIndex, null, null);
283 }
284 }
285
286 private static void OnSelectedItemPropertyChanged(BindableObject bindable, object oldValue, object newValue)
287 {
288 if (bindable is ViewSwitcher viewSwitcher)
289 {
290 viewSwitcher.RequestSelection(ViewSwitcherSelectionChangeSource.Item, null, newValue, null);
291 }
292 }
293
294 private static void OnSelectedStateKeyPropertyChanged(BindableObject bindable, object oldValue, object newValue)
295 {
296 if (bindable is ViewSwitcher viewSwitcher)
297 {
298 string? requestedKey = newValue as string;
299 viewSwitcher.RequestSelection(ViewSwitcherSelectionChangeSource.StateKey, null, null, requestedKey);
300 }
301 }
302
303 private static void OnItemsSourcePropertyChanged(BindableObject bindable, object oldValue, object newValue)
304 {
305 if (bindable is ViewSwitcher viewSwitcher)
306 {
307 viewSwitcher.OnItemsSourceChanged(oldValue as IEnumerable, newValue as IEnumerable);
308 }
309 }
310
311 private static void OnItemTemplatePropertyChanged(BindableObject bindable, object oldValue, object newValue)
312 {
313 if (bindable is ViewSwitcher viewSwitcher)
314 {
315 viewSwitcher.viewFactory.ItemTemplate = newValue as DataTemplate;
316 viewSwitcher.RefreshDescriptors();
317 }
318 }
319
320 private static void OnItemTemplateSelectorPropertyChanged(BindableObject bindable, object oldValue, object newValue)
321 {
322 if (bindable is ViewSwitcher viewSwitcher)
323 {
324 viewSwitcher.viewFactory.ItemTemplateSelector = newValue as DataTemplateSelector;
325 viewSwitcher.RefreshDescriptors();
326 }
327 }
328
329 private static void OnCacheViewsPropertyChanged(BindableObject bindable, object oldValue, object newValue)
330 {
331 if (bindable is ViewSwitcher viewSwitcher)
332 {
333 bool isEnabled = (bool)newValue;
334 viewSwitcher.viewCache.IsEnabled = isEnabled;
335 if (!isEnabled)
336 {
337 viewSwitcher.viewCache.Clear();
338 viewSwitcher.viewDescriptorMap.Clear();
339 }
340 }
341 }
342
343 private static void OnAnimatePropertyChanged(BindableObject bindable, object oldValue, object newValue)
344 {
345 if (bindable is ViewSwitcher viewSwitcher)
346 {
347 viewSwitcher.transitionCoordinator.Animate = (bool)newValue;
348 }
349 }
350
351 private static void OnTransitionDurationPropertyChanged(BindableObject bindable, object oldValue, object newValue)
352 {
353 if (bindable is ViewSwitcher viewSwitcher)
354 {
355 viewSwitcher.transitionCoordinator.Duration = (uint)newValue;
356 }
357 }
358
359 private static void OnTransitionEasingPropertyChanged(BindableObject bindable, object oldValue, object newValue)
360 {
361 if (bindable is ViewSwitcher viewSwitcher)
362 {
363 viewSwitcher.transitionCoordinator.Easing = newValue as Easing;
364 }
365 }
366
367 private static void OnTransitionPropertyChanged(BindableObject bindable, object oldValue, object newValue)
368 {
369 if (bindable is ViewSwitcher viewSwitcher && newValue is IViewTransition transition)
370 {
371 viewSwitcher.transitionCoordinator.Transition = transition;
372 }
373 }
374
375 private static void OnSelectedIndexBehaviorPropertyChanged(BindableObject bindable, object oldValue, object newValue)
376 {
377 if (bindable is ViewSwitcher viewSwitcher && newValue is ViewSwitcherSelectedIndexBehavior behavior)
378 {
379 viewSwitcher.selectionState.IndexBehavior = behavior;
380 viewSwitcher.UpdateNavigationCommands();
381 }
382 }
383
384 private void RequestSelection(ViewSwitcherSelectionChangeSource source, int? index, object? item, string? stateKey)
385 {
386 if (this.selectionState.ShouldIgnore(source))
387 return;
388
389 Task task = this.RunSelectionTaskAsync(
390 token => this.ApplySelectionAsync(index, item, stateKey, source, null, token),
391 CancellationToken.None);
392 this.ObserveSelectionTask(task);
393 }
394
395 private readonly struct SelectionWorkItem
396 {
397 public SelectionWorkItem(Func<CancellationToken, Task> factory, CancellationToken externalToken)
398 {
399 this.Factory = factory ?? throw new ArgumentNullException(nameof(factory));
400 this.ExternalToken = externalToken;
401 }
402
403 public Func<CancellationToken, Task> Factory { get; }
404
405 public CancellationToken ExternalToken { get; }
406 }
407
408 private Task RunSelectionTaskAsync(Func<CancellationToken, Task> taskFactory, CancellationToken externalToken)
409 {
410 SelectionWorkItem workItem = new SelectionWorkItem(taskFactory, externalToken);
411 this.selectionOperation.Run<SelectionWorkItem>(async (item, context) =>
412 {
413 CancellationToken effectiveToken = context.CancellationToken;
414 if (item.ExternalToken.CanBeCanceled)
415 {
416 using CancellationTokenSource linked = CancellationTokenSource.CreateLinkedTokenSource(effectiveToken, item.ExternalToken);
417 effectiveToken = linked.Token;
418 await item.Factory.Invoke(effectiveToken).ConfigureAwait(false);
419 }
420 else
421 {
422 await item.Factory.Invoke(effectiveToken).ConfigureAwait(false);
423 }
424 }, workItem);
425
426 Task? task = this.selectionOperation.CurrentTask;
427 return task ?? Task.CompletedTask;
428 }
429
430 private void ObserveSelectionTask(Task task)
431 {
432 task.ContinueWith(t =>
433 {
434 if (t.Exception is not null)
435 {
436 this.LogException(t.Exception);
437 }
438 }, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously | TaskContinuationOptions.OnlyOnFaulted, TaskScheduler.Default);
439 }
440
441 private async Task ApplySelectionAsync(
442 int? requestedIndex,
443 object? requestedItem,
444 string? requestedStateKey,
445 ViewSwitcherSelectionChangeSource source,
446 bool? animateOverride,
447 CancellationToken cancellationToken)
448 {
449 try
450 {
451 await this.Dispatcher.DispatchAsync(async () =>
452 {
453 // Instead of throwing (causing unhandled exception surfaced to user), abort gracefully if canceled.
454 if (cancellationToken.IsCancellationRequested)
455 {
456 return;
457 }
458
459 this.RefreshDescriptors();
460
461 int itemCount = this.descriptors.Count;
462 int targetIndex = this.ResolveTargetIndex(requestedIndex, requestedItem, requestedStateKey, source, itemCount);
463 int oldIndex = this.selectionState.SelectedIndex;
464 object? oldItem = this.selectionState.SelectedItem;
465 string? oldStateKey = this.selectionState.SelectedStateKey;
466
467 if (targetIndex >= itemCount)
468 {
469 targetIndex = this.selectionState.NormalizeIndex(targetIndex, itemCount);
470 }
471
472 ViewSwitcherItemDescriptor? descriptor = null;
473 object? newItem = null;
474 string? newStateKey = null;
475
476 if (targetIndex >= 0 && targetIndex < itemCount)
477 {
478 descriptor = this.descriptors[targetIndex];
479 newItem = descriptor.Item;
480 newStateKey = descriptor.StateKey;
481 }
482
483 if (itemCount == 0)
484 {
485 this.UpdateNavigationCommands();
486 return;
487 }
488
489 if (descriptor is null)
490 {
491 int FallbackIndex = this.selectionState.SelectedIndex;
492 if (FallbackIndex < 0 || FallbackIndex >= itemCount)
493 {
494 FallbackIndex = 0;
495 }
496
497 descriptor = this.descriptors[FallbackIndex];
498 targetIndex = FallbackIndex;
499 newItem = descriptor.Item;
500 newStateKey = descriptor.StateKey;
501 }
502
504 oldIndex,
505 oldItem,
506 oldStateKey,
507 targetIndex,
508 newItem,
509 newStateKey);
510
511 this.SelectionChanging?.Invoke(this, changingArgs);
512 if (changingArgs.Cancel)
513 {
514 this.RevertSelectionProperties();
515 return;
516 }
517
518 this.selectionState.UpdateSnapshot(targetIndex, newItem, newStateKey);
519
520 using (this.selectionState.BeginUpdate(ViewSwitcherSelectionChangeSource.Index))
521 {
522 this.SelectedIndex = targetIndex;
523 }
524
525 using (this.selectionState.BeginUpdate(ViewSwitcherSelectionChangeSource.Item))
526 {
527 this.SelectedItem = newItem;
528 }
529
530 using (this.selectionState.BeginUpdate(ViewSwitcherSelectionChangeSource.StateKey))
531 {
532 this.SelectedStateKey = newStateKey;
533 }
534
535 this.UpdateAutomationSemantics(newItem, newStateKey);
536 this.UpdateNavigationCommands();
537
538 View? oldViewInstance = this.transitionCoordinator.CurrentView;
539 View? viewToPresent = descriptor is not null ? this.ResolveView(descriptor, targetIndex) : null;
540
541 if (!ReferenceEquals(oldViewInstance, viewToPresent))
542 {
543 await this.InvokeLifecycleDisappearingAsync(oldViewInstance, cancellationToken).ConfigureAwait(false);
544 }
545
546 if (viewToPresent is not null)
547 {
548 await this.EnsureLifecycleInitializedAsync(viewToPresent, cancellationToken).ConfigureAwait(false);
549 }
550
551 bool previousAnimate = this.transitionCoordinator.Animate;
552 uint previousDuration = this.transitionCoordinator.Duration;
553 Easing? previousEasing = this.transitionCoordinator.Easing;
554
555 if (animateOverride.HasValue)
556 {
557 this.transitionCoordinator.Animate = animateOverride.Value;
558 }
559
560 this.transitionCoordinator.Duration = this.TransitionDuration;
561 this.transitionCoordinator.Easing = this.TransitionEasing;
562
563 try
564 {
565 await PolicyRunner.RunAsync(
566 ct => this.transitionCoordinator.SwitchAsync(this, viewToPresent, this.isInitialLoad, ct),
567 cancellationToken).ConfigureAwait(false);
568 if (!ReferenceEquals(oldViewInstance, viewToPresent))
569 {
570 await this.InvokeLifecycleAppearingAsync(viewToPresent, cancellationToken).ConfigureAwait(false);
571 }
572 await this.DisposeLifecycleIfRequiredAsync(oldViewInstance, viewToPresent).ConfigureAwait(false);
573 }
574 finally
575 {
576 this.transitionCoordinator.Animate = previousAnimate;
577 this.transitionCoordinator.Duration = previousDuration;
578 this.transitionCoordinator.Easing = previousEasing;
579 }
580
581 this.isInitialLoad = false;
582 if (cancellationToken.IsCancellationRequested)
583 {
584 return;
585 }
586
588 oldIndex,
589 oldItem,
590 oldStateKey,
591 targetIndex,
592 newItem,
593 newStateKey);
594 this.SelectionChanged?.Invoke(this, changedArgs);
595 }).ConfigureAwait(false);
596 }
597 catch (OperationCanceledException)
598 {
599 // Cancellation is an expected control-flow event when switching views rapidly. Swallow the exception
600 // so the debugger does not surface it as user-unhandled while the cancellation token semantics already
601 // halted the transition inside the dispatcher callback.
602 }
603 }
604
605 private void RevertSelectionProperties()
606 {
607 using (this.selectionState.BeginUpdate(ViewSwitcherSelectionChangeSource.Index))
608 {
609 this.SelectedIndex = this.selectionState.SelectedIndex;
610 }
611
612 using (this.selectionState.BeginUpdate(ViewSwitcherSelectionChangeSource.Item))
613 {
614 this.SelectedItem = this.selectionState.SelectedItem;
615 }
616
617 using (this.selectionState.BeginUpdate(ViewSwitcherSelectionChangeSource.StateKey))
618 {
619 this.SelectedStateKey = this.selectionState.SelectedStateKey;
620 }
621 }
622
623 private int ResolveTargetIndex(
624 int? requestedIndex,
625 object? requestedItem,
626 string? requestedStateKey,
627 ViewSwitcherSelectionChangeSource source,
628 int itemCount)
629 {
630 if (!string.IsNullOrWhiteSpace(requestedStateKey))
631 {
632 if (this.stateKeyLookup.TryGetValue(requestedStateKey, out ViewSwitcherItemDescriptor descriptor))
633 {
634 return descriptor.Index;
635 }
636 return this.selectionState.SelectedIndex;
637 }
638
639 if (requestedItem is not null)
640 {
641 for (int i = 0; i < this.descriptors.Count; i++)
642 {
643 ViewSwitcherItemDescriptor descriptor = this.descriptors[i];
644 if (ReferenceEquals(descriptor.Item, requestedItem))
645 return descriptor.Index;
646 if (descriptor.StateView is not null && ReferenceEquals(descriptor.StateView, requestedItem))
647 return descriptor.Index;
648 }
649
650 return this.selectionState.SelectedIndex;
651 }
652
653 if (requestedIndex.HasValue)
654 {
655 return this.selectionState.NormalizeIndex(requestedIndex.Value, itemCount);
656 }
657
658 return this.selectionState.SelectedIndex;
659 }
660
661 private View? ResolveView(ViewSwitcherItemDescriptor descriptor, int index)
662 {
663 if (!string.IsNullOrWhiteSpace(descriptor.StateKey))
664 {
665 if (this.viewCache.TryGetByStateKey(descriptor.StateKey!, out View? cachedByKey))
666 {
667 this.ApplyBindingContext(cachedByKey, descriptor);
668 this.viewDescriptorMap[cachedByKey] = descriptor;
669 return cachedByKey;
670 }
671 }
672
673 if (this.viewCache.TryGetByIndex(index, out View? cachedByIndex))
674 {
675 this.ApplyBindingContext(cachedByIndex, descriptor);
676 this.viewDescriptorMap[cachedByIndex] = descriptor;
677 return cachedByIndex;
678 }
679
680 View view = this.viewFactory.CreateView(descriptor);
681 this.ApplyBindingContext(view, descriptor);
682 this.viewDescriptorMap[view] = descriptor;
683
684 if (this.viewCache.IsEnabled)
685 {
686 if (!string.IsNullOrWhiteSpace(descriptor.StateKey))
687 {
688 this.viewCache.StoreByStateKey(descriptor.StateKey!, view);
689 }
690 else
691 {
692 this.viewCache.StoreByIndex(index, view);
693 }
694 }
695
696 return view;
697 }
698
699 private void ApplyBindingContext(View view, ViewSwitcherItemDescriptor descriptor)
700 {
701 if (descriptor.StateView is not null)
702 {
703 if (descriptor.StateView.ViewModelType is not null)
704 return;
705
706 if (descriptor.StateView.Content is not null && descriptor.StateView.Content.BindingContext is not null)
707 return;
708 }
709
710 if (descriptor.Item is not null && descriptor.Item is not View)
711 {
712 view.BindingContext = descriptor.Item;
713 return;
714 }
715
716 if (view.BindingContext is null)
717 {
718 view.BindingContext = this.BindingContext;
719 }
720 }
721
722 private void RefreshDescriptors()
723 {
724 this.descriptors.Clear();
725 this.stateKeyLookup.Clear();
726
727 if (this.stateViews.Count > 0)
728 {
729 int index = 0;
730 foreach (ViewSwitcherStateView stateView in this.stateViews)
731 {
732 ViewSwitcherItemDescriptor descriptor = new ViewSwitcherItemDescriptor
733 {
734 Index = index,
735 StateView = stateView,
736 StateKey = stateView.StateKey,
737 InlineView = stateView.Content,
738 Item = stateView
739 };
740 this.descriptors.Add(descriptor);
741 if (!string.IsNullOrWhiteSpace(stateView.StateKey))
742 {
743 this.stateKeyLookup[stateView.StateKey] = descriptor;
744 }
745 index++;
746 }
747 return;
748 }
749
750 if (this.ItemsSource is not null)
751 {
752 int index = 0;
753 foreach (object? item in this.ItemsSource)
754 {
755 ViewSwitcherItemDescriptor descriptor = new ViewSwitcherItemDescriptor
756 {
757 Index = index,
758 Item = item,
759 InlineView = item as View
760 };
761 this.descriptors.Add(descriptor);
762 index++;
763 }
764 return;
765 }
766
767 if (this.views.Count > 0)
768 {
769 int index = 0;
770 foreach (View view in this.views)
771 {
772 ViewSwitcherItemDescriptor descriptor = new ViewSwitcherItemDescriptor
773 {
774 Index = index,
775 Item = view,
776 InlineView = view
777 };
778 this.descriptors.Add(descriptor);
779 index++;
780 }
781 }
782 }
783
784 private void OnViewsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
785 {
786 this.RefreshDescriptors();
787 this.viewCache.Clear();
788 this.viewDescriptorMap.Clear();
789 this.RequestSelection(ViewSwitcherSelectionChangeSource.Index, this.SelectedIndex, null, null);
790 }
791
792 private void OnStateViewsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
793 {
794 this.RefreshDescriptors();
795 this.viewCache.Clear();
796 this.viewDescriptorMap.Clear();
797 this.RequestSelection(ViewSwitcherSelectionChangeSource.StateKey, null, null, this.SelectedStateKey);
798 }
799
800 private void OnItemsSourceChanged(IEnumerable? oldSource, IEnumerable? newSource)
801 {
802 if (this.itemsSourceNotifier is not null)
803 {
804 this.itemsSourceNotifier.CollectionChanged -= this.OnItemsSourceCollectionChanged;
805 this.itemsSourceNotifier = null;
806 }
807
808 if (oldSource != newSource && newSource is INotifyCollectionChanged notifier)
809 {
810 this.itemsSourceNotifier = notifier;
811 notifier.CollectionChanged += this.OnItemsSourceCollectionChanged;
812 }
813
814 this.RefreshDescriptors();
815 this.viewCache.Clear();
816 this.viewDescriptorMap.Clear();
817 this.RequestSelection(ViewSwitcherSelectionChangeSource.Index, this.SelectedIndex, null, null);
818 }
819
820 private void OnItemsSourceCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
821 {
822 this.RefreshDescriptors();
823 this.viewCache.Clear();
824 this.viewDescriptorMap.Clear();
825 this.RequestSelection(ViewSwitcherSelectionChangeSource.Index, this.SelectedIndex, null, null);
826 }
827
828 private void OnLoaded(object? sender, EventArgs e)
829 {
830 if (this.isInitialLoad)
831 {
832 this.RequestSelection(ViewSwitcherSelectionChangeSource.Index, this.SelectedIndex, null, null);
833 }
834 }
835
836 private void OnUnloaded(object? sender, EventArgs e)
837 {
838 }
839
844 public Task NotifyHostAppearingAsync(CancellationToken cancellationToken = default)
845 {
846 View? currentView = this.transitionCoordinator.CurrentView;
847 return this.InvokeLifecycleAppearingAsync(currentView, cancellationToken);
848 }
849
854 public Task NotifyHostDisappearingAsync(CancellationToken cancellationToken = default)
855 {
856 View? currentView = this.transitionCoordinator.CurrentView;
857 return this.InvokeLifecycleDisappearingAsync(currentView, cancellationToken);
858 }
859
860 private bool CanExecuteNextCommand()
861 {
862 if (this.descriptors.Count == 0)
863 return false;
864
865 if (this.selectionState.IndexBehavior == ViewSwitcherSelectedIndexBehavior.Wrap)
866 return true;
867
868 return this.selectionState.SelectedIndex < this.descriptors.Count - 1;
869 }
870
871 private bool CanExecutePreviousCommand()
872 {
873 if (this.descriptors.Count == 0)
874 return false;
875
876 if (this.selectionState.IndexBehavior == ViewSwitcherSelectedIndexBehavior.Wrap)
877 return true;
878
879 return this.selectionState.SelectedIndex > 0;
880 }
881
882 private void ExecuteNextCommand()
883 {
884 int currentIndex = this.selectionState.SelectedIndex;
885 int targetIndex = currentIndex >= 0 ? currentIndex + 1 : 0;
886 this.ObserveSelectionTask(this.SwitchToAsync(targetIndex));
887 }
888
889 private void ExecutePreviousCommand()
890 {
891 int currentIndex = this.selectionState.SelectedIndex;
892 int targetIndex = currentIndex > 0 ? currentIndex - 1 : -1;
893 this.ObserveSelectionTask(this.SwitchToAsync(targetIndex));
894 }
895
896 private void UpdateNavigationCommands()
897 {
898 this.nextCommand.ChangeCanExecute();
899 this.previousCommand.ChangeCanExecute();
900 }
901
902 private void UpdateAutomationSemantics(object? item, string? stateKey)
903 {
904 string? template = this.AutomationDescriptionTemplate;
905 if (string.IsNullOrWhiteSpace(template))
906 {
907 SemanticProperties.SetDescription(this, null);
908 return;
909 }
910
911 string placeholder = stateKey ?? item?.ToString() ?? string.Empty;
912 string description;
913 if (template.Contains("{0}", StringComparison.Ordinal))
914 {
915 description = template.Replace("{0}", placeholder, StringComparison.Ordinal);
916 }
917 else
918 {
919 description = string.Format(CultureInfo.CurrentCulture, template, placeholder);
920 }
921
922 SemanticProperties.SetDescription(this, description);
923 }
924
925 private async Task EnsureLifecycleInitializedAsync(View? view, CancellationToken token)
926 {
927 if (view is null)
928 return;
929
930 await this.EnsureLifecycleInitializedCoreAsync(view, token).ConfigureAwait(false);
931 object? bindingContext = view.BindingContext;
932 if (bindingContext is not null && !ReferenceEquals(bindingContext, view))
933 {
934 await this.EnsureLifecycleInitializedCoreAsync(bindingContext, token).ConfigureAwait(false);
935 }
936 }
937
938 private async Task EnsureLifecycleInitializedCoreAsync(object target, CancellationToken token)
939 {
940 if (target is not ILifeCycleView lifecycleView)
941 return;
942
943 if (this.initializedLifecycleTargets.Contains(target))
944 return;
945
946 await PolicyRunner.RunAsync(_ => lifecycleView.OnInitializeAsync(), token).ConfigureAwait(false);
947 this.initializedLifecycleTargets.Add(target);
948 }
949
950 private async Task InvokeLifecycleAppearingAsync(View? view, CancellationToken token)
951 {
952 if (view is null)
953 return;
954
955 await this.InvokeLifecycleAppearingCoreAsync(view, token).ConfigureAwait(false);
956 object? bindingContext = view.BindingContext;
957 if (bindingContext is not null && !ReferenceEquals(bindingContext, view))
958 {
959 await this.InvokeLifecycleAppearingCoreAsync(bindingContext, token).ConfigureAwait(false);
960 }
961 }
962
963 private Task InvokeLifecycleAppearingCoreAsync(object target, CancellationToken token)
964 {
965 if (target is not ILifeCycleView lifecycleView)
966 return Task.CompletedTask;
967
968 return PolicyRunner.RunAsync(_ => lifecycleView.OnAppearingAsync(), token);
969 }
970
971 private async Task InvokeLifecycleDisappearingAsync(View? view, CancellationToken token)
972 {
973 if (view is null)
974 return;
975
976 await this.InvokeLifecycleDisappearingCoreAsync(view, token).ConfigureAwait(false);
977 object? bindingContext = view.BindingContext;
978 if (bindingContext is not null && !ReferenceEquals(bindingContext, view))
979 {
980 await this.InvokeLifecycleDisappearingCoreAsync(bindingContext, token).ConfigureAwait(false);
981 }
982 }
983
984 private Task InvokeLifecycleDisappearingCoreAsync(object target, CancellationToken token)
985 {
986 if (target is not ILifeCycleView lifecycleView)
987 return Task.CompletedTask;
988
989 return PolicyRunner.RunAsync(_ => lifecycleView.OnDisappearingAsync(), token);
990 }
991
992 private async Task InvokeLifecycleDisposeAsync(View? view)
993 {
994 if (view is null)
995 return;
996
997 await this.InvokeLifecycleDisposeCoreAsync(view).ConfigureAwait(false);
998 object? bindingContext = view.BindingContext;
999 if (bindingContext is not null && !ReferenceEquals(bindingContext, view))
1000 {
1001 await this.InvokeLifecycleDisposeCoreAsync(bindingContext).ConfigureAwait(false);
1002 }
1003 }
1004
1005 private async Task InvokeLifecycleDisposeCoreAsync(object target)
1006 {
1007 if (target is not ILifeCycleView lifecycleView)
1008 return;
1009
1010 await PolicyRunner.RunAsync(_ => lifecycleView.OnDisposeAsync(), CancellationToken.None).ConfigureAwait(false);
1011 this.initializedLifecycleTargets.Remove(target);
1012 }
1013
1014 private async Task DisposeLifecycleIfRequiredAsync(View? oldView, View? newView)
1015 {
1016 if (oldView is null)
1017 return;
1018
1019 if (ReferenceEquals(oldView, newView))
1020 return;
1021
1022 if (!this.ShouldDisposeView(oldView))
1023 return;
1024
1025 await this.InvokeLifecycleDisposeAsync(oldView).ConfigureAwait(false);
1026 this.viewDescriptorMap.Remove(oldView);
1027 }
1028
1029 private bool ShouldDisposeView(View view)
1030 {
1031 if (this.viewCache.IsEnabled)
1032 return false;
1033
1034 if (this.viewDescriptorMap.TryGetValue(view, out ViewSwitcherItemDescriptor descriptor))
1035 {
1036 if (descriptor.InlineView is not null && ReferenceEquals(descriptor.InlineView, view) && this.views.Contains(view))
1037 return false;
1038
1039 if (descriptor.StateView is not null && ReferenceEquals(descriptor.StateView.Content, view))
1040 return false;
1041 }
1042
1043 return true;
1044 }
1045
1046 private void LogException(Exception exception)
1047 {
1048 try
1049 {
1050 ServiceRef.LogService.LogException(exception);
1051 }
1052 catch (Exception ex)
1053 {
1054 Debug.WriteLine(ex);
1055 }
1056 }
1057
1058 public void Dispose()
1059 {
1060 this.Dispose(true);
1061 GC.SuppressFinalize(this);
1062 }
1063
1064 protected virtual void Dispose(bool disposing)
1065 {
1066 if (this.disposed)
1067 return;
1068
1069 if (disposing)
1070 {
1071 this.Loaded -= this.OnLoaded;
1072 this.Unloaded -= this.OnUnloaded;
1073
1074 this.views.CollectionChanged -= this.OnViewsCollectionChanged;
1075 this.stateViews.CollectionChanged -= this.OnStateViewsCollectionChanged;
1076
1077 if (this.itemsSourceNotifier is not null)
1078 {
1079 this.itemsSourceNotifier.CollectionChanged -= this.OnItemsSourceCollectionChanged;
1080 this.itemsSourceNotifier = null;
1081 }
1082
1083 this.transitionCoordinator.Dispose();
1084 this.selectionOperation.Dispose();
1085 this.viewDescriptorMap.Clear();
1086 this.initializedLifecycleTargets.Clear();
1087 }
1088
1089 this.disposed = true;
1090 }
1091 }
1092
1093 public class ViewSwitcherItem : ContentView
1094 {
1095 }
1096}
Default implementation of IAnimationCoordinator.
Executes async operations through a pipeline of policies (timeout, retry, bulkhead,...
Definition: PolicyRunner.cs:14
Base class that references services in the app.
Definition: ServiceRef.cs:43
static ILogService LogService
Log service.
Definition: ServiceRef.cs:214
Task NotifyHostAppearingAsync(CancellationToken cancellationToken=default)
Notifies the view switcher that the hosting page is appearing. Triggers lifecycle callbacks on the cu...
Task NotifyHostDisappearingAsync(CancellationToken cancellationToken=default)
Notifies the view switcher that the hosting page is disappearing. Triggers lifecycle callbacks on the...
bool Cancel
Gets or sets a value indicating whether the selection change should be canceled.
Provides a data-binding friendly mechanism to manage and report the status of asynchronous operations...
Coordinates animation execution across the application.
Defines a strategy used by ViewSwitcher to animate between two views.
Interface for views who need to react to life-cycle events.
Definition: ImplTypes.g.cs:58
ViewSwitcherSelectedIndexBehavior
Determines how ViewSwitcher handles attempts to set a selection outside the available range.