From 0ccd2f8e939f6eca7446cbbb489fc174dc30d64b Mon Sep 17 00:00:00 2001 From: Omid Mafakher Date: Sat, 7 Sep 2024 19:09:06 +0200 Subject: [PATCH 1/7] Add tab page and custom nav bar (#40) * #27 supporting platform base theme and dynamic navbar * #27 adding hosted tab page and support dynamic navbar * #27 supporting dynamic navigation bar * #27 fix navbar conflict and selected tab * remove default nav to shell * remove useless files * add develop branch for build --- .github/workflows/dotnet.yml | 6 +- .../AvaloniaInside.Shell.csproj | 1 + src/AvaloniaInside.Shell/BindingNavigate.cs | 49 ++-- .../BindingNavigateConverter.cs | 3 +- src/AvaloniaInside.Shell/Default.axaml | 189 +------------ .../DefaultNavigationUpdateStrategy.cs | 14 +- src/AvaloniaInside.Shell/HostedItemsHelper.cs | 64 +++++ src/AvaloniaInside.Shell/IHostItems.cs | 10 + .../INavigationBarProvider.cs | 7 + .../INavigationUpdateStrategy.cs | 3 +- src/AvaloniaInside.Shell/INavigator.cs | 2 + .../INavigatorLifecycle.cs | 6 +- .../ISelectableHostItems.cs | 10 + src/AvaloniaInside.Shell/NavigationBar.cs | 171 +++++++++--- .../NavigationBarAttachType.cs | 8 + src/AvaloniaInside.Shell/NavigationChain.cs | 12 +- src/AvaloniaInside.Shell/NavigationStack.cs | 195 ++++++++------ .../NavigationStackChanges.cs | 1 + src/AvaloniaInside.Shell/Navigator.Attach.cs | 31 ++- src/AvaloniaInside.Shell/Navigator.cs | 37 +-- src/AvaloniaInside.Shell/Page.cs | 99 ++++++- .../Platform/PlatformSetup.cs | 10 + .../Windows/DrillInNavigationTransition.cs | 1 - .../Presenters/GenericPresenter.cs | 6 +- .../Presenters/PresenterBase.cs | 13 +- .../ShellView.ItemNavigator.cs | 3 +- .../ShellView.SideMenu.cs | 11 +- src/AvaloniaInside.Shell/ShellView.cs | 121 +++++++-- src/AvaloniaInside.Shell/StackContentView.cs | 13 + src/AvaloniaInside.Shell/TabPage.cs | 252 ++++++++++++++++++ .../Theme/Default/Colors.axaml | 14 + .../Theme/Default/Controls.axaml | 15 ++ .../Theme/Default/NavigationBar.axaml | 62 +++++ .../Theme/Default/Page.axaml | 19 ++ .../Theme/Default/ShellView.axaml | 72 +++++ .../Theme/Default/SideMenu.axaml | 46 ++++ .../Theme/Default/TabPage.axaml | 59 ++++ .../Theme/StackContentView.axaml | 21 -- .../ShellExample.Android.csproj | 19 +- .../ShellExample.Android/SplashActivity.cs | 5 - .../ShellExample.iOS/AppDelegate.cs | 3 - .../ShellExample/ShellExample/Helper.cs | 6 - .../ShellExample/Helpers/ImageHelper.cs | 2 - .../ShellExample/ShellExample/Styles.axaml | 86 +----- .../ShellExample/ViewModels/MainViewModel.cs | 2 - .../ViewModels/SettingViewModel.cs | 4 - .../ShopViewModels/ProductDetailViewModel.cs | 7 +- .../ViewModels/WelcomeViewModel.cs | 4 - .../ShellExample/Views/CatView.axaml.cs | 1 - .../ShellExample/Views/DogView.axaml.cs | 1 - .../ShellExample/Views/FontIconImageSource.cs | 5 - .../ShellExample/Views/HomePage.axaml | 4 + .../ShellExample/Views/HomePage.axaml.cs | 3 - .../Views/MainTabControl.axaml.cs | 1 - .../ShellExample/Views/MainView.axaml | 2 +- .../Views/PetsTabControlView.axaml | 50 ++-- .../Views/PetsTabControlView.axaml.cs | 8 +- .../ShellExample/Views/ProfileView.axaml | 5 + .../ShellExample/Views/ProfileView.axaml.cs | 3 - .../ShellExample/Views/SecondView.axaml.cs | 1 - .../ShellExample/Views/SettingView.axaml | 7 +- .../ShellExample/Views/SettingView.axaml.cs | 3 - .../ShopViews/ConfirmationCloseView.axaml.cs | 3 - .../ProductCatalogFilterView.axaml.cs | 1 - .../Views/ShopViews/ProductCatalogView.axaml | 4 + .../ShopViews/ProductCatalogView.axaml.cs | 1 - .../Views/ShopViews/ProductDetailView.axaml | 2 +- .../ShopViews/ProductDetailView.axaml.cs | 1 - .../ShellExample/Views/SimpleDialog.axaml.cs | 1 - .../ShellExample/Views/WelcomeView.axaml.cs | 3 - 70 files changed, 1246 insertions(+), 658 deletions(-) create mode 100644 src/AvaloniaInside.Shell/HostedItemsHelper.cs create mode 100644 src/AvaloniaInside.Shell/IHostItems.cs create mode 100644 src/AvaloniaInside.Shell/INavigationBarProvider.cs create mode 100644 src/AvaloniaInside.Shell/ISelectableHostItems.cs create mode 100644 src/AvaloniaInside.Shell/NavigationBarAttachType.cs create mode 100644 src/AvaloniaInside.Shell/TabPage.cs create mode 100644 src/AvaloniaInside.Shell/Theme/Default/Colors.axaml create mode 100644 src/AvaloniaInside.Shell/Theme/Default/Controls.axaml create mode 100644 src/AvaloniaInside.Shell/Theme/Default/NavigationBar.axaml create mode 100644 src/AvaloniaInside.Shell/Theme/Default/Page.axaml create mode 100644 src/AvaloniaInside.Shell/Theme/Default/ShellView.axaml create mode 100644 src/AvaloniaInside.Shell/Theme/Default/SideMenu.axaml create mode 100644 src/AvaloniaInside.Shell/Theme/Default/TabPage.axaml delete mode 100644 src/AvaloniaInside.Shell/Theme/StackContentView.axaml diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index ce764ab..1b33977 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -1,13 +1,13 @@ # This workflow will build a .NET project # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net -name: .NET +name: CI on: push: - branches: [ "main" ] + branches: [ "main", "develop" ] pull_request: - branches: [ "main" ] + branches: [ "main", "develop" ] jobs: build: diff --git a/src/AvaloniaInside.Shell/AvaloniaInside.Shell.csproj b/src/AvaloniaInside.Shell/AvaloniaInside.Shell.csproj index a5135a9..943a0a0 100644 --- a/src/AvaloniaInside.Shell/AvaloniaInside.Shell.csproj +++ b/src/AvaloniaInside.Shell/AvaloniaInside.Shell.csproj @@ -19,6 +19,7 @@ + diff --git a/src/AvaloniaInside.Shell/BindingNavigate.cs b/src/AvaloniaInside.Shell/BindingNavigate.cs index 996eb74..ea14e32 100644 --- a/src/AvaloniaInside.Shell/BindingNavigate.cs +++ b/src/AvaloniaInside.Shell/BindingNavigate.cs @@ -7,30 +7,30 @@ using System.Threading.Tasks; using System.Windows.Input; -namespace AvaloniaInside.Shell +namespace AvaloniaInside.Shell; + +[TypeConverter(typeof(BindingNavigateConverter))] +public class BindingNavigate : AvaloniaObject, ICommand { - [TypeConverter(typeof(BindingNavigateConverter))] - public class BindingNavigate : AvaloniaObject, ICommand - { - private bool _singletonCanExecute = true; - private EventHandler? _singletonCanExecuteChanged; + private bool _singletonCanExecute = true; + private EventHandler? _singletonCanExecuteChanged; - public AvaloniaObject? Sender { get; internal set; } - public string Path { get; set; } - public NavigateType? Type { get; set; } - public IPageTransition? Transition { get; set; } + public AvaloniaObject? Sender { get; internal set; } + public string Path { get; set; } + public NavigateType? Type { get; set; } + public IPageTransition? Transition { get; set; } - public event EventHandler? CanExecuteChanged - { - add => _singletonCanExecuteChanged += value; - remove => _singletonCanExecuteChanged -= value; - } + public event EventHandler? CanExecuteChanged + { + add => _singletonCanExecuteChanged += value; + remove => _singletonCanExecuteChanged -= value; + } - public bool CanExecute(object? parameter) => _singletonCanExecute; - public void Execute(object? parameter) => ExecuteAsync(parameter, CancellationToken.None); + public bool CanExecute(object? parameter) => _singletonCanExecute; + public void Execute(object? parameter) => ExecuteAsync(parameter, CancellationToken.None); - public async Task ExecuteAsync(object? parameter, CancellationToken cancellationToken) - { + public async Task ExecuteAsync(object? parameter, CancellationToken cancellationToken) + { if (Sender is not Visual visual) return; if (visual.FindAncestorOfType() is not { } shell) return; @@ -63,9 +63,8 @@ await shell.Navigator.NavigateAsync( } } - public static implicit operator BindingNavigate(string path) => new BindingNavigate - { - Path = path - }; - } -} + public static implicit operator BindingNavigate(string path) => new BindingNavigate + { + Path = path + }; +} \ No newline at end of file diff --git a/src/AvaloniaInside.Shell/BindingNavigateConverter.cs b/src/AvaloniaInside.Shell/BindingNavigateConverter.cs index 9a7f194..c319265 100644 --- a/src/AvaloniaInside.Shell/BindingNavigateConverter.cs +++ b/src/AvaloniaInside.Shell/BindingNavigateConverter.cs @@ -1,5 +1,4 @@ -using Avalonia.Media; -using System; +using System; using System.ComponentModel; using System.Globalization; diff --git a/src/AvaloniaInside.Shell/Default.axaml b/src/AvaloniaInside.Shell/Default.axaml index 87f9ab4..7a24fda 100644 --- a/src/AvaloniaInside.Shell/Default.axaml +++ b/src/AvaloniaInside.Shell/Default.axaml @@ -3,192 +3,11 @@ - - - - White - - - Black - Black - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + diff --git a/src/AvaloniaInside.Shell/DefaultNavigationUpdateStrategy.cs b/src/AvaloniaInside.Shell/DefaultNavigationUpdateStrategy.cs index aef77e2..e86bdbb 100644 --- a/src/AvaloniaInside.Shell/DefaultNavigationUpdateStrategy.cs +++ b/src/AvaloniaInside.Shell/DefaultNavigationUpdateStrategy.cs @@ -1,9 +1,9 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Avalonia.Controls; -using Avalonia.Controls.Primitives; namespace AvaloniaInside.Shell; @@ -21,7 +21,6 @@ public DefaultNavigationUpdateStrategy(IPresenterProvider presenterProvider) public async Task UpdateChangesAsync( ShellView shellView, NavigationStackChanges changes, - List newInstances, NavigateType navigateType, object? argument, bool hasArgument, @@ -29,7 +28,7 @@ public async Task UpdateChangesAsync( { var isSame = changes.Previous == changes.Front; - foreach (var instance in newInstances) + foreach (var instance in changes.NewNavigationChains.Select(s => s.Instance)) { if (instance is INavigationLifecycle navigationLifecycle) await navigationLifecycle.InitialiseAsync(cancellationToken); @@ -80,14 +79,15 @@ private async Task InvokeRemoveAsync(ShellView shellView, private void SubscribeForUpdateIfNeeded(object? instance) { - if (instance is not SelectingItemsControl selectingItemsControl) return; - selectingItemsControl.SelectionChanged += SelectingItemsControlOnSelectionChanged; + if (HostedItemsHelper.GetSelectableHostedItems(instance) is {} hosted) + hosted.SelectionChanged += SelectingItemsControlOnSelectionChanged; + } private void UnSubscribeForUpdateIfNeeded(object instance) { - if (instance is not SelectingItemsControl selectingItemsControl) return; - selectingItemsControl.SelectionChanged -= SelectingItemsControlOnSelectionChanged; + if (HostedItemsHelper.GetSelectableHostedItems(instance) is {} hosted) + hosted.SelectionChanged -= SelectingItemsControlOnSelectionChanged; } private void SelectingItemsControlOnSelectionChanged(object? sender, SelectionChangedEventArgs e) diff --git a/src/AvaloniaInside.Shell/HostedItemsHelper.cs b/src/AvaloniaInside.Shell/HostedItemsHelper.cs new file mode 100644 index 0000000..683f487 --- /dev/null +++ b/src/AvaloniaInside.Shell/HostedItemsHelper.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; + +namespace AvaloniaInside.Shell; + +public static class HostedItemsHelper +{ + private class ItemsControlProxy(ItemsControl itemsControl) : IHostItems + { + public IEnumerable? ItemsSource + { + get => itemsControl.ItemsSource; + set => itemsControl.ItemsSource = value; + } + + public ItemCollection Items => itemsControl.Items; + } + + private class SelectingItemsControlProxy(SelectingItemsControl itemsControl) + : ItemsControlProxy(itemsControl), ISelectableHostItems + { + public event EventHandler? SelectionChanged + { + add => itemsControl.SelectionChanged += value; + remove => itemsControl.SelectionChanged -= value; + } + + public object? SelectedItem + { + get => itemsControl.SelectedItem; + set => itemsControl.SelectedItem = value; + } + } + + public static bool CanBeHosted(Type viewType) => + viewType.IsSubclassOf(typeof(ItemsControl)) || typeof(IHostItems).IsAssignableFrom(viewType); + + public static bool CanBeHosted(object view) => + view is ItemsControl or SelectingItemsControl or IHostItems or ISelectableHostItems; + + public static IHostItems? GetHostedItems(object? control) + { + if (GetSelectableHostedItems(control) is { } casted) return casted; + + if (control is IHostItems hostedItems) + return hostedItems; + if (control is ItemsControl itemsControl) + return new ItemsControlProxy(itemsControl); + + return null; + } + + public static ISelectableHostItems? GetSelectableHostedItems(object? control) + { + if (control is ISelectableHostItems selectableHostedItem) + return selectableHostedItem; + if (control is SelectingItemsControl selectingItemsControl) + return new SelectingItemsControlProxy(selectingItemsControl); + + return null; + } +} diff --git a/src/AvaloniaInside.Shell/IHostItems.cs b/src/AvaloniaInside.Shell/IHostItems.cs new file mode 100644 index 0000000..ab239a3 --- /dev/null +++ b/src/AvaloniaInside.Shell/IHostItems.cs @@ -0,0 +1,10 @@ +using System.Collections; +using Avalonia.Controls; + +namespace AvaloniaInside.Shell; + +public interface IHostItems +{ + IEnumerable? ItemsSource { get; set; } + ItemCollection Items { get; } +} diff --git a/src/AvaloniaInside.Shell/INavigationBarProvider.cs b/src/AvaloniaInside.Shell/INavigationBarProvider.cs new file mode 100644 index 0000000..94d79e4 --- /dev/null +++ b/src/AvaloniaInside.Shell/INavigationBarProvider.cs @@ -0,0 +1,7 @@ +namespace AvaloniaInside.Shell; + +public interface INavigationBarProvider +{ + NavigationBar? NavigationBar { get; } + NavigationBar? AttachedNavigationBar { get; } +} diff --git a/src/AvaloniaInside.Shell/INavigationUpdateStrategy.cs b/src/AvaloniaInside.Shell/INavigationUpdateStrategy.cs index a9f0703..95e5fe4 100644 --- a/src/AvaloniaInside.Shell/INavigationUpdateStrategy.cs +++ b/src/AvaloniaInside.Shell/INavigationUpdateStrategy.cs @@ -11,9 +11,8 @@ public interface INavigationUpdateStrategy Task UpdateChangesAsync( ShellView shellView, NavigationStackChanges changes, - List newInstances, NavigateType navigateType, object? argument, bool hasArgument, CancellationToken cancellationToken); -} \ No newline at end of file +} diff --git a/src/AvaloniaInside.Shell/INavigator.cs b/src/AvaloniaInside.Shell/INavigator.cs index 5db2e4c..18d03a5 100644 --- a/src/AvaloniaInside.Shell/INavigator.cs +++ b/src/AvaloniaInside.Shell/INavigator.cs @@ -11,6 +11,8 @@ public interface INavigator INavigationRegistrar Registrar { get; } + NavigationChain? CurrentChain { get; } + void RegisterShell(ShellView shellView); bool HasItemInStack(); diff --git a/src/AvaloniaInside.Shell/INavigatorLifecycle.cs b/src/AvaloniaInside.Shell/INavigatorLifecycle.cs index 908d5d6..eeb9e51 100644 --- a/src/AvaloniaInside.Shell/INavigatorLifecycle.cs +++ b/src/AvaloniaInside.Shell/INavigatorLifecycle.cs @@ -1,9 +1,5 @@ -using Avalonia; -using Avalonia.Animation; +using Avalonia.Animation; using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Threading; using System.Threading.Tasks; diff --git a/src/AvaloniaInside.Shell/ISelectableHostItems.cs b/src/AvaloniaInside.Shell/ISelectableHostItems.cs new file mode 100644 index 0000000..0ff8475 --- /dev/null +++ b/src/AvaloniaInside.Shell/ISelectableHostItems.cs @@ -0,0 +1,10 @@ +using System; +using Avalonia.Controls; + +namespace AvaloniaInside.Shell; + +public interface ISelectableHostItems : IHostItems +{ + event EventHandler SelectionChanged; + object? SelectedItem { get; set; } +} diff --git a/src/AvaloniaInside.Shell/NavigationBar.cs b/src/AvaloniaInside.Shell/NavigationBar.cs index 3c6f9b7..29e00e2 100644 --- a/src/AvaloniaInside.Shell/NavigationBar.cs +++ b/src/AvaloniaInside.Shell/NavigationBar.cs @@ -1,45 +1,48 @@ using System; -using System.Threading; -using System.Threading.Tasks; using System.Windows.Input; using Avalonia; using Avalonia.Controls; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; +using Avalonia.Interactivity; namespace AvaloniaInside.Shell; +[TemplatePart("PART_Header", typeof(ContentControl))] +[TemplatePart("PART_ActionButton", typeof(Button))] +[TemplatePart("PART_Items", typeof(ContentControl))] public class NavigationBar : TemplatedControl { - public static readonly DirectProperty SideMenuCommandProperty = - AvaloniaProperty.RegisterDirect( - nameof(SideMenuCommand), - o => o.SideMenuCommand, - (o, v) => o.SideMenuCommand = v); - - public static readonly DirectProperty BackCommandProperty = - AvaloniaProperty.RegisterDirect( - nameof(BackCommand), - o => o.BackCommand, - (o, v) => o.BackCommand = v); - - public static readonly DirectProperty HasSideMenuOptionProperty = - AvaloniaProperty.RegisterDirect( - nameof(HasSideMenuOption), - o => o.HasSideMenuOption, - (o, v) => o.HasSideMenuOption = v); - private ContentControl? _header; private Button? _actionButton; private ContentControl? _items; private object? _pendingHeader; - private NavigateType? _pendingNavType; - public ShellView? ShellView { get; internal set; } + #region ShellView + + public static readonly StyledProperty ShellViewProperty = + AvaloniaProperty.Register(nameof(ShellView)); + + public ShellView? ShellView + { + get => GetValue(ShellViewProperty); + internal set => SetValue(ShellViewProperty, value); + } + + #endregion + + #region BackCommand - private ICommand _backCommand; + public static readonly DirectProperty BackCommandProperty = + AvaloniaProperty.RegisterDirect( + nameof(BackCommand), + o => o.BackCommand, + (o, v) => o.BackCommand = v); - public ICommand BackCommand + private ICommand? _backCommand; + + public ICommand? BackCommand { get => _backCommand; set @@ -49,9 +52,19 @@ public ICommand BackCommand } } - private ICommand _sideMenuCommand; + #endregion + + #region SideMenuCommand - public ICommand SideMenuCommand + public static readonly DirectProperty SideMenuCommandProperty = + AvaloniaProperty.RegisterDirect( + nameof(SideMenuCommand), + o => o.SideMenuCommand, + (o, v) => o.SideMenuCommand = v); + + private ICommand? _sideMenuCommand; + + public ICommand? SideMenuCommand { get => _sideMenuCommand; set @@ -61,6 +74,16 @@ public ICommand SideMenuCommand } } + #endregion + + #region HasSideMenuOption + + public static readonly DirectProperty HasSideMenuOptionProperty = + AvaloniaProperty.RegisterDirect( + nameof(HasSideMenuOption), + o => o.HasSideMenuOption, + (o, v) => o.HasSideMenuOption = v); + private bool _hasSideMenuOption = true; public bool HasSideMenuOption @@ -73,6 +96,29 @@ public bool HasSideMenuOption } } + #endregion + + #region CurrentView + + public static readonly DirectProperty CurrentViewProperty = + AvaloniaProperty.RegisterDirect( + nameof(CurrentView), + o => o.CurrentView, + (o, v) => o.CurrentView = v); + + private object? _currentView; + public object? CurrentView + { + get => _currentView; + set + { + if (SetAndRaise(CurrentViewProperty, ref _currentView, value)) + UpdateView(value); + } + } + + #endregion + #region TopSafeSpace public static readonly StyledProperty TopSafeSpaceProperty = @@ -140,6 +186,19 @@ public static void SetHeader(AvaloniaObject element, object parameter) => #endregion + #region HeaderIcon + + public static readonly AttachedProperty HeaderIconProperty = + AvaloniaProperty.RegisterAttached("HeaderIcon"); + + public static object GetHeaderIcon(AvaloniaObject element) => + element.GetValue(HeaderIconProperty); + + public static void SetHeaderIcon(AvaloniaObject element, object parameter) => + element.SetValue(HeaderIconProperty, parameter); + + #endregion + #region Visible public static readonly AttachedProperty VisibleProperty = @@ -155,10 +214,45 @@ public static void SetVisible(AvaloniaObject element, bool parameter) => #endregion + #region Setup and loading template + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + if (change.Property == ShellViewProperty) + { + ShellViewUpdated(); + } + } + + protected override void OnLoaded(RoutedEventArgs e) + { + base.OnLoaded(e); + + if (_pendingHeader != null) + UpdateView(_pendingHeader); + } + + protected virtual void ShellViewUpdated() + { + if (ShellView is not { } shellView) return; + + _backCommand = shellView.BackCommand; + _sideMenuCommand = shellView.SideMenuCommand; + + this[!TopSafePaddingProperty] = shellView[!ShellView.TopSafePaddingProperty]; + this[!TopSafeSpaceProperty] = shellView[!ShellView.TopSafeSpaceProperty]; + this[!ApplyTopSafePaddingProperty] = shellView[!ShellView.ApplyTopSafePaddingProperty]; + + if (shellView.ContentView?.CurrentView is { } currentView) + UpdateView(currentView); + } + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { base.OnApplyTemplate(e); - _header = e.NameScope.Find("PART_Header") ?? throw new ArgumentNullException("PART_Header"); + _header = e.NameScope.Find("PART_Header") ?? + throw new Exception("PART_Header cannot found for NavigationBar"); _actionButton = e.NameScope.Find