diff --git a/src/Packages.props b/src/Packages.props index f2ca092b9..0b215faf7 100644 --- a/src/Packages.props +++ b/src/Packages.props @@ -7,5 +7,6 @@ + \ No newline at end of file diff --git a/src/Wpf.Ui.Demo/App.xaml.cs b/src/Wpf.Ui.Demo/App.xaml.cs index 0fba0c658..2af198870 100644 --- a/src/Wpf.Ui.Demo/App.xaml.cs +++ b/src/Wpf.Ui.Demo/App.xaml.cs @@ -93,6 +93,8 @@ public partial class App services.AddScoped(); services.AddScoped(); + + services.AddScoped(); // Test windows services.AddTransient(); diff --git a/src/Wpf.Ui.Demo/ViewModels/BreadcrumbPagesViewModel.cs b/src/Wpf.Ui.Demo/ViewModels/BreadcrumbPagesViewModel.cs new file mode 100644 index 000000000..80037cd82 --- /dev/null +++ b/src/Wpf.Ui.Demo/ViewModels/BreadcrumbPagesViewModel.cs @@ -0,0 +1,31 @@ +using System.Windows.Input; +using Microsoft.Toolkit.Mvvm.Input; +using Wpf.Ui.Mvvm.Contracts; + +namespace Wpf.Ui.Demo.ViewModels; + +public class BreadcrumbPagesViewModel +{ + public RelayCommand OnClickCommand { get; } + public ICommand OnNavigateBackCommand { get; } + + private readonly INavigationService _navigationService; + + public BreadcrumbPagesViewModel(INavigationService navigationService) + { + _navigationService = navigationService; + + OnClickCommand = new RelayCommand(OnClick); + OnNavigateBackCommand = new RelayCommand(OnNavigateBack); + } + + private void OnClick(string pageTag) + { + _navigationService.NavigateTo($"/{pageTag}", this); + } + + private void OnNavigateBack() + { + _navigationService.NavigateTo(".."); + } +} diff --git a/src/Wpf.Ui.Demo/ViewModels/ButtonsViewModel.cs b/src/Wpf.Ui.Demo/ViewModels/ButtonsViewModel.cs index 877d7fb81..f12bf72c1 100644 --- a/src/Wpf.Ui.Demo/ViewModels/ButtonsViewModel.cs +++ b/src/Wpf.Ui.Demo/ViewModels/ButtonsViewModel.cs @@ -27,9 +27,8 @@ public ButtonsViewModel(INavigationService navigationService) var currentTheme = testGetThemeService.GetSystemTheme(); } - private void OnShowMore(string parameter) { - _navigationService.Navigate(typeof(Views.Pages.Input)); + _navigationService.NavigateTo("/input"); } } diff --git a/src/Wpf.Ui.Demo/ViewModels/DashboardViewModel.cs b/src/Wpf.Ui.Demo/ViewModels/DashboardViewModel.cs index 038786190..4c7c89793 100644 --- a/src/Wpf.Ui.Demo/ViewModels/DashboardViewModel.cs +++ b/src/Wpf.Ui.Demo/ViewModels/DashboardViewModel.cs @@ -47,19 +47,19 @@ private void OnNavigate(string parameter) switch (parameter) { case "navigate_to_input": - _navigationService.Navigate(typeof(Views.Pages.Input)); + _navigationService.NavigateTo(typeof(Views.Pages.Input)); return; case "navigate_to_controls": - _navigationService.Navigate(typeof(Views.Pages.Controls)); + _navigationService.NavigateTo(typeof(Views.Pages.Controls)); return; case "navigate_to_colors": - _navigationService.Navigate(typeof(Views.Pages.Colors)); + _navigationService.NavigateTo(typeof(Views.Pages.Colors)); return; case "navigate_to_icons": - _navigationService.Navigate(typeof(Views.Pages.Icons)); + _navigationService.NavigateTo(typeof(Views.Pages.Icons)); return; } } diff --git a/src/Wpf.Ui.Demo/Views/Container.xaml b/src/Wpf.Ui.Demo/Views/Container.xaml index aaf32c924..c6a971ab5 100644 --- a/src/Wpf.Ui.Demo/Views/Container.xaml +++ b/src/Wpf.Ui.Demo/Views/Container.xaml @@ -2,6 +2,7 @@ x:Class="Wpf.Ui.Demo.Views.Container" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:breadcrumbDemo="clr-namespace:Wpf.Ui.Demo.Views.Pages.BreadcrumbDemo" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:demo="clr-namespace:Wpf.Ui.Demo" xmlns:diagnostics="clr-namespace:Wpf.Ui.Demo.Views.Diagnostics" @@ -98,6 +99,12 @@ Content="Theme" Icon="DarkTheme24" /> + + + + + + @@ -198,8 +205,8 @@ ForceShutdown="False" Icon="pack://application:,,,/Resources/wpfui.png" MinimizeToTray="False" - ShowHelp="False" ShowClose="True" + ShowHelp="False" ShowMaximize="True" ShowMinimize="True" UseSnapLayout="True"> @@ -237,5 +244,10 @@ + + diff --git a/src/Wpf.Ui.Demo/Views/Container.xaml.cs b/src/Wpf.Ui.Demo/Views/Container.xaml.cs index e9e158b98..aa02a4864 100644 --- a/src/Wpf.Ui.Demo/Views/Container.xaml.cs +++ b/src/Wpf.Ui.Demo/Views/Container.xaml.cs @@ -4,6 +4,7 @@ // All Rights Reserved. using System; +using System.Linq; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; @@ -96,11 +97,14 @@ public Frame GetFrame() public INavigation GetNavigation() => RootNavigation; - public bool Navigate(Type pageType) - => RootNavigation.Navigate(pageType); + public void NavigateTo(Type type, object dataContext = null) + => RootNavigation.NavigateTo(type, dataContext); + + public void NavigateTo(string pageTag, object dataContext = null) + => RootNavigation.NavigateTo(pageTag, dataContext); public void SetPageService(IPageService pageService) - => RootNavigation.PageService = pageService; + => RootNavigation.SetIPageService(pageService); public void ShowWindow() => Show(); @@ -108,9 +112,10 @@ public void ShowWindow() public void CloseWindow() => Close(); + #endregion INavigationWindow methods - private void InvokeSplashScreen() + private async void InvokeSplashScreen() { if (_initialized) return; @@ -122,24 +127,16 @@ private void InvokeSplashScreen() _taskBarService.SetState(this, TaskBarProgressState.Indeterminate); - Task.Run(async () => - { - // Remember to always include Delays and Sleeps in - // your applications to be able to charge the client for optimizations later. - await Task.Delay(4000); + // Remember to always include Delays and Sleeps in + // your applications to be able to charge the client for optimizations later. + await Task.Delay(4000); - await Dispatcher.InvokeAsync(() => - { - RootWelcomeGrid.Visibility = Visibility.Hidden; - RootMainGrid.Visibility = Visibility.Visible; + RootWelcomeGrid.Visibility = Visibility.Hidden; + RootMainGrid.Visibility = Visibility.Visible; - Navigate(typeof(Pages.Dashboard)); + NavigateTo(typeof(Pages.Dashboard)); - _taskBarService.SetState(this, TaskBarProgressState.None); - }); - - return true; - }); + _taskBarService.SetState(this, TaskBarProgressState.None); } private void NavigationButtonTheme_OnClick(object sender, RoutedEventArgs e) @@ -157,16 +154,15 @@ private void TrayMenuItem_OnClick(object sender, RoutedEventArgs e) private void RootNavigation_OnNavigated(INavigation sender, RoutedNavigationEventArgs e) { - System.Diagnostics.Debug.WriteLine($"DEBUG | WPF UI Navigated to: {sender?.Current ?? null}", "Wpf.Ui.Demo"); + System.Diagnostics.Debug.WriteLine($"DEBUG | WPF UI Navigated to: {e.NavigatedTo}", "Wpf.Ui.Demo"); // This funky solution allows us to impose a negative // margin for Frame only for the Dashboard page, thanks // to which the banner will cover the entire page nicely. RootFrame.Margin = new Thickness( left: 0, - top: sender?.Current?.PageTag == "dashboard" ? -69 : 0, + top: e.NavigatedTo.PageTag == "dashboard" ? -69 : 0, right: 0, bottom: 0); } } - diff --git a/src/Wpf.Ui.Demo/Views/Pages/BreadcrumbDemo/Page1.xaml b/src/Wpf.Ui.Demo/Views/Pages/BreadcrumbDemo/Page1.xaml new file mode 100644 index 000000000..d3e8394ee --- /dev/null +++ b/src/Wpf.Ui.Demo/Views/Pages/BreadcrumbDemo/Page1.xaml @@ -0,0 +1,25 @@ + + + + + + + + + + + + diff --git a/src/Wpf.Ui.Demo/Views/Pages/BreadcrumbDemo/Page1.xaml.cs b/src/Wpf.Ui.Demo/Views/Pages/BreadcrumbDemo/Page1.xaml.cs new file mode 100644 index 000000000..2f18ed5a9 --- /dev/null +++ b/src/Wpf.Ui.Demo/Views/Pages/BreadcrumbDemo/Page1.xaml.cs @@ -0,0 +1,11 @@ +using System.Windows.Controls; + +namespace Wpf.Ui.Demo.Views.Pages.BreadcrumbDemo; + +public partial class Page1 : Page +{ + public Page1() + { + InitializeComponent(); + } +} diff --git a/src/Wpf.Ui.Demo/Views/Pages/BreadcrumbDemo/Page2.xaml b/src/Wpf.Ui.Demo/Views/Pages/BreadcrumbDemo/Page2.xaml new file mode 100644 index 000000000..583c4cc4a --- /dev/null +++ b/src/Wpf.Ui.Demo/Views/Pages/BreadcrumbDemo/Page2.xaml @@ -0,0 +1,25 @@ + + + + + + + + + + + + diff --git a/src/Wpf.Ui.Demo/Views/Pages/BreadcrumbDemo/Page2.xaml.cs b/src/Wpf.Ui.Demo/Views/Pages/BreadcrumbDemo/Page2.xaml.cs new file mode 100644 index 000000000..452815201 --- /dev/null +++ b/src/Wpf.Ui.Demo/Views/Pages/BreadcrumbDemo/Page2.xaml.cs @@ -0,0 +1,11 @@ +using System.Windows.Controls; + +namespace Wpf.Ui.Demo.Views.Pages.BreadcrumbDemo; + +public partial class Page2 : Page +{ + public Page2() + { + InitializeComponent(); + } +} diff --git a/src/Wpf.Ui.Demo/Views/Pages/BreadcrumbDemo/Page3.xaml b/src/Wpf.Ui.Demo/Views/Pages/BreadcrumbDemo/Page3.xaml new file mode 100644 index 000000000..7b4089dd7 --- /dev/null +++ b/src/Wpf.Ui.Demo/Views/Pages/BreadcrumbDemo/Page3.xaml @@ -0,0 +1,25 @@ + + + + + + + + + + + + diff --git a/src/Wpf.Ui.Demo/Views/Pages/BreadcrumbDemo/Page3.xaml.cs b/src/Wpf.Ui.Demo/Views/Pages/BreadcrumbDemo/Page3.xaml.cs new file mode 100644 index 000000000..5c2059e40 --- /dev/null +++ b/src/Wpf.Ui.Demo/Views/Pages/BreadcrumbDemo/Page3.xaml.cs @@ -0,0 +1,11 @@ +using System.Windows.Controls; + +namespace Wpf.Ui.Demo.Views.Pages.BreadcrumbDemo; + +public partial class Page3 : Page +{ + public Page3() + { + InitializeComponent(); + } +} diff --git a/src/Wpf.Ui.Demo/Views/Pages/Controls.xaml b/src/Wpf.Ui.Demo/Views/Pages/Controls.xaml index c00c43d1e..f52cba0da 100644 --- a/src/Wpf.Ui.Demo/Views/Pages/Controls.xaml +++ b/src/Wpf.Ui.Demo/Views/Pages/Controls.xaml @@ -29,6 +29,7 @@ + @@ -84,6 +85,22 @@ Text="Opens the MessageBox." /> + + + + + + + (); + _navigation.NavigateTo("/page1", viewModel); + } } diff --git a/src/Wpf.Ui.Demo/Views/Pages/ExperimentalDashboard.xaml.cs b/src/Wpf.Ui.Demo/Views/Pages/ExperimentalDashboard.xaml.cs index e1fd83569..a8ece4934 100644 --- a/src/Wpf.Ui.Demo/Views/Pages/ExperimentalDashboard.xaml.cs +++ b/src/Wpf.Ui.Demo/Views/Pages/ExperimentalDashboard.xaml.cs @@ -58,7 +58,7 @@ private void ButtonExternal_OnClick(object sender, RoutedEventArgs e) if (DataContext is not ExperimentalViewModel viewData) return; - viewData.ParentWindow.Navigate(typeof(ExperimentalDashboard)); + viewData.ParentWindow.NavigateTo(typeof(ExperimentalDashboard)); } private void TaskbarStateComboBox_OnSelectionChanged(object sender, SelectionChangedEventArgs e) diff --git a/src/Wpf.Ui.Demo/Views/Windows/ExperimentalWindow.xaml.cs b/src/Wpf.Ui.Demo/Views/Windows/ExperimentalWindow.xaml.cs index 9e6b6d692..a90b5fb71 100644 --- a/src/Wpf.Ui.Demo/Views/Windows/ExperimentalWindow.xaml.cs +++ b/src/Wpf.Ui.Demo/Views/Windows/ExperimentalWindow.xaml.cs @@ -101,7 +101,7 @@ public ExperimentalWindow(ExperimentalViewModel viewModel, IThemeService themeSe private void RootNavigationOnLoaded(object sender, RoutedEventArgs e) { - RootNavigation.Navigate(0, DataContext); + //RootNavigation.NavigateTo(0, DataContext); } private void NavigationButtonTheme_OnClick(object sender, RoutedEventArgs e) @@ -130,15 +130,18 @@ public Frame GetFrame() public INavigation GetNavigation() => RootNavigation; - public bool Navigate(Type pageType) - => RootNavigation.Navigate(pageType); - public void SetPageService(IPageService pageService) - => RootNavigation.PageService = pageService; + => RootNavigation.SetIPageService(pageService); public void ShowWindow() => Show(); public void CloseWindow() => Close(); + + public void NavigateTo(Type type, object dataContext = null) + => RootNavigation.NavigateTo(type, dataContext); + + public void NavigateTo(string pageTag, object dataContext = null) + => RootNavigation.NavigateTo(pageTag, dataContext); } diff --git a/src/Wpf.Ui.SimpleDemo/MainWindow.xaml b/src/Wpf.Ui.SimpleDemo/MainWindow.xaml index 9e4f5fe78..2a82b0442 100644 --- a/src/Wpf.Ui.SimpleDemo/MainWindow.xaml +++ b/src/Wpf.Ui.SimpleDemo/MainWindow.xaml @@ -29,12 +29,24 @@ Icon="Home24" PageTag="home" PageType="{x:Type local:DashboardPage}" /> - - + + - - + + +/// TODO +/// +public interface INavigationCancelable +{ + /// + /// TODO + /// + /// + bool CouldNavigate(INavigationItem? navigationFrom); +} diff --git a/src/Wpf.Ui/Common/RoutedNavigationEventArgs.cs b/src/Wpf.Ui/Common/RoutedNavigationEventArgs.cs index ae124110c..d93551187 100644 --- a/src/Wpf.Ui/Common/RoutedNavigationEventArgs.cs +++ b/src/Wpf.Ui/Common/RoutedNavigationEventArgs.cs @@ -3,30 +3,30 @@ // Copyright (C) Leszek Pomianowski and WPF UI Contributors. // All Rights Reserved. +#nullable enable using System.Windows; using Wpf.Ui.Controls.Interfaces; namespace Wpf.Ui.Common; /// -/// with additional . +/// /// public class RoutedNavigationEventArgs : RoutedEventArgs { - /// - /// Currently displayed page. - /// - public INavigationItem CurrentPage { get; set; } + public readonly INavigationItem? NavigatedFrom; + public readonly INavigationItem NavigatedTo; /// - /// Constructor for . + /// TODO /// - /// The new value that the SourceProperty is being set to. - /// The new value that the Property is being set to. - /// Currently displayed page. - public RoutedNavigationEventArgs(RoutedEvent routedEvent, object source, INavigationItem currentPage) : base( - routedEvent, source) + /// + /// + /// + /// + public RoutedNavigationEventArgs(RoutedEvent routedEvent, INavigation source, INavigationItem? navigatedFrom, INavigationItem navigatedTo) : base(routedEvent, source) { - CurrentPage = currentPage; + NavigatedFrom = navigatedFrom; + NavigatedTo = navigatedTo; } } diff --git a/src/Wpf.Ui/Controls/Breadcrumb.cs b/src/Wpf.Ui/Controls/Breadcrumb.cs index cb0b089ef..c4704ac5c 100644 --- a/src/Wpf.Ui/Controls/Breadcrumb.cs +++ b/src/Wpf.Ui/Controls/Breadcrumb.cs @@ -3,8 +3,12 @@ // Copyright (C) Leszek Pomianowski and WPF UI Contributors. // All Rights Reserved. -using System; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.ComponentModel; using System.Windows; +using System.Windows.Input; +using CommunityToolkit.Diagnostics; using Wpf.Ui.Common; using Wpf.Ui.Controls.Interfaces; @@ -15,27 +19,16 @@ namespace Wpf.Ui.Controls; /// public class Breadcrumb : System.Windows.Controls.Control { - /// - /// Property for . - /// - public static readonly DependencyProperty CurrentProperty = DependencyProperty.Register(nameof(Current), - typeof(string), typeof(Breadcrumb), new PropertyMetadata(String.Empty)); - /// /// Property for . /// public static readonly DependencyProperty NavigationProperty = DependencyProperty.Register(nameof(Navigation), typeof(INavigation), typeof(Breadcrumb), - new PropertyMetadata(null, OnNavigationChanged)); + new PropertyMetadata(null)); + + public static readonly DependencyProperty BreadcrumbItemsProperty = DependencyProperty.Register(nameof(BreadcrumbItems), + typeof(ObservableCollection), typeof(Breadcrumb), new PropertyMetadata(null)); - /// - /// based on which displays the titles. - /// - public string Current - { - get => (string)GetValue(CurrentProperty); - set => SetValue(CurrentProperty, value); - } /// /// based on which displays the titles. @@ -46,35 +39,76 @@ public INavigation Navigation set => SetValue(NavigationProperty, value); } - protected virtual void OnNavigated(INavigation sender, RoutedNavigationEventArgs e) + public ObservableCollection BreadcrumbItems { -#if DEBUG - System.Diagnostics.Debug.WriteLine($"INFO | {typeof(Breadcrumb)} builded, current nav: {Navigation.GetType()}", "Wpf.Ui.Breadcrumb"); -#endif + get => (ObservableCollection)GetValue(BreadcrumbItemsProperty); + private set => SetValue(BreadcrumbItemsProperty, value); + } - //TODO: Navigate with previous levels + private readonly ICommand _onClickCommand; - if (Navigation?.Current is not INavigationItem item) + public Breadcrumb() + { + BreadcrumbItems = new ObservableCollection(); + _onClickCommand = new RelayCommand(OnClick); + + if (DesignerProperties.GetIsInDesignMode(this)) return; - var pageName = item.Content as string; + Loaded += (_, _) => + { + Guard.IsNotNull(Navigation, nameof(Navigation)); + Navigation.NavigationStack.CollectionChanged += NavigationStackOnCollectionChanged; - if (String.IsNullOrEmpty(pageName)) - return; + if (Navigation.NavigationStack.Count <= 0) + return; + + foreach (var item in Navigation.NavigationStack) + BreadcrumbItems.Add( BreadcrumbItem.Create(item, _onClickCommand)); + + BreadcrumbItems[BreadcrumbItems.Count - 1].IsActive = true; + }; - Current = pageName; + Unloaded += (_, _) => + { + Navigation.NavigationStack.CollectionChanged -= NavigationStackOnCollectionChanged; + }; } - protected virtual void OnNavigationChanged() + private void NavigationStackOnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { - Navigation.Navigated += OnNavigated; + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + { + var newItem = (INavigationItem) e.NewItems![0]; + BreadcrumbItems.Add(BreadcrumbItem.Create(newItem, _onClickCommand)); + break; + } + case NotifyCollectionChangedAction.Remove: + BreadcrumbItems.RemoveAt(e.OldStartingIndex); + break; + case NotifyCollectionChangedAction.Replace: + var replaceItem = (INavigationItem) e.NewItems![0]; + var breadcrumbItem = BreadcrumbItem.Create(replaceItem, _onClickCommand); + + BreadcrumbItems[0] = breadcrumbItem; + break; + default: + return; + } + + if (BreadcrumbItems.Count > 1) + BreadcrumbItems[BreadcrumbItems.Count - 2].IsActive = false; + + BreadcrumbItems[BreadcrumbItems.Count - 1].IsActive = true; + } - private static void OnNavigationChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + private void OnClick(object obj) { - if (d is not Breadcrumb breadcrumb) - return; + var pageTag = (string)obj; - breadcrumb.OnNavigationChanged(); + Navigation.NavigateTo(pageTag); } } diff --git a/src/Wpf.Ui/Controls/BreadcrumbItem.cs b/src/Wpf.Ui/Controls/BreadcrumbItem.cs new file mode 100644 index 000000000..48a5cf507 --- /dev/null +++ b/src/Wpf.Ui/Controls/BreadcrumbItem.cs @@ -0,0 +1,51 @@ +using System.Windows; +using System.Windows.Input; +using Wpf.Ui.Controls.Interfaces; + +namespace Wpf.Ui.Controls; + +public class BreadcrumbItem : System.Windows.Controls.Control +{ + public static readonly DependencyProperty TextProperty = DependencyProperty.Register(nameof(Text), + typeof(string), typeof(BreadcrumbItem), new PropertyMetadata(string.Empty)); + + public static readonly DependencyProperty PageTagProperty = DependencyProperty.Register(nameof(PageTag), + typeof(string), typeof(BreadcrumbItem), new PropertyMetadata(string.Empty)); + + public static readonly DependencyProperty OnClickCommandProperty = DependencyProperty.Register(nameof(OnClickCommand), + typeof(ICommand), typeof(BreadcrumbItem), new PropertyMetadata(null)); + + public static readonly DependencyProperty IsActiveProperty = DependencyProperty.Register(nameof(IsActive), + typeof(bool), typeof(BreadcrumbItem), new PropertyMetadata(false)); + + public string Text + { + get => (string)GetValue(TextProperty); + set => SetValue(TextProperty, value); + } + + public string PageTag + { + get => (string)GetValue(PageTagProperty); + set => SetValue(PageTagProperty, value); + } + + public ICommand OnClickCommand + { + get => (ICommand)GetValue(OnClickCommandProperty); + set => SetValue(OnClickCommandProperty, value); + } + + public bool IsActive + { + get => (bool)GetValue(IsActiveProperty); + set => SetValue(IsActiveProperty, value); + } + + public static BreadcrumbItem Create(INavigationItem item, ICommand onClickCommand) => new BreadcrumbItem() + { + Text = item.Content as string ?? string.Empty, + PageTag = item.PageTag, + OnClickCommand = onClickCommand + }; +} diff --git a/src/Wpf.Ui/Controls/Interfaces/INavigation.cs b/src/Wpf.Ui/Controls/Interfaces/INavigation.cs index 3e2592e12..1eeb093aa 100644 --- a/src/Wpf.Ui/Controls/Interfaces/INavigation.cs +++ b/src/Wpf.Ui/Controls/Interfaces/INavigation.cs @@ -3,7 +3,9 @@ // Copyright (C) Leszek Pomianowski and WPF UI Contributors. // All Rights Reserved. +#nullable enable using System; +using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; using System.Windows.Controls; @@ -18,22 +20,12 @@ namespace Wpf.Ui.Controls.Interfaces; /// public interface INavigation { - /// - /// Service providing views. - /// - IPageService PageService { get; set; } - /// /// Navigation item ID of the current page. /// If set to a value less than , no will be loaded during initialization. /// int SelectedPageIndex { get; set; } - /// - /// Navigation item ID of the previous page. - /// - int PreviousPageIndex { get; } - /// /// Creates an instance of all pages defined with after the is loaded. /// @@ -45,9 +37,9 @@ public interface INavigation bool CanGoBack { get; } /// - /// Currently used item like . + /// TODO /// - INavigationItem Current { get; } + ObservableCollection NavigationStack { get; } /// /// Gets or sets the in which the will be loaded after navigation. @@ -58,15 +50,18 @@ public interface INavigation /// /// Gets or sets the list of that will be displayed on the navigation. /// - [Bindable(true)] ObservableCollection Items { get; set; } /// /// Gets or sets the list of which will be displayed at the bottom of the navigation and will not be scrolled. /// - [Bindable(true)] ObservableCollection Footer { get; set; } + /// + /// TODO + /// + List HiddenItems { get; set; } + /// /// Specifies dimension of children stacking. /// @@ -78,18 +73,6 @@ public interface INavigation [Category("Behavior")] event RoutedNavigationEvent Navigated; - /// - /// Gets or sets the that will be triggered during forward navigation. - /// - [Category("Behavior")] - event RoutedNavigationEvent NavigatedForward; - - /// - /// Gets or sets the that will be triggered during backward navigation. - /// - [Category("Behavior")] - event RoutedNavigationEvent NavigatedBackward; - /// /// Gets or sets a value deciding how long the effect of the transition between the pages should take. /// @@ -102,117 +85,33 @@ public interface INavigation [Bindable(true), Category("Appearance")] TransitionType TransitionType { get; set; } - /// - /// Clears all navigation items. - /// - void Flush(); - - /// - /// Clears all initialized instances of the pages. - /// - void ClearCache(); /// - /// Navigates to the previous page using the . + /// TODO /// - /// - bool NavigateBack(); + void SetIPageService(IPageService pageService); /// - /// Navigates to the page using the . + /// /// - /// Type of the page to navigate. - /// if the operation was successful. - bool Navigate(Type pageType); + void Preload(); /// - /// Navigates to the page using the . - /// - /// Type of the page to navigate. - /// When an DataContext changes, all data-bound properties (on this element or any other element) whose Bindings use this DataContext will change to reflect the new value. - /// if the operation was successful. - bool Navigate(Type pageType, object dataContext); - - /// - /// Loads a instance into based on the tag of . - /// - /// ID of the page to be loaded. - /// if the operation was successful. - bool Navigate(int pageIndex); - - /// - /// Loads a instance into based on the tag of . - /// - /// ID of the page to be loaded. - /// When an DataContext changes, all data-bound properties (on this element or any other element) whose Bindings use this DataContext will change to reflect the new value. - /// if the operation was successful. - bool Navigate(int pageIndex, object dataContext); - - /// - /// Loads a instance into based on the tag of . - /// - /// to be loaded. - /// if the operation was successful. - bool Navigate(string pageTag); - - /// - /// Loads a instance into based on the tag of . - /// - /// to be loaded. - /// When an DataContext changes, all data-bound properties (on this element or any other element) whose Bindings use this DataContext will change to reflect the new value. - bool Navigate(string pageTag, object dataContext); - - /// - /// Navigate to the given object that is outside the current navigation. - /// - /// The element you want to navigate to a that is not in the or pool. - /// if the operation was successful. - bool NavigateExternal(object frameworkElement); - - /// - /// Navigate to the given object that is outside the current navigation. - /// - /// The element you want to navigate to a that is not in the or pool. - /// Context of the data for data binding. - /// if the operation was successful. - bool NavigateExternal(object frameworkElement, object dataContext); - - /// - /// Navigate to the given that is outside the current navigation. - /// - /// to the element you want to navigate to a that is not in the or pool. - /// if the operation was successful. - bool NavigateExternal(Uri absolutePageUri); - - /// - /// Navigate to the given that is outside the current navigation. - /// - /// to the element you want to navigate to a that is not in the or pool. - /// Context of the data for data binding. - /// if the operation was successful. - bool NavigateExternal(Uri absolutePageUri, object dataContext); - - /// - /// Sets of the page. - /// If the page is not in the Cache, and is defined based on , its object will be created and then its DataContext will be defined. + /// Clears all initialized instances of the pages. /// - /// Id of the page from or . - /// Context of the data for data binding. - /// if the operation was successful. - bool SetContext(int pageId, object dataContext); + void ClearCache(); /// - /// Sets of the page. - /// If the page is not in the Cache, and is defined based on , its object will be created and then its DataContext will be defined. + /// TODO /// - /// Tag of the page from or . - /// Context of the data for data binding. - /// if the operation was successful. - bool SetContext(string pageTag, object dataContext); + /// + /// + void NavigateTo(string pageTag, object? dataContext = null); /// - /// Tires to set the DataContext for the currently displayed page. + /// TODO /// - /// Data context to be set. - void SetCurrentContext(object dataContext); + /// + /// + void NavigateTo(Type type, object? dataContext = null); } diff --git a/src/Wpf.Ui/Controls/Interfaces/INavigationItem.cs b/src/Wpf.Ui/Controls/Interfaces/INavigationItem.cs index 216171173..9ff4c1fa7 100644 --- a/src/Wpf.Ui/Controls/Interfaces/INavigationItem.cs +++ b/src/Wpf.Ui/Controls/Interfaces/INavigationItem.cs @@ -3,6 +3,7 @@ // Copyright (C) Leszek Pomianowski and WPF UI Contributors. // All Rights Reserved. +#nullable enable using System; using System.ComponentModel; using System.Windows; @@ -43,16 +44,26 @@ public interface INavigationItem /// /// A inherited from that defines page of the item. /// - Type PageType { get; set; } + Type? PageType { get; set; } /// /// Absolute path to the XAML template based on and . /// - Uri AbsolutePageSource { get; } + Uri? AbsolutePageSource { get; } /// /// Add / Remove ClickEvent handler /// [Category("Behavior")] event RoutedEventHandler Click; + + /// + /// TODO + /// + internal bool IsHidden { get; set; } + + /// + /// + /// + internal bool WasInBreadcrumb { get; set; } } diff --git a/src/Wpf.Ui/Controls/Navigation/NavigationBackButton.cs b/src/Wpf.Ui/Controls/Navigation/NavigationBackButton.cs index d1ffefb16..40f0b4d88 100644 --- a/src/Wpf.Ui/Controls/Navigation/NavigationBackButton.cs +++ b/src/Wpf.Ui/Controls/Navigation/NavigationBackButton.cs @@ -34,6 +34,6 @@ public INavigation? Navigation public NavigationBackButton() { - SetValue(CommandProperty, new Common.RelayCommand(_ => Navigation?.NavigateBack(), () => Navigation is not null && Navigation.CanGoBack)); + SetValue(CommandProperty, new Common.RelayCommand(_ => Navigation?.NavigateTo(".."), () => Navigation is not null && Navigation.CanGoBack)); } } diff --git a/src/Wpf.Ui/Controls/Navigation/NavigationBase.cs b/src/Wpf.Ui/Controls/Navigation/NavigationBase.cs index bc8ea838c..4a29a17e9 100644 --- a/src/Wpf.Ui/Controls/Navigation/NavigationBase.cs +++ b/src/Wpf.Ui/Controls/Navigation/NavigationBase.cs @@ -4,19 +4,19 @@ // All Rights Reserved. #nullable enable -#pragma warning disable CS8600 -#pragma warning disable CS8603 using System; +using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Collections.Specialized; +using System.ComponentModel; using System.Windows; using System.Windows.Controls; using System.Windows.Input; +using CommunityToolkit.Diagnostics; using Wpf.Ui.Common; using Wpf.Ui.Controls.Interfaces; using Wpf.Ui.Mvvm.Contracts; -using Wpf.Ui.Mvvm.Interfaces; +using Wpf.Ui.Services.Internal; namespace Wpf.Ui.Controls.Navigation; @@ -25,24 +25,33 @@ namespace Wpf.Ui.Controls.Navigation; /// public abstract class NavigationBase : System.Windows.Controls.Control, INavigation { - /// - /// Service used for navigation purposes. - /// - private readonly Services.Internal.NavigationService? _navigationService; + private FrameManager _frameManager = null!; + private NavigationManager _navigationManager = null!; + private IPageService? _pageService; + private bool _loaded; + + #region DependencyProperties /// /// Property for . /// public static readonly DependencyProperty ItemsProperty = DependencyProperty.Register(nameof(Items), typeof(ObservableCollection), typeof(NavigationBase), - new PropertyMetadata((ObservableCollection)null, OnItemsChanged)); + new PropertyMetadata(null)); /// /// Property for . /// public static readonly DependencyProperty FooterProperty = DependencyProperty.Register(nameof(Footer), typeof(ObservableCollection), typeof(NavigationBase), - new PropertyMetadata((ObservableCollection)null, OnFooterChanged)); + new PropertyMetadata(null)); + + /// + /// Property for . + /// + public static readonly DependencyProperty HiddenItemsProperty = DependencyProperty.Register(nameof(HiddenItems), + typeof(List), typeof(NavigationBase), + new PropertyMetadata(null)); /// /// Property for . @@ -57,7 +66,7 @@ public abstract class NavigationBase : System.Windows.Controls.Control, INavigat /// public static readonly DependencyProperty FrameProperty = DependencyProperty.Register(nameof(Frame), typeof(Frame), typeof(NavigationBase), - new PropertyMetadata((Frame)null, OnFrameChanged)); + new PropertyMetadata()); /// /// Property for . @@ -98,20 +107,31 @@ public abstract class NavigationBase : System.Windows.Controls.Control, INavigat nameof(NavigationParent), typeof(INavigation), typeof(NavigationBase), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.Inherits)); + #endregion + + #region Properties + /// public ObservableCollection Items { - get => GetValue(ItemsProperty) as ObservableCollection; + get => (ObservableCollection) GetValue(ItemsProperty); set => SetValue(ItemsProperty, value); } /// public ObservableCollection Footer { - get => GetValue(FooterProperty) as ObservableCollection; + get => (ObservableCollection) GetValue(FooterProperty); set => SetValue(FooterProperty, value); } + /// + public List HiddenItems + { + get => (List) GetValue(HiddenItemsProperty); + set => SetValue(HiddenItemsProperty, value); + } + /// [Obsolete("Work in progress.")] public Orientation Orientation @@ -135,9 +155,9 @@ public Animations.TransitionType TransitionType } /// - public Frame? Frame + public Frame Frame { - get => GetValue(FrameProperty) as Frame; + get => (GetValue(FrameProperty) as Frame)!; set => SetValue(FrameProperty, value); } @@ -161,6 +181,8 @@ internal INavigation NavigationParent private set => SetValue(NavigationParentProperty, value); } + #endregion + #region Events /// @@ -176,51 +198,12 @@ public event RoutedNavigationEvent Navigated remove => RemoveHandler(NavigatedEvent, value); } - /// - /// Event triggered when navigate to the next page. - /// - public static readonly RoutedEvent NavigatedForwardEvent = - EventManager.RegisterRoutedEvent(nameof(NavigatedForward), RoutingStrategy.Bubble, - typeof(RoutedNavigationEvent), typeof(NavigationBase)); - - /// - public event RoutedNavigationEvent NavigatedForward - { - add => AddHandler(NavigatedForwardEvent, value); - remove => RemoveHandler(NavigatedForwardEvent, value); - } - - /// - /// Event triggered when navigate to the previous page. - /// - public static readonly RoutedEvent NavigatedBackwardEvent = - EventManager.RegisterRoutedEvent(nameof(NavigatedBackward), RoutingStrategy.Bubble, - typeof(RoutedNavigationEvent), typeof(NavigationBase)); - - /// - public event RoutedNavigationEvent NavigatedBackward - { - add => AddHandler(NavigatedBackwardEvent, value); - remove => RemoveHandler(NavigatedBackwardEvent, value); - } - #endregion /// - public IPageService? PageService - { - get => _navigationService?.GetService(); - set => _navigationService?.SetService(value); - } + public bool CanGoBack => !DesignerProperties.GetIsInDesignMode(this) && _navigationManager.CanGoBack; - /// - public int PreviousPageIndex => _navigationService?.GetPreviousId() ?? 0; - - /// - public bool CanGoBack => _navigationService is not null && _navigationService.CanGoBack; - - /// - public INavigationItem? Current { get; internal set; } + public ObservableCollection NavigationStack => _navigationManager.NavigationStack; /// /// Static constructor overriding default properties. @@ -241,234 +224,75 @@ static NavigationBase() /// protected NavigationBase() { - Current = (INavigationItem)null; - - // Prepare individual collections for this navigation - Items ??= new ObservableCollection(); - Footer ??= new ObservableCollection(); - - _navigationService = new Wpf.Ui.Services.Internal.NavigationService(); - _navigationService.TransitionDuration = TransitionDuration; - _navigationService.TransitionType = TransitionType; - - if (Frame != null) - _navigationService.SetFrame(Frame); + Items = new ObservableCollection(); + Footer = new ObservableCollection(); + HiddenItems = new List(); // Let the NavigationItem children be able to get me. NavigationParent = this; - // Loaded does not have override - Loaded += OnLoaded; - } - - public bool NavigateBack() - { - if (_navigationService is null) return false; - - if (!_navigationService.NavigateBack()) - return false; - - NavigateInternal(0, true); - - return true; - } - - /// - public bool Navigate(Type pageType) - { - return Navigate(pageType, null); - } - - /// - public bool Navigate(Type pageType, object? dataContext) - { - if (!_navigationService.Navigate(pageType, dataContext)) - return false; - - NavigateInternal(0, true); - - return true; - } - - /// - public bool Navigate(string pageTag) - { - return Navigate(pageTag, null); - } - - /// - public bool Navigate(string pageTag, object? dataContext) - { - if (!_navigationService.Navigate(pageTag, dataContext)) - return false; - - NavigateInternal(0, true); - - return true; - } - - - /// - public bool Navigate(int pageId) - { - return Navigate(pageId, null); - } - - /// - public bool Navigate(int pageId, object? dataContext) - { - if (_navigationService != null) - if (!_navigationService.Navigate(pageId, dataContext)) - return false; - - NavigateInternal(-1, true); - - return true; - } - - /// - public bool NavigateExternal(object frameworkElement) - { - return NavigateExternal(frameworkElement, null); - } - - /// - public bool NavigateExternal(object frameworkElement, object? dataContext) - { - if (_navigationService != null) - if (!_navigationService.NavigateExternal(frameworkElement, dataContext)) - return false; - - NavigateInternal(-1, true); - - return true; - } - - /// - public bool NavigateExternal(Uri absolutePageUri) - { - return NavigateExternal(absolutePageUri, null); - } - - /// - public bool NavigateExternal(Uri absolutePageUri, object? dataContext) - { - if (_navigationService != null) - if (!_navigationService.NavigateExternal(absolutePageUri, dataContext)) - return false; - - NavigateInternal(-1, false); - - return true; - } - - /// - public void SetCurrentContext(object dataContext) - { - if (Frame?.Content is not FrameworkElement) + if (DesignerProperties.GetIsInDesignMode(this)) return; - ((FrameworkElement)Frame.Content).DataContext = dataContext; - - if (dataContext is IViewModel) - ((IViewModel)dataContext).OnMounted(((FrameworkElement)Frame.Content)); + Loaded += OnLoaded; + Unloaded += OnUnloaded; } /// - public bool SetContext(string pageTag, object dataContext) + public void SetIPageService(IPageService pageService) { - if (_navigationService == null) - return false; - - return _navigationService.SetContext(pageTag, dataContext); + _pageService = pageService; } /// - public bool SetContext(int pageId, object dataContext) - { - if (_navigationService == null) - return false; + public void Preload() => _navigationManager.Preload(); - return _navigationService.SetContext(pageId, dataContext); - } + /// + public void ClearCache() => _navigationManager.ClearCache(); /// - public void Flush() + public void NavigateTo(string pageTag, object? dataContext = null) { - Items.Clear(); - Footer.Clear(); - - Current = (INavigationItem)null; + if (_navigationManager.NavigateTo(pageTag, dataContext)) + OnNavigated(); } /// - public void ClearCache() + public void NavigateTo(Type type, object? dataContext = null) { - if (_navigationService == null) - return; - - _navigationService.ClearCache(); + if (_navigationManager.NavigateTo(type, dataContext)) + OnNavigated(); } /// - /// Updates property and modifies Active attribute of navigation items. + /// This virtual method is called when is loaded. /// - private void UpdateItems() + protected virtual void OnLoaded(object sender, RoutedEventArgs e) { - var currentTag = _navigationService?.GetCurrentTag() ?? String.Empty; - - foreach (var singleNavigationControl in Items) - { - if (singleNavigationControl is not INavigationItem) - continue; + Guard.IsNotNull(Frame, nameof(Frame)); - if (((INavigationItem)singleNavigationControl).PageTag == currentTag) - { - ((INavigationItem)singleNavigationControl).IsActive = true; - Current = (INavigationItem)singleNavigationControl; - } - else - { - ((INavigationItem)singleNavigationControl).IsActive = false; - } - } + _frameManager = new FrameManager(Frame, TransitionDuration, TransitionType); + _navigationManager = new NavigationManager(Frame, _pageService, MergeItems()); - foreach (var singleNavigationControl in Footer) + if (SelectedPageIndex > -1) { - if (singleNavigationControl is not INavigationItem) - continue; - - if (((INavigationItem)singleNavigationControl).PageTag == currentTag) - { - ((INavigationItem)singleNavigationControl).IsActive = true; - Current = (INavigationItem)singleNavigationControl; - } - else - { - ((INavigationItem)singleNavigationControl).IsActive = false; - } + _navigationManager.NavigateTo(SelectedPageIndex); + OnNavigated(); } + + _loaded = true; } - /// - /// This virtual method is called when is loaded. - /// - protected virtual void OnLoaded(object sender, RoutedEventArgs e) + protected virtual void OnUnloaded(object sender, RoutedEventArgs e) { - UpdateServiceItems(); - - if (PageService == null && Frame != null && SelectedPageIndex > -1) - Navigate(SelectedPageIndex); + Loaded -= OnLoaded; + Unloaded -= OnUnloaded; - // If we are using the MVVM model, do not use the cache. - if (Precache) - { - if (PageService != null) - throw new InvalidOperationException("The cache cannot be used if you are using IPageService."); + foreach (var item in _navigationManager.NavigationItems) + item.Click -= OnNavigationItemClicked; - // TODO: Precache - //await PrecacheInstances(); - } + _frameManager.Dispose(); + _navigationManager.Dispose(); } /// @@ -521,25 +345,9 @@ static void MoveFocus(FrameworkElement element, FocusNavigationDirection directi /// protected virtual void OnNavigated() { - var newEvent = new RoutedNavigationEventArgs(NavigatedEvent, this, Current); - RaiseEvent(newEvent); - } + var navigatedFrom = _navigationManager.History.Count > 1 ? _navigationManager.NavigationItems[_navigationManager.History[_navigationManager.History.Count - 2]] : null; - /// - /// This virtual method is called during forward navigation and it raises the . - /// - protected virtual void OnNavigatedForward() - { - var newEvent = new RoutedNavigationEventArgs(NavigatedForwardEvent, this, Current); - RaiseEvent(newEvent); - } - - /// - /// This virtual method is called during backward navigation and it raises the . - /// - protected virtual void OnNavigatedBackward() - { - var newEvent = new RoutedNavigationEventArgs(NavigatedBackwardEvent, this, Current); + var newEvent = new RoutedNavigationEventArgs(NavigatedEvent, this, navigatedFrom, NavigationStack[NavigationStack.Count - 1]); RaiseEvent(newEvent); } @@ -551,94 +359,7 @@ protected virtual void OnNavigationItemClicked(object sender, RoutedEventArgs e) if (sender is not INavigationItem navigationItem) return; - if (navigationItem.AbsolutePageSource == null && navigationItem.PageType == null) - return; - - if (PageService == null) - { - Navigate(navigationItem.PageTag); - - return; - } - - if (navigationItem.PageType == null) - throw new InvalidOperationException("When navigating through the IPageService, the navigated page type must be defined the INavigationItem.PageType."); - - Navigate(navigationItem.PageType); - } - - /// - /// This virtual method is called when something is added, deleted or changed in or . - /// - protected virtual void OnNavigationCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) - { - if (IsLoaded) - UpdateServiceItems(); - - if (e.NewItems != null) - foreach (var addedItem in e.NewItems) - if (addedItem is INavigationItem) - { - ((INavigationItem)addedItem).Click -= OnNavigationItemClicked; // Unsafe - Remove duplicates - ((INavigationItem)addedItem).Click += OnNavigationItemClicked; - } - - if (e.OldItems == null) - return; - - foreach (var deletedItem in e.OldItems) - ((INavigationItem)deletedItem).Click -= OnNavigationItemClicked; - } - - /// - /// Triggered when is changed. - /// - private static void OnItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - if (d is not NavigationBase navigationBase) - return; - - navigationBase.InitializeServiceItems(); - - if (e.NewValue is not ObservableCollection itemsCollection) - return; - - itemsCollection.CollectionChanged += navigationBase.OnNavigationCollectionChanged; - } - - /// - /// Triggered when is changed. - /// - private static void OnFooterChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - if (d is not NavigationBase navigationBase) - return; - - navigationBase.InitializeServiceItems(); - - if (e.NewValue is not ObservableCollection itemsCollection) - return; - - itemsCollection.CollectionChanged += navigationBase.OnNavigationCollectionChanged; - } - - /// - /// This virtual method is called when one of the navigation items is clicked. - /// - protected virtual void OnFrameChanged(Frame frame) - { - _navigationService?.SetFrame(frame); - } - - /// - /// Triggered when is changed. - /// - private static void OnFrameChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - if (d is not NavigationBase navigationBase || e.NewValue is not Frame frame) - return; - - navigationBase.OnFrameChanged(frame); + NavigateTo(navigationItem.PageTag); } private static void OnTransitionDurationChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) @@ -646,10 +367,8 @@ private static void OnTransitionDurationChanged(DependencyObject d, DependencyPr if (d is not NavigationBase navigation) return; - if (navigation._navigationService == null) - return; - - navigation._navigationService.TransitionDuration = (int)e.NewValue; + if (navigation._loaded) + navigation._frameManager.TransitionDuration = (int)e.NewValue; } private static void OnTransitionTypeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) @@ -657,10 +376,8 @@ private static void OnTransitionTypeChanged(DependencyObject d, DependencyProper if (d is not NavigationBase navigation) return; - if (navigation._navigationService == null) - return; - - navigation._navigationService.TransitionType = (Animations.TransitionType)e.NewValue; + if (navigation._loaded) + navigation._frameManager.TransitionType = (Animations.TransitionType)e.NewValue; } /// @@ -674,49 +391,28 @@ private static void OnTransitionTypeChanged(DependencyObject d, DependencyProper return (NavigationBase?)navigationItem.GetValue(NavigationParentProperty); } - private void InitializeServiceItems() - { - var navigationItems = GetValue(ItemsProperty) as ObservableCollection ?? new ObservableCollection { }; - var navigationFooter = GetValue(FooterProperty) as ObservableCollection ?? new ObservableCollection { }; - - foreach (var addedItem in navigationItems) - if (addedItem is INavigationItem) - { - ((INavigationItem)addedItem).Click -= OnNavigationItemClicked; // Unsafe - Remove duplicates - ((INavigationItem)addedItem).Click += OnNavigationItemClicked; - } - - foreach (var addedItem in navigationFooter) - if (addedItem is INavigationItem) - { - ((INavigationItem)addedItem).Click -= OnNavigationItemClicked; // Unsafe - Remove duplicates - ((INavigationItem)addedItem).Click += OnNavigationItemClicked; - } - - UpdateServiceItems(); - } - - private void UpdateServiceItems() + private INavigationItem[] MergeItems() { - var navigationItems = GetValue(ItemsProperty) as ObservableCollection ?? new ObservableCollection { }; - var navigationFooter = GetValue(FooterProperty) as ObservableCollection ?? new ObservableCollection { }; + List buffer = new List(Items.Count); - if (_navigationService != null) - _navigationService.UpdateItems(navigationItems, navigationFooter); - } + AddToBuffer(Items); + AddToBuffer(Footer); + AddToBuffer(HiddenItems, item => item.IsHidden = true); - private void NavigateInternal(int arg, bool updateItems) - { - SelectedPageIndex = _navigationService?.GetCurrentId() ?? +arg; + return buffer.ToArray(); - if (updateItems) - UpdateItems(); + void AddToBuffer(IEnumerable list, Action? action = null) + { + foreach (var addedItem in list) + { + if (addedItem is not INavigationItem item) continue; - OnNavigated(); + if (item.PageType is not null) + item.Click += OnNavigationItemClicked; - if (SelectedPageIndex > (_navigationService?.GetPreviousId() ?? +arg)) - OnNavigatedForward(); - else - OnNavigatedBackward(); + action?.Invoke(item); + buffer.Add(item); + } + } } } diff --git a/src/Wpf.Ui/Controls/NavigationItem.cs b/src/Wpf.Ui/Controls/NavigationItem.cs index 36fc11fb3..427cd3781 100644 --- a/src/Wpf.Ui/Controls/NavigationItem.cs +++ b/src/Wpf.Ui/Controls/NavigationItem.cs @@ -237,6 +237,12 @@ protected virtual Uri BaseUri set => SetValue(BaseUriHelper.BaseUriProperty, value); } + /// + bool INavigationItem.IsHidden { get; set; } + + /// + bool INavigationItem.WasInBreadcrumb { get; set; } + /// protected override void OnContentChanged(object oldContent, object newContent) { @@ -293,7 +299,7 @@ protected override void OnKeyDown(KeyEventArgs e) { e.Handled = true; - navigation.Navigate(pageTag); + navigation.NavigateTo(pageTag); } break; } diff --git a/src/Wpf.Ui/Mvvm/Contracts/INavigationService.cs b/src/Wpf.Ui/Mvvm/Contracts/INavigationService.cs index 1914de8c0..73bf29cfa 100644 --- a/src/Wpf.Ui/Mvvm/Contracts/INavigationService.cs +++ b/src/Wpf.Ui/Mvvm/Contracts/INavigationService.cs @@ -3,6 +3,7 @@ // Copyright (C) Leszek Pomianowski and WPF UI Contributors. // All Rights Reserved. +#nullable enable using System; using System.Windows.Controls; using Wpf.Ui.Controls.Interfaces; @@ -15,6 +16,14 @@ namespace Wpf.Ui.Mvvm.Contracts; /// public interface INavigationService { + + /// + /// TODO + /// + /// + /// + void Initialize(INavigation navigation, IPageService pageService); + /// /// Provides direct access to the used in navigation. /// @@ -38,7 +47,6 @@ public interface INavigationService /// /// Instance of the . void SetNavigationControl(INavigation navigation); - /// /// Lets you attach the service that delivers page instances to . /// @@ -46,20 +54,16 @@ public interface INavigationService void SetPageService(IPageService pageService); /// - /// Lets you navigate to the selected page based on it's type. Should be used with . - /// - /// of the page. - bool Navigate(Type pageType); - - /// - /// Lets you navigate to the selected page based on it's id. Should be used with . + /// Lets you navigate to the selected page based on it's tag. Should be used with . /// - /// Id of the page. - bool Navigate(int pageId); + /// Tag of the page. + /// + void NavigateTo(string pageTag, object? dataContext = null); /// - /// Lets you navigate to the selected page based on it's tag. Should be used with . + /// TODO /// - /// Tag of the page. - bool Navigate(string pageTag); + /// + /// + void NavigateTo(Type type, object? dataContext = null); } diff --git a/src/Wpf.Ui/Mvvm/Contracts/INavigationWindow.cs b/src/Wpf.Ui/Mvvm/Contracts/INavigationWindow.cs index 6d46035a2..62515ebe4 100644 --- a/src/Wpf.Ui/Mvvm/Contracts/INavigationWindow.cs +++ b/src/Wpf.Ui/Mvvm/Contracts/INavigationWindow.cs @@ -3,6 +3,7 @@ // Copyright (C) Leszek Pomianowski and WPF UI Contributors. // All Rights Reserved. +#nullable enable using System; using System.Windows.Controls; using Wpf.Ui.Controls.Interfaces; @@ -27,12 +28,6 @@ public interface INavigationWindow /// Instance of the control. INavigation GetNavigation(); - /// - /// Lets you navigate to the selected page based on it's type. Should be used with . - /// - /// of the page. - bool Navigate(Type pageType); - /// /// Lets you attach the service that delivers page instances to . /// @@ -48,4 +43,18 @@ public interface INavigationWindow /// Triggers the command to close a window. /// void CloseWindow(); + + /// + /// TODO + /// + /// + /// + void NavigateTo(string pageTag, object? dataContext = null); + + /// + /// TODO + /// + /// + /// + void NavigateTo(Type type, object? dataContext = null); } diff --git a/src/Wpf.Ui/Mvvm/Services/NavigationService.cs b/src/Wpf.Ui/Mvvm/Services/NavigationService.cs index 4818c102d..0d973fddc 100644 --- a/src/Wpf.Ui/Mvvm/Services/NavigationService.cs +++ b/src/Wpf.Ui/Mvvm/Services/NavigationService.cs @@ -3,8 +3,10 @@ // Copyright (C) Leszek Pomianowski and WPF UI Contributors. // All Rights Reserved. +#nullable enable using System; using System.Windows.Controls; +using CommunityToolkit.Diagnostics; using Wpf.Ui.Controls.Interfaces; using Wpf.Ui.Mvvm.Contracts; @@ -15,28 +17,41 @@ namespace Wpf.Ui.Mvvm.Services; /// public partial class NavigationService : INavigationService { + private INavigation? _navigationControl; + /// /// Locally attached page service. /// - private IPageService _pageService; + private IPageService? _pageService; /// /// Control representing navigation. /// - protected INavigation NavigationControl; + private INavigation NavigationControl + { + get + { + Guard.IsNotNull(_navigationControl, nameof(NavigationControl)); + return _navigationControl; + } + set => _navigationControl = value; + } + + public void Initialize(INavigation navigation, IPageService pageService) + { + NavigationControl = navigation; + NavigationControl.SetIPageService(pageService); + } /// public Frame GetFrame() { - return NavigationControl?.Frame; + return NavigationControl.Frame; } /// public void SetFrame(Frame frame) { - if (NavigationControl == null) - return; - NavigationControl.Frame = frame; } @@ -46,52 +61,34 @@ public INavigation GetNavigationControl() return NavigationControl; } - /// public void SetNavigationControl(INavigation navigation) { NavigationControl = navigation; - if (_pageService != null) - NavigationControl.PageService = _pageService; + if (_pageService is not null) + NavigationControl.SetIPageService(_pageService); } - /// public void SetPageService(IPageService pageService) { - if (NavigationControl == null) + if (_navigationControl is null) { _pageService = pageService; - return; } - NavigationControl.PageService = pageService; - } - - /// - public bool Navigate(Type pageType) - { - if (NavigationControl == null) - return false; - - return NavigationControl.Navigate(pageType); + NavigationControl.SetIPageService(pageService); } /// - public bool Navigate(int pageId) + public void NavigateTo(string pageTag, object? dataContext = null) { - if (NavigationControl == null) - return false; - - return NavigationControl.Navigate(pageId); + NavigationControl.NavigateTo(pageTag, dataContext); } /// - public bool Navigate(string pageTag) + public void NavigateTo(Type type, object? dataContext = null) { - if (NavigationControl == null) - return false; - - return NavigationControl.Navigate(pageTag); + NavigationControl.NavigateTo(type, dataContext); } } diff --git a/src/Wpf.Ui/Services/Internal/FrameManager.cs b/src/Wpf.Ui/Services/Internal/FrameManager.cs new file mode 100644 index 000000000..d2fce91e0 --- /dev/null +++ b/src/Wpf.Ui/Services/Internal/FrameManager.cs @@ -0,0 +1,81 @@ +#nullable enable +using System; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Navigation; +using Wpf.Ui.Animations; +using Wpf.Ui.Common.Interfaces; + +namespace Wpf.Ui.Services.Internal; + +internal sealed class FrameManager : IDisposable +{ + private readonly Frame _frame; + + public int TransitionDuration { get; set; } + public TransitionType TransitionType { get; set; } + + public FrameManager(Frame frame, int transitionDuration, TransitionType transitionType) + { + _frame = frame; + TransitionDuration = transitionDuration; + TransitionType = transitionType; + + _frame.NavigationUIVisibility = NavigationUIVisibility.Hidden; + + _frame.Navigating += OnFrameNavigating; + _frame.Navigated += OnFrameNavigated; + } + + public void Dispose() + { + _frame.Navigating -= OnFrameNavigating; + _frame.Navigated -= OnFrameNavigated; + } + + private void OnFrameNavigating(object sender, NavigatingCancelEventArgs e) + { + NotifyFrameContentAboutLeave(); + } + + private void OnFrameNavigated(object sender, NavigationEventArgs e) + { + _frame.NavigationService.RemoveBackEntry(); + + if (TransitionDuration > 0 && e.Content != null) + Transitions.ApplyTransition(e.Content, TransitionType, TransitionDuration); + + //TODO + /*// Finally, the navigation took place internally, + // the context was set from extra data, the cache has to be saved, + // so we save it, notify it and this is the end of the method + _navigationServiceItems[extraData.PageId].Instance = _frame.Content;*/ + + NotifyFrameContentAboutEnter(); + } + + private void NotifyFrameContentAboutEnter() + { + var navigationAware = GetINavigationAwareOrDefault(); + navigationAware?.OnNavigatedTo(); + } + + private void NotifyFrameContentAboutLeave() + { + var navigationAware = GetINavigationAwareOrDefault(); + navigationAware?.OnNavigatedFrom(); + } + + private INavigationAware? GetINavigationAwareOrDefault() + { + INavigationAware? navigationAware = _frame.Content switch + { + INavigationAware aware => aware, + INavigableView {ViewModel: INavigationAware viewModelNavigationAware} => viewModelNavigationAware, + FrameworkElement {DataContext: INavigationAware dataContextNavigationAware} => dataContextNavigationAware, + _ => null + }; + + return navigationAware; + } +} diff --git a/src/Wpf.Ui/Services/Internal/NavigationManager.cs b/src/Wpf.Ui/Services/Internal/NavigationManager.cs new file mode 100644 index 000000000..78d7a3629 --- /dev/null +++ b/src/Wpf.Ui/Services/Internal/NavigationManager.cs @@ -0,0 +1,238 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Windows; +using System.Windows.Controls; +using CommunityToolkit.Diagnostics; +using Wpf.Ui.Common.Interfaces; +using Wpf.Ui.Controls.Interfaces; +using Wpf.Ui.Mvvm.Contracts; + +namespace Wpf.Ui.Services.Internal; + +internal sealed class NavigationManager : IDisposable +{ + private readonly NavigationStackManager _navigationStackManager; + private readonly Frame _frame; + private readonly FrameworkElement?[] _instances; + private readonly IPageService? _pageService; + + private bool _isBackwardsNavigated; + private bool _addToNavigationStack; + + public bool CanGoBack => History.Count > 1; + public readonly List History = new(); + public ObservableCollection NavigationStack => _navigationStackManager.NavigationStack; + public readonly INavigationItem[] NavigationItems; + + public NavigationManager(Frame frame, IPageService? pageService, INavigationItem[] navigationItems) + { + _instances = new FrameworkElement[navigationItems.Length]; + + NavigationItems = navigationItems; + _frame = frame; + _pageService = pageService; + + _navigationStackManager = new NavigationStackManager(this); + } + + #region Public methods + + public void Dispose() + { + _navigationStackManager.Dispose(); + ClearCache(); + } + + public void Preload() + { + //Why URI + //Application.LoadComponent() + + ThrowHelper.ThrowNotSupportedException("Preloading currently not supported"); + } + + public void ClearCache() + { + for (int i = 0; i < _instances.Length; i++) + { + _instances[i] = null; + } + } + + public bool NavigateTo(string tag, object? dataContext = null) + { + Guard.IsNotNullOrEmpty(tag, nameof(tag)); + + if (tag == "..") + { + return NavigateBack(); + } + + _addToNavigationStack = tag.Contains("/"); + if (_addToNavigationStack) + tag = tag.Remove(0, 1); + + var itemId = GetItemId(item => item.PageTag == tag); + if (itemId < 0) + ThrowHelper.ThrowArgumentException($"Item with: {tag} tag not found"); + + return NavigateInternal(itemId, dataContext); + } + + public bool NavigateTo(Type type, object? dataContext = null) + { + var itemId = GetItemId(serviceItem => serviceItem.PageType == type); + if (itemId < 0) + ThrowHelper.ThrowArgumentException($"Item with: {type} type not found"); + + return NavigateInternal(itemId, dataContext); + } + + public void NavigateTo(int id, object? dataContext = null) + { + NavigateInternal(id, dataContext); + } + + public int GetItemId(Func prediction) + { + int selectedIndex = -1; + + for (int i = 0; i < NavigationItems.Length; i++) + { + if (!prediction.Invoke(NavigationItems[i])) continue; + + selectedIndex = i; + break; + } + + return selectedIndex; + } + + #endregion + + #region NavigationInternal + + private bool NavigateBack() + { + if (History.Count <= 1) + return false; + + var itemId = History[History.Count - 2]; + _isBackwardsNavigated = true; + return NavigateInternal(itemId, null); + } + + private bool NavigateInternal(int itemId, object? dataContext) + { + if (NavigationItems.ElementAtOrDefault(itemId) is not { } item) + return false; + + var instance = GetFrameworkElement((itemId, item), dataContext); + + if (!CheckForNavigationCanceling(instance)) + { + _addToNavigationStack = false; + return false; + } + + if (!_navigationStackManager.AddFirstItemAndCheckIfNavigatingToCurrentItem(item)) + return false; + + _navigationStackManager.AddToNavigationStack(item, _addToNavigationStack, _isBackwardsNavigated); + ActivateItem(item); + AddToHistory(itemId); + + _frame.Navigate(instance); + _addToNavigationStack = false; + return true; + } + + private void ActivateItem(INavigationItem item) + { + if (NavigationStack.Count > 1) + { + if (NavigationStack[NavigationStack.Count - 1].IsHidden) + item.IsActive = true; + } + else + item.IsActive = true; + } + + private void AddToHistory(int itemId) + { + if (_isBackwardsNavigated) + { + _isBackwardsNavigated = false; + History.RemoveAt(History.LastIndexOf(History[History.Count - 2])); + History.RemoveAt(History.LastIndexOf(History[History.Count - 1])); + } + + History.Add(itemId); + } + + private bool CheckForNavigationCanceling(FrameworkElement instance) + { + INavigationCancelable? navigationCancelable = instance switch + { + INavigationCancelable cancelable => cancelable, + {DataContext: INavigationCancelable dataContextNavigationCancelable} => dataContextNavigationCancelable, + _ => null + }; + + if (navigationCancelable is null) + return true; + + var navigationFrom = History.Count > 0 ? NavigationItems[History[History.Count - 1]] : null; + return navigationCancelable.CouldNavigate(navigationFrom); + } + + #endregion + + #region PrivateMethods + + private FrameworkElement GetFrameworkElement((int itemId, INavigationItem item) itemData, object? dataContext) + { + Guard.IsNotNull(itemData.item.PageType, nameof(itemData.item.PageType)); + + if (_pageService is not null && _pageService!.GetPage(itemData.item.PageType) is { } fromServicesElement) + return fromServicesElement; + + + if (itemData.item.Cache) + return GetFrameworkElementFromCache(itemData, dataContext); + + if (!itemData.item.Cache && NavigationServiceActivator.CreateInstance(itemData.item.PageType, dataContext) is { } element) + { + if (dataContext is not null) + element.DataContext = dataContext; + + return element; + } + + ThrowHelper.ThrowArgumentException("Failed to create instance"); + return null; + } + + private FrameworkElement GetFrameworkElementFromCache((int itemId, INavigationItem item) itemData, object? dataContext) + { + if (_instances[itemData.itemId] is not null) + return _instances[itemData.itemId]!; + + if (NavigationServiceActivator.CreateInstance(itemData.item.PageType, dataContext) is not { } element) + { + ThrowHelper.ThrowArgumentNullException("Failed to create instance"); + return null; + } + + if (dataContext is not null) + element.DataContext = dataContext; + + _instances[itemData.itemId] = element; + return _instances[itemData.itemId]!; + } + + #endregion +} diff --git a/src/Wpf.Ui/Services/Internal/NavigationStackManager.cs b/src/Wpf.Ui/Services/Internal/NavigationStackManager.cs new file mode 100644 index 000000000..a2f59c6f2 --- /dev/null +++ b/src/Wpf.Ui/Services/Internal/NavigationStackManager.cs @@ -0,0 +1,160 @@ +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using Wpf.Ui.Controls.Interfaces; + +namespace Wpf.Ui.Services.Internal; + +internal sealed class NavigationStackManager : IDisposable +{ + private readonly NavigationManager _navigationManager; + private readonly ArrayPool _arrayPool = ArrayPool.Create(); + private readonly Dictionary _complexHistory = new(); + public readonly ObservableCollection NavigationStack = new(); + + public NavigationStackManager(NavigationManager navigationManager) + { + _navigationManager = navigationManager; + } + + public void Dispose() + { + NavigationStack.Clear(); + _complexHistory.Clear(); + } + + public bool AddFirstItemAndCheckIfNavigatingToCurrentItem(INavigationItem item) + { + switch (NavigationStack.Count) + { + case > 0 when NavigationStack[NavigationStack.Count -1] == item: + return false; + case 0: + NavigationStack.Add(item); + return true; + } + + return true; + } + + public void AddToNavigationStack(INavigationItem item, bool addToNavigationStack, bool isBackwardsNavigated) + { + if (isBackwardsNavigated) + RecreateBreadcrumbsFromHistory(item); + + if (addToNavigationStack && !NavigationStack.Contains(item)) + { + item.WasInBreadcrumb = true; + NavigationStack.Add(item); + } + + if (!addToNavigationStack) + UpdateCurrentItem(item); + + ClearNavigationStack(item); + } + + private void UpdateCurrentItem(INavigationItem item) + { + if (item.IsHidden || NavigationStack.Contains(item)) + return; + + if (NavigationStack.Count > 1) + AddToHistory(item); + + NavigationStack[0].IsActive = false; + NavigationStack[0] = item; + + ClearNavigationStack(1); + } + + private void RecreateBreadcrumbsFromHistory(INavigationItem item) + { + if (!item.WasInBreadcrumb && !_complexHistory.ContainsKey(item)) + return; + + var history = _complexHistory[item]; + + var startIndex = 0; + + var index = _navigationManager.GetItemId(navigationItem => navigationItem == history[0]); + if (index >= 0 && !history[0].IsHidden) + { + startIndex = 1; + NavigationStack[0].IsActive = false; + NavigationStack[0] = history[0]; + } + + for (var i = startIndex; i < history.Length; i++) + { + var historyItem = history[i]; + AddToNavigationStack(historyItem, true, false); + historyItem.WasInBreadcrumb = false; + } + + _complexHistory.Remove(item); + AddToNavigationStack(item, true, false); + } + + private void ClearNavigationStack(INavigationItem item) + { + var navigationStackCount = NavigationStack.Count; + if (navigationStackCount <= 1) + return; + + var navItem = NavigationStack[NavigationStack.Count - 2]; + if (navItem.IsHidden) + navItem.IsActive = false; + + var index = NavigationStack.IndexOf(item); + if (index >= navigationStackCount - 1 || _complexHistory.ContainsKey(item)) + return; + + AddToHistory(item); + + ClearNavigationStack(++index); + } + + private void ClearNavigationStack(int navigationStackItemIndex) + { + var navigationStackCount = NavigationStack.Count; + var length = navigationStackCount - navigationStackItemIndex; + var buffer = _arrayPool.Rent(length); + + int i = 0; + for (int j = navigationStackItemIndex; j <= navigationStackCount - 1; j++) + { + buffer[i] = NavigationStack[j]; + i++; + } + + for (var index = 0; index < length; index++) + { + var item = buffer[index]; + NavigationStack.Remove(item); + } + + _arrayPool.Return(buffer, true); + } + + private void AddToHistory(INavigationItem item) + { + var lastItem = NavigationStack[NavigationStack.Count - 1]; + var startIndex = NavigationStack.IndexOf(item); + if (startIndex < 0) + startIndex = 0; + + if (_complexHistory.ContainsKey(lastItem)) + _complexHistory.Remove(lastItem); + + _complexHistory.Add(lastItem, new INavigationItem[NavigationStack.Count - 1 - startIndex]); + + int i = 0; + for (int j = startIndex; j < NavigationStack.Count - 1; j++) + { + _complexHistory[lastItem][i] = NavigationStack[j]; + i++; + } + } +} diff --git a/src/Wpf.Ui/Styles/Controls/Breadcrumb.xaml b/src/Wpf.Ui/Styles/Controls/Breadcrumb.xaml index df9936ef6..57d4846b3 100644 --- a/src/Wpf.Ui/Styles/Controls/Breadcrumb.xaml +++ b/src/Wpf.Ui/Styles/Controls/Breadcrumb.xaml @@ -10,8 +10,6 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:controls="clr-namespace:Wpf.Ui.Controls"> - + + + + \ No newline at end of file diff --git a/src/Wpf.Ui/Styles/Wpf.Ui.xaml b/src/Wpf.Ui/Styles/Wpf.Ui.xaml index f1258ed10..81c6fd434 100644 --- a/src/Wpf.Ui/Styles/Wpf.Ui.xaml +++ b/src/Wpf.Ui/Styles/Wpf.Ui.xaml @@ -33,6 +33,7 @@ + diff --git a/src/Wpf.Ui/Wpf.Ui.csproj b/src/Wpf.Ui/Wpf.Ui.csproj index c723d1d40..f86152436 100644 --- a/src/Wpf.Ui/Wpf.Ui.csproj +++ b/src/Wpf.Ui/Wpf.Ui.csproj @@ -71,6 +71,10 @@ + + + +