/* Based on Windows UI Library https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.controls.navigationview?view=winrt-22621 */ using System.Collections; using System.Collections.ObjectModel; using System.Collections.Specialized; using System.Diagnostics; using System.Windows.Input; // ReSharper disable once CheckNamespace namespace WPFluent.Controls; /// /// Represents a container that enables navigation of app content. It has a header, a view for the main content, and a /// menu pane for navigation commands. /// public partial class NavigationView : System.Windows.Controls.Control, INavigationView { private static readonly Thickness AutoSuggestBoxMarginDefault = new(8, 8, 8, 16); private static readonly Thickness FrameMarginDefault = new(0, 50, 0, 0); private static readonly Thickness TitleBarPaneCompactMarginDefault = new(35, 0, 0, 0); private static readonly Thickness TitleBarPaneOpenMarginDefault = new(35, 0, 0, 0); private readonly ObservableCollection _autoSuggestBoxItems = []; private readonly ObservableCollection _breadcrumbBarItems = []; /// /// Initializes static members of the class and overrides default property metadata. /// static NavigationView() { DefaultStyleKeyProperty.OverrideMetadata( typeof(NavigationView), new FrameworkPropertyMetadata(typeof(NavigationView))); MarginProperty.OverrideMetadata( typeof(NavigationView), new FrameworkPropertyMetadata(new Thickness(0, 0, 0, 0))); } /// /// Initializes a new instance of the class. /// public NavigationView() { NavigationParent = this; Loaded += OnLoaded; Unloaded += OnUnloaded; SizeChanged += OnSizeChanged; // Initialize MenuItems collection var menuItems = new ObservableCollection(); menuItems.CollectionChanged += OnMenuItems_CollectionChanged; SetValue(MenuItemsPropertyKey, menuItems); var footerMenuItems = new ObservableCollection(); footerMenuItems.CollectionChanged += OnMenuItems_CollectionChanged; SetValue(FooterMenuItemsPropertyKey, footerMenuItems); } private void AutoSuggestBoxOnQuerySubmitted(AutoSuggestBox sender, AutoSuggestBoxQuerySubmittedEventArgs args) { var suggestions = new List(); var querySplit = args.QueryText.Split(' '); foreach(var item in _autoSuggestBoxItems) { bool isMatch = true; foreach(string queryToken in querySplit) { if(item.IndexOf(queryToken, StringComparison.CurrentCultureIgnoreCase) < 0) { isMatch = false; } } if(isMatch) { suggestions.Add(item); } } if(suggestions.Count <= 0) { return; } var element = suggestions.First(); if(NavigateToMenuItemFromAutoSuggestBox(MenuItems, element)) { return; } NavigateToMenuItemFromAutoSuggestBox(FooterMenuItems, element); } /// /// Navigate to the page after its name is selected in . /// private void AutoSuggestBoxOnSuggestionChosen(AutoSuggestBox sender, AutoSuggestBoxSuggestionChosenEventArgs args) { if(sender.IsSuggestionListOpen) { return; } if(args.SelectedItem is not string selectedSuggestBoxItem) { return; } if(NavigateToMenuItemFromAutoSuggestBox(MenuItems, selectedSuggestBoxItem)) { return; } NavigateToMenuItemFromAutoSuggestBox(FooterMenuItems, selectedSuggestBoxItem); } [DebuggerStepThrough] private void NavigationStackOnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) { switch(e.Action) { case NotifyCollectionChangedAction.Add: _breadcrumbBarItems.Add(new NavigationViewBreadcrumbItem((INavigationViewItem)e.NewItems![0]!)); break; case NotifyCollectionChangedAction.Remove: _breadcrumbBarItems.RemoveAt(e.OldStartingIndex); break; case NotifyCollectionChangedAction.Replace: _breadcrumbBarItems[0] = new NavigationViewBreadcrumbItem((INavigationViewItem)e.NewItems![0]!); break; case NotifyCollectionChangedAction.Move: break; case NotifyCollectionChangedAction.Reset: _breadcrumbBarItems.Clear(); break; default: throw new ArgumentOutOfRangeException(nameof(e), e.Action, $"Unsupported action: {e.Action}"); } } private void OnLoaded(object sender, RoutedEventArgs e) { // TODO: Refresh UpdateVisualState((NavigationView)sender); } private void UpdateAutoSuggestBoxSuggestions() { if(AutoSuggestBox == null) { return; } _autoSuggestBoxItems.Clear(); AddItemsToAutoSuggestBoxItems(); } protected virtual void AddItemsToAutoSuggestBoxItems() { AddItemsToAutoSuggestBoxItems(MenuItems); AddItemsToAutoSuggestBoxItems(FooterMenuItems); } protected virtual void AddItemsToAutoSuggestBoxItems(IEnumerable list) { foreach(NavigationViewItem singleNavigationViewItem in list.OfType()) { if(singleNavigationViewItem is { Content: string content, TargetPageType: { } } && !string.IsNullOrWhiteSpace(content)) { _autoSuggestBoxItems.Add(content); } if(singleNavigationViewItem.HasMenuItems) { AddItemsToAutoSuggestBoxItems(singleNavigationViewItem.MenuItems); } } } protected virtual void AddItemsToDictionaries() { AddItemsToDictionaries(MenuItems); AddItemsToDictionaries(FooterMenuItems); } protected virtual void AddItemsToDictionaries(IEnumerable list) { foreach(NavigationViewItem singleNavigationViewItem in list.OfType()) { if(!PageIdOrTargetTagNavigationViewsDictionary.ContainsKey(singleNavigationViewItem.Id)) { PageIdOrTargetTagNavigationViewsDictionary.Add(singleNavigationViewItem.Id, singleNavigationViewItem); } if(!PageIdOrTargetTagNavigationViewsDictionary.ContainsKey(singleNavigationViewItem.TargetPageTag)) { PageIdOrTargetTagNavigationViewsDictionary.Add( singleNavigationViewItem.TargetPageTag, singleNavigationViewItem); } if(singleNavigationViewItem.TargetPageType != null && !PageTypeNavigationViewsDictionary.ContainsKey(singleNavigationViewItem.TargetPageType)) { PageTypeNavigationViewsDictionary.Add(singleNavigationViewItem.TargetPageType, singleNavigationViewItem); } singleNavigationViewItem.IsMenuElement = true; if(singleNavigationViewItem.HasMenuItems) { AddItemsToDictionaries(singleNavigationViewItem.MenuItems); } } } /// /// This virtual method is called when is clicked. /// protected virtual void AutoSuggestBoxSymbolButtonOnClick(object sender, RoutedEventArgs e) { SetCurrentValue(IsPaneOpenProperty, !IsPaneOpen); AutoSuggestBox?.Focus(); } protected virtual void BreadcrumbBarOnItemClicked( Controls.BreadcrumbBar sender, BreadcrumbBarItemClickedEventArgs e) { var item = (NavigationViewBreadcrumbItem)e.Item; Navigate(item.PageId); } protected virtual void CloseNavigationViewItemMenus() { if(Journal.Count <= 0 || IsPaneOpen) { return; } DeactivateMenuItems(MenuItems); DeactivateMenuItems(FooterMenuItems); INavigationViewItem currentItem = PageIdOrTargetTagNavigationViewsDictionary[Journal[^1]]; if(currentItem.NavigationViewItemParent is null) { currentItem.Activate(this); return; } currentItem.Deactivate(this); currentItem.NavigationViewItemParent?.Activate(this); } protected void DeactivateMenuItems(IEnumerable list) { foreach(var item in list) { if(item is NavigationViewItem singleNavigationViewItem) { singleNavigationViewItem.Deactivate(this); } } } protected virtual bool NavigateToMenuItemFromAutoSuggestBox(IEnumerable list, string selectedSuggestBoxItem) { foreach(NavigationViewItem singleNavigationViewItem in list.OfType()) { if(singleNavigationViewItem.Content is string content && content == selectedSuggestBoxItem) { NavigateInternal(singleNavigationViewItem); singleNavigationViewItem.BringIntoView(); singleNavigationViewItem.Focus(); // TODO: Element or content? return true; } if(NavigateToMenuItemFromAutoSuggestBox(singleNavigationViewItem.MenuItems, selectedSuggestBoxItem)) { return true; } } return false; } /// /// This virtual method is called when is clicked. /// protected virtual void OnBackButtonClick(object sender, RoutedEventArgs e) { GoBack(); } /// protected override void OnInitialized(EventArgs e) { base.OnInitialized(e); NavigationStack.CollectionChanged += NavigationStackOnCollectionChanged; InvalidateArrange(); InvalidateVisual(); UpdateLayout(); UpdateAutoSuggestBoxSuggestions(); AddItemsToDictionaries(); } /// /// This virtual method is called when is changed. /// protected virtual void OnItemTemplateChanged() { UpdateMenuItemsTemplate(); } protected override void OnMouseDown(MouseButtonEventArgs e) { // Back button if(e.ChangedButton is MouseButton.XButton1) { GoBack(); e.Handled = true; } base.OnMouseDown(e); } /// /// This virtual method is called when is changed. /// protected virtual void OnPaneDisplayModeChanged() { switch(PaneDisplayMode) { case NavigationViewPaneDisplayMode.LeftFluent: SetCurrentValue(IsBackButtonVisibleProperty, NavigationViewBackButtonVisible.Collapsed); SetCurrentValue(IsPaneToggleVisibleProperty, false); break; } } /// /// This virtual method is called when ActualWidth or ActualHeight (or both) changed. /// protected virtual void OnSizeChanged(object sender, SizeChangedEventArgs e) { // TODO: Update reveal } /// /// This virtual method is called when is clicked. /// protected virtual void OnToggleButtonClick(object sender, RoutedEventArgs e) { SetCurrentValue(IsPaneOpenProperty, !IsPaneOpen); } /// /// This virtual method is called when this element is detached form a loaded tree. /// protected virtual void OnUnloaded(object sender, RoutedEventArgs e) { Loaded -= OnLoaded; Unloaded -= OnUnloaded; SizeChanged -= OnSizeChanged; NavigationStack.CollectionChanged -= NavigationStackOnCollectionChanged; PageIdOrTargetTagNavigationViewsDictionary.Clear(); PageTypeNavigationViewsDictionary.Clear(); ClearJournal(); if(AutoSuggestBox is not null) { AutoSuggestBox.SuggestionChosen -= AutoSuggestBoxOnSuggestionChosen; AutoSuggestBox.QuerySubmitted -= AutoSuggestBoxOnQuerySubmitted; } if(Header is Controls.BreadcrumbBar breadcrumbBar) { breadcrumbBar.ItemClicked -= BreadcrumbBarOnItemClicked; } if(ToggleButton is not null) { ToggleButton.Click -= OnToggleButtonClick; } if(BackButton is not null) { BackButton.Click -= OnToggleButtonClick; } if(AutoSuggestBoxSymbolButton is not null) { AutoSuggestBoxSymbolButton.Click -= AutoSuggestBoxSymbolButtonOnClick; } } protected virtual void UpdateMenuItemsTemplate() { UpdateMenuItemsTemplate(MenuItems); UpdateMenuItemsTemplate(FooterMenuItems); } protected virtual void UpdateMenuItemsTemplate(IEnumerable list) { if(ItemTemplate == null) { return; } foreach(var item in list) { if(item is NavigationViewItem singleNavigationViewItem) { singleNavigationViewItem.Template = ItemTemplate; } } } protected static void UpdateVisualState(NavigationView navigationView) { // Skip display modes that don't have multiple states if(navigationView.PaneDisplayMode is NavigationViewPaneDisplayMode.LeftFluent or NavigationViewPaneDisplayMode.Top or NavigationViewPaneDisplayMode.Bottom) { return; } VisualStateManager.GoToState(navigationView, navigationView.IsPaneOpen ? "PaneOpen" : "PaneCompact", true); } protected Dictionary PageIdOrTargetTagNavigationViewsDictionary { get; } = []; protected Dictionary PageTypeNavigationViewsDictionary { get; } = []; internal void OnNavigationViewItemClick(NavigationViewItem navigationViewItem) { OnItemInvoked(); NavigateInternal(navigationViewItem); } internal void ToggleAllExpands() { // TODO: When shift clicked on navigationviewitem } /// public INavigationViewItem? SelectedItem { get; protected set; } }