|
#nullable disable
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.CompilerServices;
using Microsoft.Maui.Controls.Internals;
namespace Microsoft.Maui.Controls
{
[ContentProperty("Children")]
public abstract class MultiPage<[DynamicallyAccessedMembers(BindableProperty.DeclaringTypeMembers | BindableProperty.ReturnTypeMembers)] T> : Page, IViewContainer<T>, IPageContainer<T>, IItemsView<T>, IMultiPageController<T> where T : Page
{
/// <summary>Bindable property for <see cref="ItemsSource"/>.</summary>
public static readonly BindableProperty ItemsSourceProperty = BindableProperty.Create(nameof(ItemsSource), typeof(IEnumerable), typeof(MultiPage<>), null);
/// <summary>Bindable property for <see cref="ItemTemplate"/>.</summary>
public static readonly BindableProperty ItemTemplateProperty = BindableProperty.Create(nameof(ItemTemplate), typeof(DataTemplate), typeof(MultiPage<>), null);
/// <summary>Bindable property for <see cref="SelectedItem"/>.</summary>
public static readonly BindableProperty SelectedItemProperty = BindableProperty.Create(nameof(SelectedItem), typeof(object), typeof(MultiPage<>), null, BindingMode.TwoWay);
internal static readonly BindableProperty IndexProperty = BindableProperty.Create("Index", typeof(int), typeof(Page), -1);
readonly ElementCollection<T> _children;
readonly TemplatedItemsList<MultiPage<T>, T> _templatedItems;
T _current;
protected MultiPage()
{
_templatedItems = new TemplatedItemsList<MultiPage<T>, T>(this, ItemsSourceProperty, ItemTemplateProperty);
_templatedItems.CollectionChanged += OnTemplatedItemsChanged;
_children = new ElementCollection<T>(InternalChildren);
InternalChildren.CollectionChanged += OnChildrenChanged;
}
public IEnumerable ItemsSource
{
get { return (IEnumerable)GetValue(ItemsSourceProperty); }
set { SetValue(ItemsSourceProperty, value); }
}
public DataTemplate ItemTemplate
{
get { return (DataTemplate)GetValue(ItemTemplateProperty); }
set { SetValue(ItemTemplateProperty, value); }
}
public object SelectedItem
{
get { return GetValue(SelectedItemProperty); }
set { SetValue(SelectedItemProperty, value); }
}
T IItemsView<T>.CreateDefault(object item)
{
return CreateDefault(item);
}
void IItemsView<T>.SetupContent(T content, int index)
{
SetupContent(content, index);
}
void IItemsView<T>.UnhookContent(T content)
{
UnhookContent(content);
}
public T CurrentPage
{
get { return _current; }
set
{
if (_current == value)
return;
var previousPage = _current;
OnPropertyChanging();
// TODO: MAUI refine this to fire earlier
_current?.SendNavigatingFrom(new NavigatingFromEventArgs());
_current = value;
previousPage?.SendDisappearing();
OnPropertyChanged();
OnCurrentPageChanged();
if (HasAppeared)
_current?.SendAppearing();
previousPage?.SendNavigatedFrom(new NavigatedFromEventArgs(_current, NavigationType.PageSwap));
_current?.SendNavigatedTo(new NavigatedToEventArgs(previousPage));
}
}
public IList<T> Children
{
get { return _children; }
}
public event EventHandler CurrentPageChanged;
public event NotifyCollectionChangedEventHandler PagesChanged;
protected abstract T CreateDefault(object item);
protected override bool OnBackButtonPressed()
{
if (CurrentPage != null)
{
bool handled = CurrentPage.SendBackButtonPressed();
if (handled)
return true;
}
return base.OnBackButtonPressed();
}
protected override void OnChildAdded(Element child)
{
base.OnChildAdded(child);
ForceLayout();
}
protected virtual void OnCurrentPageChanged()
{
EventHandler changed = CurrentPageChanged;
if (changed != null)
changed(this, EventArgs.Empty);
}
protected virtual void OnPagesChanged(NotifyCollectionChangedEventArgs e)
=> PagesChanged?.Invoke(this, e);
protected override void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
if (propertyName == ItemsSourceProperty.PropertyName)
_children.IsReadOnly = ItemsSource != null;
else if (propertyName == SelectedItemProperty.PropertyName)
{
UpdateCurrentPage();
}
else if (propertyName == "CurrentPage" && ItemsSource != null)
{
if (CurrentPage == null)
{
SelectedItem = null;
}
else
{
int index = _templatedItems.IndexOf(CurrentPage);
SelectedItem = index != -1 ? _templatedItems.ListProxy[index] : null;
}
}
base.OnPropertyChanged(propertyName);
}
protected virtual void SetupContent(T content, int index)
{
}
protected virtual void UnhookContent(T content)
{
}
[EditorBrowsable(EditorBrowsableState.Never)]
public static int GetIndex(T page)
{
if (page == null)
throw new ArgumentNullException(nameof(page));
return (int)page.GetValue(IndexProperty);
}
[EditorBrowsable(EditorBrowsableState.Never)]
public T GetPageByIndex(int index)
{
foreach (T page in InternalChildren)
{
if (index == GetIndex(page))
return page;
}
return null;
}
[EditorBrowsable(EditorBrowsableState.Never)]
public static void SetIndex(Page page, int index)
{
if (page == null)
throw new ArgumentNullException(nameof(page));
page.SetValue(IndexProperty, index);
}
void OnChildrenChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (Children.IsReadOnly)
return;
var i = 0;
foreach (T page in Children)
SetIndex(page, i++);
OnPagesChanged(e);
if (CurrentPage == null || Children.IndexOf(CurrentPage) == -1)
CurrentPage = Children.FirstOrDefault();
}
void OnTemplatedItemsChanged(object sender, NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
if (e.NewStartingIndex < 0)
goto case NotifyCollectionChangedAction.Reset;
for (int i = e.NewStartingIndex; i < Children.Count; i++)
SetIndex((T)InternalChildren[i], i + e.NewItems.Count);
for (var i = 0; i < e.NewItems.Count; i++)
{
var page = (T)e.NewItems[i];
page.Owned = true;
int index = i + e.NewStartingIndex;
SetIndex(page, index);
InternalChildren.Insert(index, (T)e.NewItems[i]);
}
break;
case NotifyCollectionChangedAction.Remove:
if (e.OldStartingIndex < 0)
goto case NotifyCollectionChangedAction.Reset;
int removeIndex = e.OldStartingIndex;
for (int i = removeIndex + e.OldItems.Count; i < Children.Count; i++)
SetIndex((T)InternalChildren[i], removeIndex++);
for (var i = 0; i < e.OldItems.Count; i++)
{
Element element = InternalChildren[e.OldStartingIndex];
InternalChildren.RemoveAt(e.OldStartingIndex);
element.Owned = false;
}
break;
case NotifyCollectionChangedAction.Move:
if (e.NewStartingIndex < 0 || e.OldStartingIndex < 0)
goto case NotifyCollectionChangedAction.Reset;
if (e.NewStartingIndex == e.OldStartingIndex)
return;
bool movingForward = e.OldStartingIndex < e.NewStartingIndex;
if (movingForward)
{
int moveIndex = e.OldStartingIndex;
for (int i = moveIndex + e.OldItems.Count; i <= e.NewStartingIndex; i++)
SetIndex((T)InternalChildren[i], moveIndex++);
}
else
{
for (var i = 0; i < e.OldStartingIndex - e.NewStartingIndex; i++)
{
var page = (T)InternalChildren[i + e.NewStartingIndex];
SetIndex(page, GetIndex(page) + e.OldItems.Count);
}
}
for (var i = 0; i < e.OldItems.Count; i++)
InternalChildren.RemoveAt(e.OldStartingIndex);
int insertIndex = e.NewStartingIndex;
if (movingForward)
insertIndex -= e.OldItems.Count - 1;
for (var i = 0; i < e.OldItems.Count; i++)
{
var page = (T)e.OldItems[i];
SetIndex(page, insertIndex + i);
InternalChildren.Insert(insertIndex + i, page);
}
break;
case NotifyCollectionChangedAction.Replace:
if (e.OldStartingIndex < 0)
goto case NotifyCollectionChangedAction.Reset;
for (int i = e.OldStartingIndex; i - e.OldStartingIndex < e.OldItems.Count; i++)
{
Element element = InternalChildren[i];
InternalChildren.RemoveAt(i);
element.Owned = false;
T page = _templatedItems.GetOrCreateContent(i, e.NewItems[i - e.OldStartingIndex]);
page.Owned = true;
SetIndex(page, i);
InternalChildren.Insert(i, page);
}
break;
case NotifyCollectionChangedAction.Reset:
Reset();
return;
}
OnPagesChanged(e);
UpdateCurrentPage();
}
void Reset()
{
List<Element> snapshot = InternalChildren.ToList();
InternalChildren.Clear();
foreach (Element element in snapshot)
element.Owned = false;
for (var i = 0; i < _templatedItems.Count; i++)
{
T page = _templatedItems.GetOrCreateContent(i, _templatedItems.ListProxy[i]);
page.Owned = true;
SetIndex(page, i);
InternalChildren.Add(page);
}
var currentNeedsUpdate = true;
BatchBegin();
if (ItemsSource != null)
{
object selected = SelectedItem;
if (selected == null || !ItemsSource.Cast<object>().Contains(selected))
{
SelectedItem = ItemsSource.Cast<object>().FirstOrDefault();
currentNeedsUpdate = false;
}
}
if (currentNeedsUpdate)
UpdateCurrentPage();
OnPagesChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
BatchCommit();
}
void UpdateCurrentPage()
{
if (ItemsSource != null)
{
int index = _templatedItems.ListProxy.IndexOf(SelectedItem);
if (index == -1)
CurrentPage = (T)InternalChildren.FirstOrDefault();
else
CurrentPage = _templatedItems.GetOrCreateContent(index, SelectedItem);
}
else if (SelectedItem is T)
CurrentPage = (T)SelectedItem;
}
}
} |