Files
ShrlAlgoToolkit/WPFluent/Controls/NavigationView/NavigationView.Navigation.cs
2025-04-24 20:56:44 +08:00

538 lines
15 KiB
C#

// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT.
// Copyright (C) Leszek Pomianowski and WPF UI Contributors.
// All Rights Reserved.
/* Based on Windows UI Library */
using WPFluent.Abstractions;
using System.Collections.ObjectModel;
using System.Diagnostics;
// ReSharper disable once CheckNamespace
namespace WPFluent.Controls;
/// <content>
/// Defines navigation logic and state management for <see cref="NavigationView"/>.
/// </content>
public partial class NavigationView
{
protected List<string> Journal { get; } = new(50);
protected ObservableCollection<INavigationViewItem> NavigationStack { get; } = [];
private readonly NavigationCache _cache = new();
private readonly Dictionary<
INavigationViewItem,
List<INavigationViewItem?[]>
> _complexNavigationStackHistory = [];
private IServiceProvider? _serviceProvider;
private INavigationViewPageProvider? _pageService;
private int _currentIndexInJournal;
/// <inheritdoc />
public bool CanGoBack => Journal.Count > 1 && _currentIndexInJournal >= 0;
/// <inheritdoc />
public void SetPageProviderService(INavigationViewPageProvider navigationViewPageProvider) =>
_pageService = navigationViewPageProvider;
/// <inheritdoc />
public void SetServiceProvider(IServiceProvider serviceProvider) => _serviceProvider = serviceProvider;
/// <inheritdoc />
public virtual bool Navigate(Type pageType, object? dataContext = null)
{
if (
PageTypeNavigationViewsDictionary.TryGetValue(
pageType,
out var navigationViewItem
)
)
{
return NavigateInternal(navigationViewItem, dataContext);
}
return TryToNavigateWithoutINavigationViewItem(pageType, false, dataContext);
}
/// <inheritdoc />
public virtual bool Navigate(string pageIdOrTargetTag, object? dataContext = null)
{
if (
PageIdOrTargetTagNavigationViewsDictionary.TryGetValue(
pageIdOrTargetTag,
out var navigationViewItem
)
)
{
return NavigateInternal(navigationViewItem, dataContext);
}
return false;
}
/// <inheritdoc />
public virtual bool NavigateWithHierarchy(Type pageType, object? dataContext = null)
{
if (
PageTypeNavigationViewsDictionary.TryGetValue(
pageType,
out var navigationViewItem
)
)
{
return NavigateInternal(navigationViewItem, dataContext, true);
}
return TryToNavigateWithoutINavigationViewItem(pageType, true, dataContext);
}
/// <inheritdoc />
public virtual bool ReplaceContent(Type? pageTypeToEmbed)
{
if (pageTypeToEmbed == null)
{
return false;
}
if (_serviceProvider != null)
{
UpdateContent(_serviceProvider.GetService(pageTypeToEmbed));
return true;
}
if (_pageService == null)
{
return false;
}
UpdateContent(_pageService.GetPage(pageTypeToEmbed));
return true;
}
/// <inheritdoc />
public virtual bool ReplaceContent(UIElement pageInstanceToEmbed, object? dataContext = null)
{
UpdateContent(pageInstanceToEmbed, dataContext);
return true;
}
/// <inheritdoc />
public virtual bool GoForward()
{
throw new NotImplementedException();
/*if (Journal.Count <= 1)
{
return false;
}
_currentIndexInJournal += 1;
if (_currentIndexInJournal > Journal.Count - 1)
{
return false;
}
return Navigate(Journal[_currentIndexInJournal]);*/
}
/// <inheritdoc />
public virtual bool GoBack()
{
if (Journal.Count <= 1)
{
return false;
}
var itemId = Journal[^2];
OnBackRequested();
return NavigateInternal(PageIdOrTargetTagNavigationViewsDictionary[itemId], null, false, true);
}
/// <inheritdoc />
public virtual void ClearJournal()
{
_currentIndexInJournal = 0;
Journal.Clear();
_complexNavigationStackHistory.Clear();
}
private bool TryToNavigateWithoutINavigationViewItem(
Type pageType,
bool addToNavigationStack,
object? dataContext = null
)
{
var navigationViewItem = new NavigationViewItem(pageType);
if (!NavigateInternal(navigationViewItem, dataContext, addToNavigationStack))
{
return false;
}
PageTypeNavigationViewsDictionary.Add(pageType, navigationViewItem);
PageIdOrTargetTagNavigationViewsDictionary.Add(navigationViewItem.Id, navigationViewItem);
return true;
}
private bool NavigateInternal(
INavigationViewItem viewItem,
object? dataContext = null,
bool addToNavigationStack = false,
bool isBackwardsNavigated = false
)
{
if (NavigationStack.Count > 0 && NavigationStack[^1] == viewItem)
{
return false;
}
var pageInstance = GetNavigationItemInstance(viewItem);
if (OnNavigating(pageInstance))
{
Debug.WriteLineIf(EnableDebugMessages, "Navigation canceled");
return false;
}
Debug.WriteLineIf(
EnableDebugMessages,
$"DEBUG | {viewItem.Id} - {(string.IsNullOrEmpty(viewItem.TargetPageTag) ? "NO_TAG" : viewItem.TargetPageTag)} - {viewItem.TargetPageType} | NAVIGATED"
);
OnNavigated(pageInstance);
ApplyAttachedProperties(viewItem, pageInstance);
UpdateContent(pageInstance, dataContext);
AddToNavigationStack(viewItem, addToNavigationStack, isBackwardsNavigated);
AddToJournal(viewItem, isBackwardsNavigated);
if (SelectedItem != NavigationStack[0] && NavigationStack[0].IsMenuElement)
{
SelectedItem = NavigationStack[0];
OnSelectionChanged();
}
return true;
}
private void AddToJournal(INavigationViewItem viewItem, bool isBackwardsNavigated)
{
if (isBackwardsNavigated)
{
Journal.RemoveAt(Journal.LastIndexOf(Journal[^2]));
Journal.RemoveAt(Journal.LastIndexOf(Journal[^1]));
_currentIndexInJournal -= 2;
}
Journal.Add(viewItem.Id);
_currentIndexInJournal++;
SetCurrentValue(IsBackEnabledProperty, CanGoBack);
Debug.WriteLineIf(EnableDebugMessages, $"JOURNAL INDEX {_currentIndexInJournal}");
if (Journal.Count > 0)
{
Debug.WriteLineIf(EnableDebugMessages, $"JOURNAL LAST ELEMENT {Journal[^1]}");
}
}
private object GetNavigationItemInstance(INavigationViewItem viewItem)
{
if (viewItem.TargetPageType is null)
{
throw new InvalidOperationException(
$"The {nameof(viewItem)}.{nameof(viewItem.TargetPageType)} property cannot be null."
);
}
if (_serviceProvider is not null)
{
return _serviceProvider.GetService(viewItem.TargetPageType)
?? throw new InvalidOperationException(
$"{nameof(_serviceProvider)}.{nameof(_serviceProvider.GetService)} returned null for type {viewItem.TargetPageType}."
);
}
if (_pageService is not null)
{
return _pageService.GetPage(viewItem.TargetPageType)
?? throw new InvalidOperationException(
$"{nameof(_pageService)}.{nameof(_pageService.GetPage)} returned null for type {viewItem.TargetPageType}."
);
}
return _cache.Remember(
viewItem.TargetPageType,
viewItem.NavigationCacheMode,
ComputeCachedNavigationInstance
)
?? throw new InvalidOperationException(
$"Unable to get or create instance of {viewItem.TargetPageType} from cache."
);
object? ComputeCachedNavigationInstance() => GetPageInstanceFromCache(viewItem.TargetPageType);
}
private object? GetPageInstanceFromCache(Type? targetPageType)
{
if (targetPageType is null)
{
return default;
}
if (_serviceProvider is not null)
{
Debug.WriteLine(
$"Getting {targetPageType} from cache using IServiceProvider."
);
return _serviceProvider.GetService(targetPageType)
?? throw new InvalidOperationException(
$"{nameof(_serviceProvider.GetService)} returned null"
);
}
if (_pageService is not null)
{
Debug.WriteLine(
$"Getting {targetPageType} from cache using INavigationViewPageProvider."
);
return _pageService.GetPage(targetPageType)
?? throw new InvalidOperationException($"{nameof(_pageService.GetPage)} returned null");
}
Debug.WriteLine($"Getting {targetPageType} from cache using reflection.");
return NavigationViewActivator.CreateInstance(targetPageType)
?? throw new InvalidOperationException("Failed to create instance of the page");
}
private static void ApplyAttachedProperties(INavigationViewItem viewItem, object pageInstance)
{
if (
pageInstance is FrameworkElement frameworkElement
&& GetHeaderContent(frameworkElement) is { } headerContent
)
{
viewItem.Content = headerContent;
}
}
private void UpdateContent(object? content, object? dataContext = null)
{
if (dataContext is not null && content is FrameworkElement frameworkViewContent)
{
frameworkViewContent.DataContext = dataContext;
}
NavigationViewContentPresenter.Navigate(content);
}
private void OnNavigationViewContentPresenterNavigated(
object sender,
System.Windows.Navigation.NavigationEventArgs e
)
{
if (sender is not System.Windows.Controls.Frame frame)
{
return;
}
frame.RemoveBackEntry();
/*var replaced = 1;
((NavigationViewContentPresenter)sender).JournalOwnership =*/
}
private void AddToNavigationStack(
INavigationViewItem viewItem,
bool addToNavigationStack,
bool isBackwardsNavigated
)
{
if (isBackwardsNavigated)
{
RecreateNavigationStackFromHistory(viewItem);
}
if (addToNavigationStack && !NavigationStack.Contains(viewItem))
{
viewItem.Activate(this);
NavigationStack.Add(viewItem);
}
if (!addToNavigationStack)
{
UpdateCurrentNavigationStackItem(viewItem);
}
ClearNavigationStack(viewItem);
}
private void UpdateCurrentNavigationStackItem(INavigationViewItem viewItem)
{
if (NavigationStack.Contains(viewItem))
{
return;
}
if (NavigationStack.Count > 1)
{
AddToNavigationStackHistory(viewItem);
}
if (NavigationStack.Count == 0)
{
viewItem.Activate(this);
NavigationStack.Add(viewItem);
}
else
{
ReplaceThirstElementInNavigationStack(viewItem);
}
ClearNavigationStack(1);
}
private void RecreateNavigationStackFromHistory(INavigationViewItem item)
{
List<INavigationViewItem?[]>? historyList;
if (!_complexNavigationStackHistory.TryGetValue(item, out historyList) || historyList.Count == 0)
{
return;
}
var latestHistory = historyList[^1];
var startIndex = 0;
if (latestHistory[0]!.IsMenuElement)
{
startIndex = 1;
ReplaceThirstElementInNavigationStack(latestHistory[0]!);
}
for (var i = startIndex; i < latestHistory.Length; i++)
{
if (latestHistory[i] is null)
{
break;
}
AddToNavigationStack(latestHistory[i]!, true, false);
}
historyList.Remove(latestHistory);
/*if (historyList.Count == 0)
_complexNavigationStackHistory.Remove(item);
*/
#if NET6_0_OR_GREATER
System.Buffers.ArrayPool<INavigationViewItem>.Shared.Return(latestHistory!, true);
#endif
AddToNavigationStack(item, true, false);
}
private void AddToNavigationStackHistory(INavigationViewItem viewItem)
{
var lastItem = NavigationStack[^1];
var startIndex = NavigationStack.IndexOf(viewItem);
if (startIndex < 0)
{
startIndex = 0;
}
List<INavigationViewItem?[]>? historyList;
if (!_complexNavigationStackHistory.TryGetValue(lastItem, out historyList))
{
historyList = new List<INavigationViewItem?[]>(5);
_complexNavigationStackHistory.Add(lastItem, historyList);
}
var arrayLength = NavigationStack.Count - 1 - startIndex;
INavigationViewItem[] array;
// OPTIMIZATION: Initializing an array every time well... not an ideal
#if NET6_0_OR_GREATER
array = System.Buffers.ArrayPool<INavigationViewItem>.Shared.Rent(arrayLength);
#else
array = new INavigationViewItem[arrayLength];
#endif
historyList.Add(array);
var latestHistory = historyList[^1];
var i = 0;
for (var j = startIndex; j < NavigationStack.Count - 1; j++)
{
latestHistory[i] = NavigationStack[j];
i++;
}
}
private void ClearNavigationStack(int navigationStackItemIndex)
{
var navigationStackCount = NavigationStack.Count;
var length = navigationStackCount - navigationStackItemIndex;
if (length == 0)
{
return;
}
for (var j = navigationStackCount - 1; j >= navigationStackCount - length; j--)
{
NavigationStack.Remove(NavigationStack[j]);
}
}
private void ClearNavigationStack(INavigationViewItem item)
{
var navigationStackCount = NavigationStack.Count;
if (navigationStackCount <= 1)
{
return;
}
var index = NavigationStack.IndexOf(item);
if (index >= navigationStackCount - 1)
{
return;
}
AddToNavigationStackHistory(item);
ClearNavigationStack(++index);
}
private void ReplaceThirstElementInNavigationStack(INavigationViewItem newItem)
{
NavigationStack[0].Deactivate(this);
NavigationStack[0] = newItem;
NavigationStack[0].Activate(this);
}
}