// 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; /// /// Defines navigation logic and state management for . /// public partial class NavigationView { protected List Journal { get; } = new(50); protected ObservableCollection NavigationStack { get; } = []; private readonly NavigationCache _cache = new(); private readonly Dictionary< INavigationViewItem, List > _complexNavigationStackHistory = []; private IServiceProvider? _serviceProvider; private INavigationViewPageProvider? _pageService; private int _currentIndexInJournal; /// public bool CanGoBack => Journal.Count > 1 && _currentIndexInJournal >= 0; /// public void SetPageProviderService(INavigationViewPageProvider navigationViewPageProvider) => _pageService = navigationViewPageProvider; /// public void SetServiceProvider(IServiceProvider serviceProvider) => _serviceProvider = serviceProvider; /// 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); } /// public virtual bool Navigate(string pageIdOrTargetTag, object? dataContext = null) { if ( PageIdOrTargetTagNavigationViewsDictionary.TryGetValue( pageIdOrTargetTag, out var navigationViewItem ) ) { return NavigateInternal(navigationViewItem, dataContext); } return false; } /// 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); } /// 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; } /// public virtual bool ReplaceContent(UIElement pageInstanceToEmbed, object? dataContext = null) { UpdateContent(pageInstanceToEmbed, dataContext); return true; } /// 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]);*/ } /// public virtual bool GoBack() { if (Journal.Count <= 1) { return false; } var itemId = Journal[^2]; OnBackRequested(); return NavigateInternal(PageIdOrTargetTagNavigationViewsDictionary[itemId], null, false, true); } /// 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? 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.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? historyList; if (!_complexNavigationStackHistory.TryGetValue(lastItem, out historyList)) { historyList = new List(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.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); } }