From df520645bfdc02bb9cfe5f39a998a8f85019adf4 Mon Sep 17 00:00:00 2001 From: Omid Mafakher Date: Sun, 25 Aug 2024 17:11:59 +0200 Subject: [PATCH 01/10] #27 supporting platform base theme and dynamic navbar --- .../AvaloniaInside.Shell.csproj | 1 + src/AvaloniaInside.Shell/Default.axaml | 189 +----------------- .../DefaultNavigationUpdateStrategy.cs | 9 +- src/AvaloniaInside.Shell/HostedItemsHelper.cs | 64 ++++++ src/AvaloniaInside.Shell/IHostedItems.cs | 10 + src/AvaloniaInside.Shell/INavigator.cs | 2 + .../ISelectableHostedItems.cs | 10 + src/AvaloniaInside.Shell/NavigationBar.cs | 83 ++++++-- src/AvaloniaInside.Shell/NavigationChain.cs | 12 +- src/AvaloniaInside.Shell/Navigator.cs | 10 +- src/AvaloniaInside.Shell/Page.cs | 23 ++- .../Presenters/GenericPresenter.cs | 6 +- .../Presenters/PresenterBase.cs | 11 +- .../ShellView.ItemNavigator.cs | 2 +- .../ShellView.SideMenu.cs | 10 +- src/AvaloniaInside.Shell/ShellView.cs | 59 +++--- src/AvaloniaInside.Shell/StackContentView.cs | 13 ++ src/AvaloniaInside.Shell/TabPage.cs | 14 ++ .../Theme/Default/Colors.axaml | 14 ++ .../Theme/Default/Controls.axaml | 14 ++ .../Theme/Default/HostedTabPage.axaml | 5 + .../Theme/Default/NavigationBar.axaml | 60 ++++++ .../Theme/Default/Page.axaml | 7 + .../Theme/Default/ShellView.axaml | 72 +++++++ .../Theme/Default/SideMenu.axaml | 46 +++++ .../Theme/Ios/Colors.axaml | 14 ++ .../Theme/Ios/Controls.axaml | 14 ++ .../Theme/Ios/NavigationBar.axaml | 60 ++++++ src/AvaloniaInside.Shell/Theme/Ios/Page.axaml | 22 ++ .../Theme/Ios/ShellView.axaml | 71 +++++++ .../Theme/Ios/SideMenu.axaml | 46 +++++ .../Theme/StackContentView.axaml | 21 -- .../ShellExample.Android.csproj | 19 +- 33 files changed, 727 insertions(+), 286 deletions(-) create mode 100644 src/AvaloniaInside.Shell/HostedItemsHelper.cs create mode 100644 src/AvaloniaInside.Shell/IHostedItems.cs create mode 100644 src/AvaloniaInside.Shell/ISelectableHostedItems.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/HostedTabPage.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/Ios/Colors.axaml create mode 100644 src/AvaloniaInside.Shell/Theme/Ios/Controls.axaml create mode 100644 src/AvaloniaInside.Shell/Theme/Ios/NavigationBar.axaml create mode 100644 src/AvaloniaInside.Shell/Theme/Ios/Page.axaml create mode 100644 src/AvaloniaInside.Shell/Theme/Ios/ShellView.axaml create mode 100644 src/AvaloniaInside.Shell/Theme/Ios/SideMenu.axaml delete mode 100644 src/AvaloniaInside.Shell/Theme/StackContentView.axaml 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/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..8fca8b5 100644 --- a/src/AvaloniaInside.Shell/DefaultNavigationUpdateStrategy.cs +++ b/src/AvaloniaInside.Shell/DefaultNavigationUpdateStrategy.cs @@ -80,14 +80,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..501a284 --- /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) : IHostedItems + { + public IEnumerable? ItemsSource + { + get => itemsControl.ItemsSource; + set => itemsControl.ItemsSource = value; + } + + public ItemCollection Items => itemsControl.Items; + } + + private class SelectingItemsControlProxy(SelectingItemsControl itemsControl) + : ItemsControlProxy(itemsControl), ISelectableHostedItems + { + 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)) || viewType.IsSubclassOf(typeof(IHostedItems)); + + public static bool CanBeHosted(object view) => + view is ItemsControl or SelectingItemsControl or IHostedItems or ISelectableHostedItems; + + public static IHostedItems? GetHostedItems(object? control) + { + if (GetSelectableHostedItems(control) is { } casted) return casted; + + if (control is IHostedItems hostedItems) + return hostedItems; + if (control is ItemsControl itemsControl) + return new ItemsControlProxy(itemsControl); + + return null; + } + + public static ISelectableHostedItems? GetSelectableHostedItems(object? control) + { + if (control is ISelectableHostedItems selectableHostedItem) + return selectableHostedItem; + if (control is SelectingItemsControl selectingItemsControl) + return new SelectingItemsControlProxy(selectingItemsControl); + + return null; + } +} diff --git a/src/AvaloniaInside.Shell/IHostedItems.cs b/src/AvaloniaInside.Shell/IHostedItems.cs new file mode 100644 index 0000000..8d857e5 --- /dev/null +++ b/src/AvaloniaInside.Shell/IHostedItems.cs @@ -0,0 +1,10 @@ +using System.Collections; +using Avalonia.Controls; + +namespace AvaloniaInside.Shell; + +public interface IHostedItems +{ + IEnumerable? ItemsSource { get; set; } + ItemCollection Items { get; } +} \ 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/ISelectableHostedItems.cs b/src/AvaloniaInside.Shell/ISelectableHostedItems.cs new file mode 100644 index 0000000..4445d04 --- /dev/null +++ b/src/AvaloniaInside.Shell/ISelectableHostedItems.cs @@ -0,0 +1,10 @@ +using System; +using Avalonia.Controls; + +namespace AvaloniaInside.Shell; + +public interface ISelectableHostedItems : IHostedItems +{ + event EventHandler SelectionChanged; + object? SelectedItem { get; set; } +} diff --git a/src/AvaloniaInside.Shell/NavigationBar.cs b/src/AvaloniaInside.Shell/NavigationBar.cs index 3c6f9b7..04b7dfe 100644 --- a/src/AvaloniaInside.Shell/NavigationBar.cs +++ b/src/AvaloniaInside.Shell/NavigationBar.cs @@ -10,14 +10,14 @@ namespace AvaloniaInside.Shell; public class NavigationBar : TemplatedControl { - public static readonly DirectProperty SideMenuCommandProperty = - AvaloniaProperty.RegisterDirect( + public static readonly DirectProperty SideMenuCommandProperty = + AvaloniaProperty.RegisterDirect( nameof(SideMenuCommand), o => o.SideMenuCommand, (o, v) => o.SideMenuCommand = v); - public static readonly DirectProperty BackCommandProperty = - AvaloniaProperty.RegisterDirect( + public static readonly DirectProperty BackCommandProperty = + AvaloniaProperty.RegisterDirect( nameof(BackCommand), o => o.BackCommand, (o, v) => o.BackCommand = v); @@ -33,13 +33,19 @@ public class NavigationBar : TemplatedControl private ContentControl? _items; private object? _pendingHeader; - private NavigateType? _pendingNavType; - public ShellView? ShellView { get; internal set; } + public static readonly StyledProperty ShellViewProperty = + AvaloniaProperty.Register(nameof(ShellView)); - private ICommand _backCommand; + public ShellView? ShellView + { + get => GetValue(ShellViewProperty); + set => SetValue(ShellViewProperty, value); + } - public ICommand BackCommand + private ICommand? _backCommand; + + public ICommand? BackCommand { get => _backCommand; set @@ -49,9 +55,9 @@ public ICommand BackCommand } } - private ICommand _sideMenuCommand; + private ICommand? _sideMenuCommand; - public ICommand SideMenuCommand + public ICommand? SideMenuCommand { get => _sideMenuCommand; set @@ -73,6 +79,27 @@ public bool HasSideMenuOption } } + #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 = @@ -155,6 +182,32 @@ public static void SetVisible(AvaloniaObject element, bool parameter) => #endregion + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + if (change.Property == ShellViewProperty) + { + ShellViewUpdated(); + } + } + + 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); + + this[!CurrentViewProperty] = shellView.ContentView?[!StackContentView.CurrentViewProperty]; + } + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { base.OnApplyTemplate(e); @@ -166,20 +219,17 @@ protected override void OnApplyTemplate(TemplateAppliedEventArgs e) _actionButton.Command = _backCommand; if (_pendingHeader != null) - _ = UpdateAsync(_pendingHeader, _pendingNavType ?? NavigateType.Normal, CancellationToken.None); + UpdateView(_pendingHeader); } - public Task UpdateAsync(object view, NavigateType navigateType, CancellationToken cancellationToken) + private void UpdateView(object? view) { if (_header == null && _items == null) { _pendingHeader = view; - _pendingNavType = navigateType; - return Task.CompletedTask; + return; } - //TODO: Animation can apply for specific navigate type - if (_header != null) UpdateHeader(view, _header); @@ -192,7 +242,6 @@ public Task UpdateAsync(object view, NavigateType navigateType, CancellationToke IsVisible = true; UpdateButtons(); - return Task.CompletedTask; } protected virtual void UpdateButtons() diff --git a/src/AvaloniaInside.Shell/NavigationChain.cs b/src/AvaloniaInside.Shell/NavigationChain.cs index 3970b32..2076932 100644 --- a/src/AvaloniaInside.Shell/NavigationChain.cs +++ b/src/AvaloniaInside.Shell/NavigationChain.cs @@ -5,12 +5,12 @@ namespace AvaloniaInside.Shell; public class NavigationChain { - public NavigationNode Node { get; set; } = default!; - public object Instance { get; set; } = default!; - public NavigateType Type { get; set; } - public Uri Uri { get; set; } = default!; - public NavigationChain? Back { get; set; } - public bool Hosted { get; set; } = false; + public NavigationNode Node { get; internal set; } = default!; + public object Instance { get; internal set; } = default!; + public NavigateType Type { get; internal set; } + public Uri Uri { get; internal set; } = default!; + public NavigationChain? Back { get; internal set; } + public bool Hosted { get; internal set; } public IEnumerable GetAscendingNodes() { diff --git a/src/AvaloniaInside.Shell/Navigator.cs b/src/AvaloniaInside.Shell/Navigator.cs index 92e79ca..b43106e 100644 --- a/src/AvaloniaInside.Shell/Navigator.cs +++ b/src/AvaloniaInside.Shell/Navigator.cs @@ -22,6 +22,8 @@ public partial class Navigator : INavigator public Uri CurrentUri => _stack.Current?.Uri ?? Registrar.RootUri; + public NavigationChain? CurrentChain => _stack.Current; + public INavigationRegistrar Registrar { get; } public Navigator( @@ -99,7 +101,7 @@ private async Task NotifyAsync( await fromPage.OnNavigatingAsync(args, cancellationToken); if (args.Cancel) return; - //Check for overrides + //Check for overrides if (argument != args.Argument) { @@ -292,11 +294,11 @@ public Task NavigateAndWaitAsync( NavigateAndWaitAsync(path, navigateType, null, false, sender, withAnimation, overrideTransition, cancellationToken); public Task NavigateAndWaitAsync( string path, - object? argument, - object? sender, + object? argument, + object? sender, NavigateType navigateType, bool withAnimation, - IPageTransition? overrideTransition = null, + IPageTransition? overrideTransition = null, CancellationToken cancellationToken = default) => NavigateAndWaitAsync(path, navigateType, argument, true, sender, withAnimation, overrideTransition, cancellationToken); diff --git a/src/AvaloniaInside.Shell/Page.cs b/src/AvaloniaInside.Shell/Page.cs index d0408e8..73b0872 100644 --- a/src/AvaloniaInside.Shell/Page.cs +++ b/src/AvaloniaInside.Shell/Page.cs @@ -2,11 +2,26 @@ using Avalonia.Controls; using System.Threading; using System.Threading.Tasks; +using Avalonia; +using Avalonia.Controls.Primitives; namespace AvaloniaInside.Shell; + public class Page : UserControl, INavigationLifecycle, INavigatorLifecycle { - public ShellView? Shell { get; internal set; } + public static readonly StyledProperty ShellProperty = + AvaloniaProperty.Register(nameof(Shell)); + + private NavigationBar? _navigationBar; + + public NavigationBar? NavigationBar => _navigationBar ?? Shell?.AttachedNavigationBar; + + public ShellView? Shell + { + get => GetValue(ShellProperty); + set => SetValue(ShellProperty, value); + } + public INavigator? Navigator => Shell?.Navigator; protected override Type StyleKeyOverride => typeof(Page); @@ -18,4 +33,10 @@ public class Page : UserControl, INavigationLifecycle, INavigatorLifecycle public virtual Task TerminateAsync(CancellationToken cancellationToken) => Task.CompletedTask; public virtual Task OnNavigateAsync(NaviagateEventArgs args, CancellationToken cancellationToken) => Task.CompletedTask; public virtual Task OnNavigatingAsync(NaviagatingEventArgs args, CancellationToken cancellationToken) => Task.CompletedTask; + + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + _navigationBar = e.NameScope.Find("PART_NavigationBar"); + } } diff --git a/src/AvaloniaInside.Shell/Presenters/GenericPresenter.cs b/src/AvaloniaInside.Shell/Presenters/GenericPresenter.cs index ef85e5b..715167e 100644 --- a/src/AvaloniaInside.Shell/Presenters/GenericPresenter.cs +++ b/src/AvaloniaInside.Shell/Presenters/GenericPresenter.cs @@ -5,7 +5,9 @@ namespace AvaloniaInside.Shell.Presenters; public class GenericPresenter : PresenterBase { - public override async Task PresentAsync(ShellView shellView, NavigationChain chain, + public override async Task PresentAsync( + ShellView shellView, + NavigationChain chain, NavigateType navigateType, CancellationToken cancellationToken) { @@ -15,7 +17,5 @@ public override async Task PresentAsync(ShellView shellView, NavigationChain cha hostControl ?? chain.Instance, navigateType, cancellationToken) ?? Task.CompletedTask); - - await (shellView.NavigationBar?.UpdateAsync(chain.Instance, navigateType, cancellationToken) ?? Task.CompletedTask); } } diff --git a/src/AvaloniaInside.Shell/Presenters/PresenterBase.cs b/src/AvaloniaInside.Shell/Presenters/PresenterBase.cs index ea5b485..1e7f114 100644 --- a/src/AvaloniaInside.Shell/Presenters/PresenterBase.cs +++ b/src/AvaloniaInside.Shell/Presenters/PresenterBase.cs @@ -18,11 +18,12 @@ protected object GetHostControl(NavigationChain chain) var current = chain; while (current != null) { - if (current.Back is HostNavigationChain { Instance: ItemsControl itemsControl } parent) + if (current.Back is HostNavigationChain parent && + HostedItemsHelper.GetHostedItems(current.Back?.Instance) is { } hostedItems) { - if ((itemsControl.Items ?? itemsControl.ItemsSource) is not IList collection) + if ((hostedItems.Items ?? hostedItems.ItemsSource) is not IList collection) { - itemsControl.ItemsSource = collection = new AvaloniaList(); + hostedItems.ItemsSource = collection = new AvaloniaList(); } foreach (var hostedChildChain in parent.Nodes.Where(hostedChildChain => @@ -31,10 +32,8 @@ protected object GetHostControl(NavigationChain chain) collection.Add(hostedChildChain); } - if (itemsControl is SelectingItemsControl selectingItemsControl) - { + if (hostedItems is ISelectableHostedItems selectingItemsControl) selectingItemsControl.SelectedItem = current; - } } else { diff --git a/src/AvaloniaInside.Shell/ShellView.ItemNavigator.cs b/src/AvaloniaInside.Shell/ShellView.ItemNavigator.cs index 383041b..adc177e 100644 --- a/src/AvaloniaInside.Shell/ShellView.ItemNavigator.cs +++ b/src/AvaloniaInside.Shell/ShellView.ItemNavigator.cs @@ -40,7 +40,7 @@ private void AddRoute(Route route, string basePath) var path = $"{basePath}/{route.Path}"; var host = route as Host; - if (host != null && !host.Page.IsSubclassOf(typeof(ItemsControl))) + if (host != null && !HostedItemsHelper.CanBeHosted(host.Page)) throw new AggregateException("Host must inherits from ItemsControl"); Navigator.Registrar.RegisterRoute( diff --git a/src/AvaloniaInside.Shell/ShellView.SideMenu.cs b/src/AvaloniaInside.Shell/ShellView.SideMenu.cs index c9be5c8..3861a2e 100644 --- a/src/AvaloniaInside.Shell/ShellView.SideMenu.cs +++ b/src/AvaloniaInside.Shell/ShellView.SideMenu.cs @@ -260,30 +260,30 @@ protected virtual Task MenuActionAsync(CancellationToken cancellationToken) protected virtual void UpdateSideMenu() { - if (_splitView == null || _navigationBar == null) return; + if (_splitView == null || NavigationBar == null) return; switch (GetCurrentBehave()) { case SideMenuBehaveType.Default: _splitView.OpenPaneLength = SideMenuPresented ? SideMenuSize : 0; _splitView.IsPaneOpen = SideMenuPresented; - _navigationBar.HasSideMenuOption = true; + NavigationBar.HasSideMenuOption = true; break; case SideMenuBehaveType.Keep: _splitView.OpenPaneLength = SideMenuSize; _splitView.IsPaneOpen = true; - _navigationBar.HasSideMenuOption = false; + NavigationBar.HasSideMenuOption = false; break; case SideMenuBehaveType.Closed: _splitView.OpenPaneLength = 0; _splitView.IsPaneOpen = true; - _navigationBar.HasSideMenuOption = true; + NavigationBar.HasSideMenuOption = true; break; case SideMenuBehaveType.Removed: _splitView.OpenPaneLength = 0; _splitView.CompactPaneLength = 0; _splitView.IsPaneOpen = false; - _navigationBar.HasSideMenuOption = false; + NavigationBar.HasSideMenuOption = false; break; } } diff --git a/src/AvaloniaInside.Shell/ShellView.cs b/src/AvaloniaInside.Shell/ShellView.cs index 0af1253..4d4d552 100644 --- a/src/AvaloniaInside.Shell/ShellView.cs +++ b/src/AvaloniaInside.Shell/ShellView.cs @@ -1,6 +1,7 @@ using System; using System.Threading; using System.Threading.Tasks; +using System.Windows.Input; using Avalonia; using Avalonia.Animation; using Avalonia.Controls; @@ -9,7 +10,6 @@ using Avalonia.Interactivity; using Avalonia.Threading; using AvaloniaInside.Shell.Platform; -using AvaloniaInside.Shell.Platform.Windows; using ReactiveUI; using Splat; @@ -46,14 +46,24 @@ public enum SideMenuBehaveType private StackContentView? _modalView; private SideMenu? _sideMenu; - private bool _loadedFlag = false; - private bool _topLevelEventFlag = false; + private bool _loadedFlag; + private bool _topLevelEventFlag; #endregion #region Properties - public NavigationBar NavigationBar => _navigationBar; + public NavigationBar? NavigationBar => + _navigationBar ?? + (Navigator.CurrentChain?.Instance as Page)?.NavigationBar; + + public NavigationBar? AttachedNavigationBar => _navigationBar; + + public StackContentView? ContentView => _contentView; + + public ICommand BackCommand { get; set; } + + public ICommand SideMenuCommand { get; set; } #region ScreenSize @@ -265,7 +275,10 @@ public ShellView() .GetService() ?? throw new ArgumentException("Cannot find INavigationService"); Navigator.RegisterShell(this); - var isMobile = OperatingSystem.IsAndroid() || OperatingSystem.IsIOS(); + BackCommand = ReactiveCommand.CreateFromTask(BackActionAsync); + SideMenuCommand = ReactiveCommand.CreateFromTask(MenuActionAsync); + + _isMobile = OperatingSystem.IsAndroid() || OperatingSystem.IsIOS(); if (!_isMobile) { Classes.Add("Mobile"); @@ -305,22 +318,13 @@ protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { base.OnApplyTemplate(e); _splitView = e.NameScope.Find("PART_SplitView"); + _navigationBar = e.NameScope.Find("PART_NavigationBar"); _contentView = e.NameScope.Find("PART_ContentView"); _modalView = e.NameScope.Find("PART_Modal"); - _navigationBar = e.NameScope.Find("PART_NavigationBar"); _sideMenu = e.NameScope.Find("PART_SideMenu"); SetupUi(); - if (_splitView != null) - { - _splitView.PaneClosing += SplitViewOnPaneClosing; - } - - if (_sideMenu != null) - { - _sideMenu.Items = _sideMenuItems; - } } protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) @@ -341,15 +345,23 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang private void SetupUi() { - if (_navigationBar != null) + OnSafeEdgeSetup(); + UpdateSideMenu(); + + if (_navigationBar is { } navigationBar) { - _navigationBar.ShellView = this; - _navigationBar.BackCommand = ReactiveCommand.CreateFromTask(BackActionAsync); - _navigationBar.SideMenuCommand = ReactiveCommand.CreateFromTask(MenuActionAsync); + navigationBar.ShellView = this; } - OnSafeEdgeSetup(); - UpdateSideMenu(); + if (_splitView != null) + { + _splitView.PaneClosing += SplitViewOnPaneClosing; + } + + if (_sideMenu != null) + { + _sideMenu.Items = _sideMenuItems; + } } protected virtual void OnSafeEdgeSetup() @@ -378,7 +390,8 @@ protected virtual void OnSafeEdgeSetup() #region View Stack Manager - public async Task PushViewAsync(object view, + public async Task PushViewAsync( + object view, NavigateType navigateType, CancellationToken cancellationToken = default) { @@ -482,7 +495,7 @@ private void UpdateScreenSize(ScreenSizeType old, ScreenSizeType newScreen) private void UpdateBindings() { - var view = _contentView.CurrentView; + var view = _contentView?.CurrentView; if (view is StyledElement element) { this[!ApplyTopSafePaddingProperty] = element[!EnableSafeAreaForTopProperty]; diff --git a/src/AvaloniaInside.Shell/StackContentView.cs b/src/AvaloniaInside.Shell/StackContentView.cs index 192fb50..98ffb74 100644 --- a/src/AvaloniaInside.Shell/StackContentView.cs +++ b/src/AvaloniaInside.Shell/StackContentView.cs @@ -39,6 +39,11 @@ public bool HasContent private set => SetValue(HasContentProperty, value); } + public static readonly DirectProperty CurrentViewProperty = + AvaloniaProperty.RegisterDirect( + nameof(CurrentView), + o => o.Children.LastOrDefault()); + public object? CurrentView => Children.LastOrDefault(); public async Task PushViewAsync(object view, @@ -60,6 +65,8 @@ public async Task PushViewAsync(object view, await OnContentUpdateAsync(control, cancellationToken); await UpdateCurrentViewAsync(current, control, navigateType, false, cancellationToken); + + RaisePropertyChanged(CurrentViewProperty, current, CurrentView); } finally { @@ -88,6 +95,7 @@ public async Task RemoveViewAsync(object view, NavigateType navigateType, { if (!Children.Contains(view)) return false; + var current = CurrentView; if (CurrentView == view) { var to = Children.Count > 1 ? Children[^2] : null; @@ -97,6 +105,8 @@ public async Task RemoveViewAsync(object view, NavigateType navigateType, Children.Remove(view as Control); await OnContentUpdateAsync(CurrentView, cancellationToken); + RaisePropertyChanged(CurrentViewProperty, current, CurrentView); + return true; } finally @@ -113,9 +123,12 @@ protected virtual Task OnContentUpdateAsync(object? view, CancellationToken canc public Task ClearStackAsync(CancellationToken cancellationToken) { + var current = CurrentView; while (Children.Count > 1) Children.RemoveAt(0); + RaisePropertyChanged(CurrentViewProperty, current, CurrentView); + return Task.CompletedTask; } } diff --git a/src/AvaloniaInside.Shell/TabPage.cs b/src/AvaloniaInside.Shell/TabPage.cs new file mode 100644 index 0000000..3a1e4ff --- /dev/null +++ b/src/AvaloniaInside.Shell/TabPage.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections; +using Avalonia.Controls; + +namespace AvaloniaInside.Shell; + +public class HostedTabPage : Page, ISelectableHostedItems +{ + public event EventHandler? SelectionChanged; + + public IEnumerable? ItemsSource { get; set; } + public ItemCollection Items { get; } + public object? SelectedItem { get; set; } +} diff --git a/src/AvaloniaInside.Shell/Theme/Default/Colors.axaml b/src/AvaloniaInside.Shell/Theme/Default/Colors.axaml new file mode 100644 index 0000000..b22b614 --- /dev/null +++ b/src/AvaloniaInside.Shell/Theme/Default/Colors.axaml @@ -0,0 +1,14 @@ + + + + + White + + + Black + Black + + + + diff --git a/src/AvaloniaInside.Shell/Theme/Default/Controls.axaml b/src/AvaloniaInside.Shell/Theme/Default/Controls.axaml new file mode 100644 index 0000000..c4fc01b --- /dev/null +++ b/src/AvaloniaInside.Shell/Theme/Default/Controls.axaml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + diff --git a/src/AvaloniaInside.Shell/Theme/Default/HostedTabPage.axaml b/src/AvaloniaInside.Shell/Theme/Default/HostedTabPage.axaml new file mode 100644 index 0000000..2f0ccb9 --- /dev/null +++ b/src/AvaloniaInside.Shell/Theme/Default/HostedTabPage.axaml @@ -0,0 +1,5 @@ + + + + diff --git a/src/AvaloniaInside.Shell/Theme/Default/NavigationBar.axaml b/src/AvaloniaInside.Shell/Theme/Default/NavigationBar.axaml new file mode 100644 index 0000000..d0819cf --- /dev/null +++ b/src/AvaloniaInside.Shell/Theme/Default/NavigationBar.axaml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/AvaloniaInside.Shell/Theme/Default/Page.axaml b/src/AvaloniaInside.Shell/Theme/Default/Page.axaml new file mode 100644 index 0000000..2fe7006 --- /dev/null +++ b/src/AvaloniaInside.Shell/Theme/Default/Page.axaml @@ -0,0 +1,7 @@ + + + + + + diff --git a/src/AvaloniaInside.Shell/Theme/Default/ShellView.axaml b/src/AvaloniaInside.Shell/Theme/Default/ShellView.axaml new file mode 100644 index 0000000..a6b3417 --- /dev/null +++ b/src/AvaloniaInside.Shell/Theme/Default/ShellView.axaml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/AvaloniaInside.Shell/Theme/Default/SideMenu.axaml b/src/AvaloniaInside.Shell/Theme/Default/SideMenu.axaml new file mode 100644 index 0000000..0fc5111 --- /dev/null +++ b/src/AvaloniaInside.Shell/Theme/Default/SideMenu.axaml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/AvaloniaInside.Shell/Theme/Ios/Colors.axaml b/src/AvaloniaInside.Shell/Theme/Ios/Colors.axaml new file mode 100644 index 0000000..35f5b42 --- /dev/null +++ b/src/AvaloniaInside.Shell/Theme/Ios/Colors.axaml @@ -0,0 +1,14 @@ + + + + + White + + + Black + Black + + + + diff --git a/src/AvaloniaInside.Shell/Theme/Ios/Controls.axaml b/src/AvaloniaInside.Shell/Theme/Ios/Controls.axaml new file mode 100644 index 0000000..e1ac1bf --- /dev/null +++ b/src/AvaloniaInside.Shell/Theme/Ios/Controls.axaml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + diff --git a/src/AvaloniaInside.Shell/Theme/Ios/NavigationBar.axaml b/src/AvaloniaInside.Shell/Theme/Ios/NavigationBar.axaml new file mode 100644 index 0000000..d0819cf --- /dev/null +++ b/src/AvaloniaInside.Shell/Theme/Ios/NavigationBar.axaml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/AvaloniaInside.Shell/Theme/Ios/Page.axaml b/src/AvaloniaInside.Shell/Theme/Ios/Page.axaml new file mode 100644 index 0000000..d5da259 --- /dev/null +++ b/src/AvaloniaInside.Shell/Theme/Ios/Page.axaml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + diff --git a/src/AvaloniaInside.Shell/Theme/Ios/ShellView.axaml b/src/AvaloniaInside.Shell/Theme/Ios/ShellView.axaml new file mode 100644 index 0000000..280aa8d --- /dev/null +++ b/src/AvaloniaInside.Shell/Theme/Ios/ShellView.axaml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/AvaloniaInside.Shell/Theme/Ios/SideMenu.axaml b/src/AvaloniaInside.Shell/Theme/Ios/SideMenu.axaml new file mode 100644 index 0000000..0fc5111 --- /dev/null +++ b/src/AvaloniaInside.Shell/Theme/Ios/SideMenu.axaml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/AvaloniaInside.Shell/Theme/StackContentView.axaml b/src/AvaloniaInside.Shell/Theme/StackContentView.axaml deleted file mode 100644 index b6eb5ba..0000000 --- a/src/AvaloniaInside.Shell/Theme/StackContentView.axaml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - diff --git a/src/Example/ShellExample/ShellExample.Android/ShellExample.Android.csproj b/src/Example/ShellExample/ShellExample.Android/ShellExample.Android.csproj index c6c8e65..ac3cbd1 100644 --- a/src/Example/ShellExample/ShellExample.Android/ShellExample.Android.csproj +++ b/src/Example/ShellExample/ShellExample.Android/ShellExample.Android.csproj @@ -1,15 +1,14 @@ - Exe - net8.0-android - 34 - enable - com.CompanyName.ShellExample - 1 - 1.0 - apk - v15.0 - true + Exe + net8.0-android + 21 + enable + com.CompanyName.ShellExample + 1 + 1.0 + apk + false True From 0aa81091130eac43ec8ad21ddfc180e8ee0202da Mon Sep 17 00:00:00 2001 From: Omid Mafakher Date: Mon, 26 Aug 2024 22:32:47 +0200 Subject: [PATCH 02/10] #27 adding hosted tab page and support dynamic navbar --- .../BindingNavigateConverter.cs | 3 +- .../DefaultNavigationUpdateStrategy.cs | 1 - src/AvaloniaInside.Shell/HostedItemsHelper.cs | 2 +- src/AvaloniaInside.Shell/HostedTabPage.cs | 124 ++++++++++++++++++ .../INavigationBarProvider.cs | 7 + .../INavigatorLifecycle.cs | 6 +- src/AvaloniaInside.Shell/NavigationBar.cs | 2 - src/AvaloniaInside.Shell/Page.cs | 39 +++++- .../Windows/DrillInNavigationTransition.cs | 1 - .../Presenters/PresenterBase.cs | 2 - .../ShellView.ItemNavigator.cs | 1 - .../ShellView.SideMenu.cs | 1 + src/AvaloniaInside.Shell/ShellView.cs | 2 +- src/AvaloniaInside.Shell/TabPage.cs | 14 -- .../Theme/Default/Controls.axaml | 1 + .../Theme/Default/HostedTabPage.axaml | 44 ++++++- .../Theme/Default/Page.axaml | 12 ++ src/AvaloniaInside.Shell/Theme/Ios/Page.axaml | 10 +- .../ShellExample.Android/SplashActivity.cs | 5 - .../ShellExample.iOS/AppDelegate.cs | 3 - .../ShellExample/ShellExample/Helper.cs | 6 - .../ShellExample/Helpers/ImageHelper.cs | 2 - .../ShellExample/ViewModels/MainViewModel.cs | 2 - .../ViewModels/SettingViewModel.cs | 4 - .../ShopViewModels/ProductDetailViewModel.cs | 5 - .../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.cs | 1 - .../Views/MainTabControl.axaml.cs | 1 - .../ShellExample/Views/MainView.axaml | 2 +- .../Views/PetsTabControlView.axaml.cs | 1 - .../ShellExample/Views/ProfileView.axaml.cs | 1 - .../ShellExample/Views/SecondView.axaml.cs | 1 - .../ShellExample/Views/SettingView.axaml.cs | 1 - .../ShopViews/ConfirmationCloseView.axaml.cs | 3 - .../ProductCatalogFilterView.axaml.cs | 1 - .../ShopViews/ProductCatalogView.axaml.cs | 1 - .../ShopViews/ProductDetailView.axaml.cs | 1 - .../ShellExample/Views/SimpleDialog.axaml.cs | 1 - .../ShellExample/Views/WelcomeView.axaml.cs | 3 - 42 files changed, 233 insertions(+), 95 deletions(-) create mode 100644 src/AvaloniaInside.Shell/HostedTabPage.cs create mode 100644 src/AvaloniaInside.Shell/INavigationBarProvider.cs delete mode 100644 src/AvaloniaInside.Shell/TabPage.cs 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/DefaultNavigationUpdateStrategy.cs b/src/AvaloniaInside.Shell/DefaultNavigationUpdateStrategy.cs index 8fca8b5..72e0793 100644 --- a/src/AvaloniaInside.Shell/DefaultNavigationUpdateStrategy.cs +++ b/src/AvaloniaInside.Shell/DefaultNavigationUpdateStrategy.cs @@ -3,7 +3,6 @@ using System.Threading; using System.Threading.Tasks; using Avalonia.Controls; -using Avalonia.Controls.Primitives; namespace AvaloniaInside.Shell; diff --git a/src/AvaloniaInside.Shell/HostedItemsHelper.cs b/src/AvaloniaInside.Shell/HostedItemsHelper.cs index 501a284..b2aa90e 100644 --- a/src/AvaloniaInside.Shell/HostedItemsHelper.cs +++ b/src/AvaloniaInside.Shell/HostedItemsHelper.cs @@ -35,7 +35,7 @@ public object? SelectedItem } public static bool CanBeHosted(Type viewType) => - viewType.IsSubclassOf(typeof(ItemsControl)) || viewType.IsSubclassOf(typeof(IHostedItems)); + viewType.IsSubclassOf(typeof(ItemsControl)) || typeof(IHostedItems).IsAssignableFrom(viewType); public static bool CanBeHosted(object view) => view is ItemsControl or SelectingItemsControl or IHostedItems or ISelectableHostedItems; diff --git a/src/AvaloniaInside.Shell/HostedTabPage.cs b/src/AvaloniaInside.Shell/HostedTabPage.cs new file mode 100644 index 0000000..1978e03 --- /dev/null +++ b/src/AvaloniaInside.Shell/HostedTabPage.cs @@ -0,0 +1,124 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Templates; +using Avalonia.Data; + +namespace AvaloniaInside.Shell; + +public class HostedTabPage : Page, ISelectableHostedItems +{ + public event EventHandler? SelectionChanged; + + protected override Type StyleKeyOverride => typeof(HostedTabPage); + + #region ItemTemplate + + public static readonly StyledProperty ItemTemplateProperty = + AvaloniaProperty.Register(nameof(ItemTemplate)); + + public IDataTemplate? ItemTemplate + { + get => GetValue(ItemTemplateProperty); + set => SetValue(ItemTemplateProperty, value); + } + + #endregion + + #region ItemsSource + + public static readonly StyledProperty ItemsSourceProperty = + AvaloniaProperty.Register(nameof(ItemsSource)); + + public IEnumerable? ItemsSource + { + get => GetValue(ItemsSourceProperty); + set => SetValue(ItemsSourceProperty, value); + } + +#pragma warning disable CS8603 // Possible null reference return. + public ItemCollection Items => null; +#pragma warning restore CS8603 // Possible null reference return. + + #endregion + + #region SelectedIndex + + public static readonly DirectProperty SelectedIndexProperty = + AvaloniaProperty.RegisterDirect( + nameof(SelectedIndex), + o => o.SelectedIndex, + (o, v) => o.SelectedIndex = v, + unsetValue: -1, + defaultBindingMode: BindingMode.TwoWay); + + private int _selectedIndex; + public int SelectedIndex + { + get => _selectedIndex; + set + { + if (ItemsSource is not { } itemsSource) return; + + if (itemsSource.Cast().Skip(value).FirstOrDefault() is not { } found) + throw new IndexOutOfRangeException($"{value} out of index"); + + if (SetAndRaise(SelectedIndexProperty, ref _selectedIndex, value)) + { + SelectedItem = found; + } + } + } + + #endregion + + #region SelectedItem + + public static readonly DirectProperty SelectedItemProperty = + AvaloniaProperty.RegisterDirect( + nameof(SelectedItem), + o => o.SelectedItem, + (o, v) => o.SelectedItem = v, + defaultBindingMode: BindingMode.TwoWay, enableDataValidation: true); + + private object? _selectedItem; + + public object? SelectedItem + { + get => _selectedItem; + set + { + var current = _selectedItem; + if (SetAndRaise(SelectedItemProperty, ref _selectedItem, value)) + SelectionChanged?.Invoke( + this, + new SelectionChangedEventArgs( + null, + current == null ? [] : new List { current }, + value == null ? [] : new List { value })); + } + } + + #endregion + + #region SelectedContentTemplate + + public static readonly DirectProperty SelectedContentTemplateProperty = + AvaloniaProperty.RegisterDirect( + nameof(SelectedContentTemplate), + o => o.SelectedContentTemplate, + (o, v) => o.SelectedContentTemplate = v); + + private IDataTemplate? _selectedContentTemplate; + + public IDataTemplate? SelectedContentTemplate + { + get => _selectedContentTemplate; + set => SetAndRaise(SelectedContentTemplateProperty, ref _selectedContentTemplate, value); + } + + #endregion +} 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/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/NavigationBar.cs b/src/AvaloniaInside.Shell/NavigationBar.cs index 04b7dfe..84e183b 100644 --- a/src/AvaloniaInside.Shell/NavigationBar.cs +++ b/src/AvaloniaInside.Shell/NavigationBar.cs @@ -1,6 +1,4 @@ using System; -using System.Threading; -using System.Threading.Tasks; using System.Windows.Input; using Avalonia; using Avalonia.Controls; diff --git a/src/AvaloniaInside.Shell/Page.cs b/src/AvaloniaInside.Shell/Page.cs index 73b0872..b1c28ca 100644 --- a/src/AvaloniaInside.Shell/Page.cs +++ b/src/AvaloniaInside.Shell/Page.cs @@ -3,18 +3,23 @@ using System.Threading; using System.Threading.Tasks; using Avalonia; +using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; +using Avalonia.Interactivity; namespace AvaloniaInside.Shell; -public class Page : UserControl, INavigationLifecycle, INavigatorLifecycle +public class Page : UserControl, INavigationLifecycle, INavigatorLifecycle, INavigationBarProvider { public static readonly StyledProperty ShellProperty = AvaloniaProperty.Register(nameof(Shell)); + private ContentPresenter? _navigationBarPlaceHolder; private NavigationBar? _navigationBar; - public NavigationBar? NavigationBar => _navigationBar ?? Shell?.AttachedNavigationBar; + public NavigationBar? NavigationBar => FindNavigationBar(true) ?? Shell?.AttachedNavigationBar; + + public NavigationBar? AttachedNavigationBar => _navigationBar; public ShellView? Shell { @@ -37,6 +42,36 @@ public ShellView? Shell protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { base.OnApplyTemplate(e); + _navigationBarPlaceHolder = e.NameScope.Find("PART_NavigationBarPlaceHolder"); _navigationBar = e.NameScope.Find("PART_NavigationBar"); } + + protected override void OnLoaded(RoutedEventArgs e) + { + base.OnLoaded(e); + + if (_navigationBarPlaceHolder == null) return; + if (FindNavigationBar(true) != null) return; + + _navigationBarPlaceHolder.Content = _navigationBar = new NavigationBar() + { + ShellView = Shell + }; + } + + private NavigationBar? FindNavigationBar(bool includeSelf) + { + if (includeSelf && _navigationBar != null) return _navigationBar; + if (Shell?.AttachedNavigationBar is { } shellAttachedNavigationBar) return shellAttachedNavigationBar; + if (Navigator?.CurrentChain?.GetAscendingNodes() is not { } nodes) return null; + + foreach (var item in nodes) + { + if (!item.Hosted) return null; + if (item.Instance is INavigationBarProvider { AttachedNavigationBar: { } navigationBar }) + return navigationBar; + } + + return null; + } } diff --git a/src/AvaloniaInside.Shell/Platform/Windows/DrillInNavigationTransition.cs b/src/AvaloniaInside.Shell/Platform/Windows/DrillInNavigationTransition.cs index fb3122e..cf6ddfd 100644 --- a/src/AvaloniaInside.Shell/Platform/Windows/DrillInNavigationTransition.cs +++ b/src/AvaloniaInside.Shell/Platform/Windows/DrillInNavigationTransition.cs @@ -5,7 +5,6 @@ using System; using System.Threading; using System.Threading.Tasks; -using AvaloniaInside.Shell.Platform.Ios; namespace AvaloniaInside.Shell.Platform.Windows; public class DrillInNavigationTransition : PlatformBasePageTransition diff --git a/src/AvaloniaInside.Shell/Presenters/PresenterBase.cs b/src/AvaloniaInside.Shell/Presenters/PresenterBase.cs index 1e7f114..3deb539 100644 --- a/src/AvaloniaInside.Shell/Presenters/PresenterBase.cs +++ b/src/AvaloniaInside.Shell/Presenters/PresenterBase.cs @@ -3,8 +3,6 @@ using System.Threading; using System.Threading.Tasks; using Avalonia.Collections; -using Avalonia.Controls; -using Avalonia.Controls.Primitives; namespace AvaloniaInside.Shell.Presenters; diff --git a/src/AvaloniaInside.Shell/ShellView.ItemNavigator.cs b/src/AvaloniaInside.Shell/ShellView.ItemNavigator.cs index adc177e..8e183a8 100644 --- a/src/AvaloniaInside.Shell/ShellView.ItemNavigator.cs +++ b/src/AvaloniaInside.Shell/ShellView.ItemNavigator.cs @@ -2,7 +2,6 @@ using System.Collections.Specialized; using System.Linq; using Avalonia.Collections; -using Avalonia.Controls; using Avalonia.Metadata; using AvaloniaInside.Shell.Data; diff --git a/src/AvaloniaInside.Shell/ShellView.SideMenu.cs b/src/AvaloniaInside.Shell/ShellView.SideMenu.cs index 3861a2e..3c6f02f 100644 --- a/src/AvaloniaInside.Shell/ShellView.SideMenu.cs +++ b/src/AvaloniaInside.Shell/ShellView.SideMenu.cs @@ -218,6 +218,7 @@ public AvaloniaList SideMenuContents nameof(SideMenuSelectedItem), o => o.SideMenuSelectedItem, (o, v) => o.SideMenuSelectedItem = v); + public SideMenuItem? SideMenuSelectedItem { get => _sideMenuSelectedItem; diff --git a/src/AvaloniaInside.Shell/ShellView.cs b/src/AvaloniaInside.Shell/ShellView.cs index 4d4d552..06c8b79 100644 --- a/src/AvaloniaInside.Shell/ShellView.cs +++ b/src/AvaloniaInside.Shell/ShellView.cs @@ -15,7 +15,7 @@ namespace AvaloniaInside.Shell; -public partial class ShellView : TemplatedControl +public partial class ShellView : TemplatedControl, INavigationBarProvider { #region Enums diff --git a/src/AvaloniaInside.Shell/TabPage.cs b/src/AvaloniaInside.Shell/TabPage.cs deleted file mode 100644 index 3a1e4ff..0000000 --- a/src/AvaloniaInside.Shell/TabPage.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; -using System.Collections; -using Avalonia.Controls; - -namespace AvaloniaInside.Shell; - -public class HostedTabPage : Page, ISelectableHostedItems -{ - public event EventHandler? SelectionChanged; - - public IEnumerable? ItemsSource { get; set; } - public ItemCollection Items { get; } - public object? SelectedItem { get; set; } -} diff --git a/src/AvaloniaInside.Shell/Theme/Default/Controls.axaml b/src/AvaloniaInside.Shell/Theme/Default/Controls.axaml index c4fc01b..3664cd8 100644 --- a/src/AvaloniaInside.Shell/Theme/Default/Controls.axaml +++ b/src/AvaloniaInside.Shell/Theme/Default/Controls.axaml @@ -7,6 +7,7 @@ + diff --git a/src/AvaloniaInside.Shell/Theme/Default/HostedTabPage.axaml b/src/AvaloniaInside.Shell/Theme/Default/HostedTabPage.axaml index 2f0ccb9..1e3bea3 100644 --- a/src/AvaloniaInside.Shell/Theme/Default/HostedTabPage.axaml +++ b/src/AvaloniaInside.Shell/Theme/Default/HostedTabPage.axaml @@ -1,5 +1,47 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/AvaloniaInside.Shell/Theme/Default/Page.axaml b/src/AvaloniaInside.Shell/Theme/Default/Page.axaml index 2fe7006..64d34ba 100644 --- a/src/AvaloniaInside.Shell/Theme/Default/Page.axaml +++ b/src/AvaloniaInside.Shell/Theme/Default/Page.axaml @@ -2,6 +2,18 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> + + + + + + + + + + + diff --git a/src/AvaloniaInside.Shell/Theme/Ios/Page.axaml b/src/AvaloniaInside.Shell/Theme/Ios/Page.axaml index d5da259..e5fc08a 100644 --- a/src/AvaloniaInside.Shell/Theme/Ios/Page.axaml +++ b/src/AvaloniaInside.Shell/Theme/Ios/Page.axaml @@ -6,17 +6,13 @@ - - + + Content="{TemplateBinding Content}"> + - diff --git a/src/Example/ShellExample/ShellExample.Android/SplashActivity.cs b/src/Example/ShellExample/ShellExample.Android/SplashActivity.cs index 84fa595..92f63b0 100644 --- a/src/Example/ShellExample/ShellExample.Android/SplashActivity.cs +++ b/src/Example/ShellExample/ShellExample.Android/SplashActivity.cs @@ -1,11 +1,6 @@ using Android.App; using Android.Content; -using Android.OS; using Application = Android.App.Application; -using Avalonia; -using Avalonia.Android; -using Avalonia.ReactiveUI; -using AvaloniaInside.Shell; namespace ShellExample.Android; diff --git a/src/Example/ShellExample/ShellExample.iOS/AppDelegate.cs b/src/Example/ShellExample/ShellExample.iOS/AppDelegate.cs index 4b9cbc0..c575914 100644 --- a/src/Example/ShellExample/ShellExample.iOS/AppDelegate.cs +++ b/src/Example/ShellExample/ShellExample.iOS/AppDelegate.cs @@ -1,9 +1,6 @@ using Foundation; -using UIKit; using Avalonia; -using Avalonia.Controls; using Avalonia.iOS; -using Avalonia.Media; using Avalonia.ReactiveUI; using AvaloniaInside.Shell; diff --git a/src/Example/ShellExample/ShellExample/Helper.cs b/src/Example/ShellExample/ShellExample/Helper.cs index bf85104..22e731d 100644 --- a/src/Example/ShellExample/ShellExample/Helper.cs +++ b/src/Example/ShellExample/ShellExample/Helper.cs @@ -1,11 +1,5 @@ using Avalonia.Controls; using Avalonia.VisualTree; -using AvaloniaInside.Shell.Platform; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace ShellExample; public static class Helper diff --git a/src/Example/ShellExample/ShellExample/Helpers/ImageHelper.cs b/src/Example/ShellExample/ShellExample/Helpers/ImageHelper.cs index 86f53ad..568b3eb 100644 --- a/src/Example/ShellExample/ShellExample/Helpers/ImageHelper.cs +++ b/src/Example/ShellExample/ShellExample/Helpers/ImageHelper.cs @@ -1,8 +1,6 @@ using System; -using Avalonia; using Avalonia.Media.Imaging; using Avalonia.Platform; -using Splat; namespace ShellExample.Helpers; diff --git a/src/Example/ShellExample/ShellExample/ViewModels/MainViewModel.cs b/src/Example/ShellExample/ShellExample/ViewModels/MainViewModel.cs index f064e54..8bfb6c2 100644 --- a/src/Example/ShellExample/ShellExample/ViewModels/MainViewModel.cs +++ b/src/Example/ShellExample/ShellExample/ViewModels/MainViewModel.cs @@ -1,8 +1,6 @@ using Avalonia.Animation; using AvaloniaInside.Shell.Platform; -using AvaloniaInside.Shell.Platform.Windows; using ReactiveUI; -using static ShellExample.ViewModels.SettingViewModel; namespace ShellExample.ViewModels; diff --git a/src/Example/ShellExample/ShellExample/ViewModels/SettingViewModel.cs b/src/Example/ShellExample/ShellExample/ViewModels/SettingViewModel.cs index 57871df..fd8bddd 100644 --- a/src/Example/ShellExample/ShellExample/ViewModels/SettingViewModel.cs +++ b/src/Example/ShellExample/ShellExample/ViewModels/SettingViewModel.cs @@ -5,11 +5,7 @@ using AvaloniaInside.Shell.Platform.Ios; using AvaloniaInside.Shell.Platform.Windows; using ReactiveUI; -using System; -using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace ShellExample.ViewModels; public class SettingViewModel : ViewModelBase diff --git a/src/Example/ShellExample/ShellExample/ViewModels/ShopViewModels/ProductDetailViewModel.cs b/src/Example/ShellExample/ShellExample/ViewModels/ShopViewModels/ProductDetailViewModel.cs index 228cedc..fbd4ac2 100644 --- a/src/Example/ShellExample/ShellExample/ViewModels/ShopViewModels/ProductDetailViewModel.cs +++ b/src/Example/ShellExample/ShellExample/ViewModels/ShopViewModels/ProductDetailViewModel.cs @@ -1,9 +1,4 @@ using ShellExample.Models; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace ShellExample.ViewModels.ShopViewModels; public class ProductDetailViewModel : ViewModelBase diff --git a/src/Example/ShellExample/ShellExample/ViewModels/WelcomeViewModel.cs b/src/Example/ShellExample/ShellExample/ViewModels/WelcomeViewModel.cs index 7ef5fe9..7bfad94 100644 --- a/src/Example/ShellExample/ShellExample/ViewModels/WelcomeViewModel.cs +++ b/src/Example/ShellExample/ShellExample/ViewModels/WelcomeViewModel.cs @@ -1,9 +1,5 @@ using AvaloniaInside.Shell; using ReactiveUI; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Threading; using System.Threading.Tasks; using System.Windows.Input; diff --git a/src/Example/ShellExample/ShellExample/Views/CatView.axaml.cs b/src/Example/ShellExample/ShellExample/Views/CatView.axaml.cs index 1222a6d..2e8b755 100644 --- a/src/Example/ShellExample/ShellExample/Views/CatView.axaml.cs +++ b/src/Example/ShellExample/ShellExample/Views/CatView.axaml.cs @@ -1,4 +1,3 @@ -using Avalonia.Controls; using Avalonia.Markup.Xaml; using AvaloniaInside.Shell; diff --git a/src/Example/ShellExample/ShellExample/Views/DogView.axaml.cs b/src/Example/ShellExample/ShellExample/Views/DogView.axaml.cs index e632157..6793a6e 100644 --- a/src/Example/ShellExample/ShellExample/Views/DogView.axaml.cs +++ b/src/Example/ShellExample/ShellExample/Views/DogView.axaml.cs @@ -1,4 +1,3 @@ -using Avalonia.Controls; using Avalonia.Markup.Xaml; using AvaloniaInside.Shell; diff --git a/src/Example/ShellExample/ShellExample/Views/FontIconImageSource.cs b/src/Example/ShellExample/ShellExample/Views/FontIconImageSource.cs index 5a15579..f2f0337 100644 --- a/src/Example/ShellExample/ShellExample/Views/FontIconImageSource.cs +++ b/src/Example/ShellExample/ShellExample/Views/FontIconImageSource.cs @@ -1,11 +1,6 @@ using Avalonia.Media; using Avalonia; using Projektanker.Icons.Avalonia; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace ShellExample.Views; internal class FontIconImageSource : DrawingImage diff --git a/src/Example/ShellExample/ShellExample/Views/HomePage.axaml.cs b/src/Example/ShellExample/ShellExample/Views/HomePage.axaml.cs index d121172..0b8d2d1 100644 --- a/src/Example/ShellExample/ShellExample/Views/HomePage.axaml.cs +++ b/src/Example/ShellExample/ShellExample/Views/HomePage.axaml.cs @@ -1,4 +1,3 @@ -using Avalonia.Controls; using Avalonia.Markup.Xaml; using AvaloniaInside.Shell; using System.Threading; diff --git a/src/Example/ShellExample/ShellExample/Views/MainTabControl.axaml.cs b/src/Example/ShellExample/ShellExample/Views/MainTabControl.axaml.cs index e8ea2ea..2c14ce9 100644 --- a/src/Example/ShellExample/ShellExample/Views/MainTabControl.axaml.cs +++ b/src/Example/ShellExample/ShellExample/Views/MainTabControl.axaml.cs @@ -1,7 +1,6 @@ using System; using Avalonia.Controls; using Avalonia.Markup.Xaml; -using Avalonia.Styling; namespace ShellExample.Views; diff --git a/src/Example/ShellExample/ShellExample/Views/MainView.axaml b/src/Example/ShellExample/ShellExample/Views/MainView.axaml index 4f68fc7..d2a6560 100644 --- a/src/Example/ShellExample/ShellExample/Views/MainView.axaml +++ b/src/Example/ShellExample/ShellExample/Views/MainView.axaml @@ -34,7 +34,7 @@ - + diff --git a/src/Example/ShellExample/ShellExample/Views/PetsTabControlView.axaml.cs b/src/Example/ShellExample/ShellExample/Views/PetsTabControlView.axaml.cs index 9876058..fc00ab8 100644 --- a/src/Example/ShellExample/ShellExample/Views/PetsTabControlView.axaml.cs +++ b/src/Example/ShellExample/ShellExample/Views/PetsTabControlView.axaml.cs @@ -1,7 +1,6 @@ using System; using Avalonia.Controls; using Avalonia.Markup.Xaml; -using Avalonia.Styling; namespace ShellExample.Views; diff --git a/src/Example/ShellExample/ShellExample/Views/ProfileView.axaml.cs b/src/Example/ShellExample/ShellExample/Views/ProfileView.axaml.cs index 53e50d0..13238da 100644 --- a/src/Example/ShellExample/ShellExample/Views/ProfileView.axaml.cs +++ b/src/Example/ShellExample/ShellExample/Views/ProfileView.axaml.cs @@ -1,4 +1,3 @@ -using Avalonia.Controls; using Avalonia.Markup.Xaml; using AvaloniaInside.Shell; diff --git a/src/Example/ShellExample/ShellExample/Views/SecondView.axaml.cs b/src/Example/ShellExample/ShellExample/Views/SecondView.axaml.cs index 1af1195..2c8a409 100644 --- a/src/Example/ShellExample/ShellExample/Views/SecondView.axaml.cs +++ b/src/Example/ShellExample/ShellExample/Views/SecondView.axaml.cs @@ -1,4 +1,3 @@ -using Avalonia.Controls; using Avalonia.Markup.Xaml; using AvaloniaInside.Shell; diff --git a/src/Example/ShellExample/ShellExample/Views/SettingView.axaml.cs b/src/Example/ShellExample/ShellExample/Views/SettingView.axaml.cs index 427beac..f78743a 100644 --- a/src/Example/ShellExample/ShellExample/Views/SettingView.axaml.cs +++ b/src/Example/ShellExample/ShellExample/Views/SettingView.axaml.cs @@ -1,4 +1,3 @@ -using Avalonia.Controls; using Avalonia.Markup.Xaml; using AvaloniaInside.Shell; using ShellExample.ViewModels; diff --git a/src/Example/ShellExample/ShellExample/Views/ShopViews/ConfirmationCloseView.axaml.cs b/src/Example/ShellExample/ShellExample/Views/ShopViews/ConfirmationCloseView.axaml.cs index dab85e3..84fef53 100644 --- a/src/Example/ShellExample/ShellExample/Views/ShopViews/ConfirmationCloseView.axaml.cs +++ b/src/Example/ShellExample/ShellExample/Views/ShopViews/ConfirmationCloseView.axaml.cs @@ -1,6 +1,3 @@ -using Avalonia; -using Avalonia.Controls; -using Avalonia.Markup.Xaml; using AvaloniaInside.Shell; namespace ShellExample.Views.ShopViews; diff --git a/src/Example/ShellExample/ShellExample/Views/ShopViews/ProductCatalogFilterView.axaml.cs b/src/Example/ShellExample/ShellExample/Views/ShopViews/ProductCatalogFilterView.axaml.cs index c216f33..281f0cf 100644 --- a/src/Example/ShellExample/ShellExample/Views/ShopViews/ProductCatalogFilterView.axaml.cs +++ b/src/Example/ShellExample/ShellExample/Views/ShopViews/ProductCatalogFilterView.axaml.cs @@ -1,6 +1,5 @@ using System.Threading; using System.Threading.Tasks; -using Avalonia.Controls; using Avalonia.Markup.Xaml; using AvaloniaInside.Shell; using ShellExample.ViewModels.ShopViewModels; diff --git a/src/Example/ShellExample/ShellExample/Views/ShopViews/ProductCatalogView.axaml.cs b/src/Example/ShellExample/ShellExample/Views/ShopViews/ProductCatalogView.axaml.cs index aa1ccba..511b4a0 100644 --- a/src/Example/ShellExample/ShellExample/Views/ShopViews/ProductCatalogView.axaml.cs +++ b/src/Example/ShellExample/ShellExample/Views/ShopViews/ProductCatalogView.axaml.cs @@ -1,4 +1,3 @@ -using Avalonia.Controls; using Avalonia.Markup.Xaml; using AvaloniaInside.Shell; using ShellExample.ViewModels.ShopViewModels; diff --git a/src/Example/ShellExample/ShellExample/Views/ShopViews/ProductDetailView.axaml.cs b/src/Example/ShellExample/ShellExample/Views/ShopViews/ProductDetailView.axaml.cs index 38cc83d..cb6a8a7 100644 --- a/src/Example/ShellExample/ShellExample/Views/ShopViews/ProductDetailView.axaml.cs +++ b/src/Example/ShellExample/ShellExample/Views/ShopViews/ProductDetailView.axaml.cs @@ -1,4 +1,3 @@ -using Avalonia.Controls; using AvaloniaInside.Shell; using ShellExample.Models; using ShellExample.ViewModels.ShopViewModels; diff --git a/src/Example/ShellExample/ShellExample/Views/SimpleDialog.axaml.cs b/src/Example/ShellExample/ShellExample/Views/SimpleDialog.axaml.cs index a6f6d15..2c3c5fc 100644 --- a/src/Example/ShellExample/ShellExample/Views/SimpleDialog.axaml.cs +++ b/src/Example/ShellExample/ShellExample/Views/SimpleDialog.axaml.cs @@ -1,4 +1,3 @@ -using Avalonia.Controls; using Avalonia.Interactivity; using Avalonia.Markup.Xaml; using AvaloniaInside.Shell; diff --git a/src/Example/ShellExample/ShellExample/Views/WelcomeView.axaml.cs b/src/Example/ShellExample/ShellExample/Views/WelcomeView.axaml.cs index 1078d15..a2f7243 100644 --- a/src/Example/ShellExample/ShellExample/Views/WelcomeView.axaml.cs +++ b/src/Example/ShellExample/ShellExample/Views/WelcomeView.axaml.cs @@ -1,7 +1,4 @@ -using Avalonia.Controls; -using Avalonia.Media; using AvaloniaInside.Shell; -using Projektanker.Icons.Avalonia; using System.Threading; using System.Threading.Tasks; From 15113d083903b1cb260263ece0621c05c1f899db Mon Sep 17 00:00:00 2001 From: Omid Mafakher Date: Wed, 4 Sep 2024 22:45:43 +0200 Subject: [PATCH 03/10] #27 supporting dynamic navigation bar --- .../DefaultNavigationUpdateStrategy.cs | 4 +- src/AvaloniaInside.Shell/HostedTabPage.cs | 124 --------- .../INavigationUpdateStrategy.cs | 3 +- src/AvaloniaInside.Shell/NavigationBar.cs | 15 +- .../NavigationBarAttachType.cs | 8 + src/AvaloniaInside.Shell/NavigationStack.cs | 195 ++++++++------- .../NavigationStackChanges.cs | 1 + src/AvaloniaInside.Shell/Navigator.cs | 27 +- src/AvaloniaInside.Shell/Page.cs | 97 ++++---- .../Platform/PlatformSetup.cs | 10 + src/AvaloniaInside.Shell/ShellView.cs | 70 +++++- src/AvaloniaInside.Shell/TabPage.cs | 235 ++++++++++++++++++ .../Theme/Default/Controls.axaml | 2 +- .../Theme/Default/NavigationBar.axaml | 2 + .../Theme/Default/ShellView.axaml | 2 +- .../{HostedTabPage.axaml => TabPage.axaml} | 24 +- .../ShellExample/ShellExample/Styles.axaml | 86 +------ .../ShellExample/Views/HomePage.axaml | 4 + .../ShellExample/Views/HomePage.axaml.cs | 2 - .../ShellExample/Views/MainView.axaml | 4 +- .../Views/PetsTabControlView.axaml | 50 ++-- .../Views/PetsTabControlView.axaml.cs | 7 +- .../ShellExample/Views/ProfileView.axaml | 5 + .../ShellExample/Views/ProfileView.axaml.cs | 2 - .../ShellExample/Views/SettingView.axaml | 7 +- .../ShellExample/Views/SettingView.axaml.cs | 2 - .../Views/ShopViews/ProductCatalogView.axaml | 4 + 27 files changed, 578 insertions(+), 414 deletions(-) delete mode 100644 src/AvaloniaInside.Shell/HostedTabPage.cs create mode 100644 src/AvaloniaInside.Shell/NavigationBarAttachType.cs create mode 100644 src/AvaloniaInside.Shell/TabPage.cs rename src/AvaloniaInside.Shell/Theme/Default/{HostedTabPage.axaml => TabPage.axaml} (66%) diff --git a/src/AvaloniaInside.Shell/DefaultNavigationUpdateStrategy.cs b/src/AvaloniaInside.Shell/DefaultNavigationUpdateStrategy.cs index 72e0793..e86bdbb 100644 --- a/src/AvaloniaInside.Shell/DefaultNavigationUpdateStrategy.cs +++ b/src/AvaloniaInside.Shell/DefaultNavigationUpdateStrategy.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Avalonia.Controls; @@ -20,7 +21,6 @@ public DefaultNavigationUpdateStrategy(IPresenterProvider presenterProvider) public async Task UpdateChangesAsync( ShellView shellView, NavigationStackChanges changes, - List newInstances, NavigateType navigateType, object? argument, bool hasArgument, @@ -28,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); diff --git a/src/AvaloniaInside.Shell/HostedTabPage.cs b/src/AvaloniaInside.Shell/HostedTabPage.cs deleted file mode 100644 index 1978e03..0000000 --- a/src/AvaloniaInside.Shell/HostedTabPage.cs +++ /dev/null @@ -1,124 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using Avalonia; -using Avalonia.Controls; -using Avalonia.Controls.Templates; -using Avalonia.Data; - -namespace AvaloniaInside.Shell; - -public class HostedTabPage : Page, ISelectableHostedItems -{ - public event EventHandler? SelectionChanged; - - protected override Type StyleKeyOverride => typeof(HostedTabPage); - - #region ItemTemplate - - public static readonly StyledProperty ItemTemplateProperty = - AvaloniaProperty.Register(nameof(ItemTemplate)); - - public IDataTemplate? ItemTemplate - { - get => GetValue(ItemTemplateProperty); - set => SetValue(ItemTemplateProperty, value); - } - - #endregion - - #region ItemsSource - - public static readonly StyledProperty ItemsSourceProperty = - AvaloniaProperty.Register(nameof(ItemsSource)); - - public IEnumerable? ItemsSource - { - get => GetValue(ItemsSourceProperty); - set => SetValue(ItemsSourceProperty, value); - } - -#pragma warning disable CS8603 // Possible null reference return. - public ItemCollection Items => null; -#pragma warning restore CS8603 // Possible null reference return. - - #endregion - - #region SelectedIndex - - public static readonly DirectProperty SelectedIndexProperty = - AvaloniaProperty.RegisterDirect( - nameof(SelectedIndex), - o => o.SelectedIndex, - (o, v) => o.SelectedIndex = v, - unsetValue: -1, - defaultBindingMode: BindingMode.TwoWay); - - private int _selectedIndex; - public int SelectedIndex - { - get => _selectedIndex; - set - { - if (ItemsSource is not { } itemsSource) return; - - if (itemsSource.Cast().Skip(value).FirstOrDefault() is not { } found) - throw new IndexOutOfRangeException($"{value} out of index"); - - if (SetAndRaise(SelectedIndexProperty, ref _selectedIndex, value)) - { - SelectedItem = found; - } - } - } - - #endregion - - #region SelectedItem - - public static readonly DirectProperty SelectedItemProperty = - AvaloniaProperty.RegisterDirect( - nameof(SelectedItem), - o => o.SelectedItem, - (o, v) => o.SelectedItem = v, - defaultBindingMode: BindingMode.TwoWay, enableDataValidation: true); - - private object? _selectedItem; - - public object? SelectedItem - { - get => _selectedItem; - set - { - var current = _selectedItem; - if (SetAndRaise(SelectedItemProperty, ref _selectedItem, value)) - SelectionChanged?.Invoke( - this, - new SelectionChangedEventArgs( - null, - current == null ? [] : new List { current }, - value == null ? [] : new List { value })); - } - } - - #endregion - - #region SelectedContentTemplate - - public static readonly DirectProperty SelectedContentTemplateProperty = - AvaloniaProperty.RegisterDirect( - nameof(SelectedContentTemplate), - o => o.SelectedContentTemplate, - (o, v) => o.SelectedContentTemplate = v); - - private IDataTemplate? _selectedContentTemplate; - - public IDataTemplate? SelectedContentTemplate - { - get => _selectedContentTemplate; - set => SetAndRaise(SelectedContentTemplateProperty, ref _selectedContentTemplate, value); - } - - #endregion -} 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/NavigationBar.cs b/src/AvaloniaInside.Shell/NavigationBar.cs index 84e183b..2c1e21b 100644 --- a/src/AvaloniaInside.Shell/NavigationBar.cs +++ b/src/AvaloniaInside.Shell/NavigationBar.cs @@ -165,6 +165,19 @@ public static void SetHeader(AvaloniaObject element, object parameter) => #endregion + #region Header + + 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 = @@ -220,7 +233,7 @@ protected override void OnApplyTemplate(TemplateAppliedEventArgs e) UpdateView(_pendingHeader); } - private void UpdateView(object? view) + internal void UpdateView(object? view) { if (_header == null && _items == null) { diff --git a/src/AvaloniaInside.Shell/NavigationBarAttachType.cs b/src/AvaloniaInside.Shell/NavigationBarAttachType.cs new file mode 100644 index 0000000..ed135c8 --- /dev/null +++ b/src/AvaloniaInside.Shell/NavigationBarAttachType.cs @@ -0,0 +1,8 @@ +namespace AvaloniaInside.Shell; + +public enum NavigationBarAttachType +{ + ToShell, + ToFirstHostThenPage, + ToLastPage +} diff --git a/src/AvaloniaInside.Shell/NavigationStack.cs b/src/AvaloniaInside.Shell/NavigationStack.cs index f8c1de4..431c366 100644 --- a/src/AvaloniaInside.Shell/NavigationStack.cs +++ b/src/AvaloniaInside.Shell/NavigationStack.cs @@ -4,40 +4,41 @@ namespace AvaloniaInside.Shell; -public class NavigationStack +public class NavigationStack(INavigationViewLocator viewLocator) { public NavigationChain? Current { get; set; } - public NavigationStackChanges Push(NavigationNode node, NavigateType type, Uri uri, - Func instance) + public NavigationStackChanges Push( + NavigationNode node, + NavigateType type, + Uri uri) { - var chain = type switch + var changes = type switch { - NavigateType.ReplaceRoot => PushReplaceRoot(node, type, instance), - NavigateType.Normal or NavigateType.Modal => PushNormal(node, type, instance), - NavigateType.Replace => PushReplace(node, type, instance), - NavigateType.Top => PushTop(node, type, instance), - NavigateType.Clear => PushClear(node, type, instance), - NavigateType.Pop => Pop(node, type, uri, instance), - NavigateType.HostedItemChange => HostedItemChange(node, type, uri, instance) + NavigateType.ReplaceRoot => PushReplaceRoot(node, type), + NavigateType.Normal or NavigateType.Modal => PushNormal(node, type), + NavigateType.Replace => PushReplace(node, type), + NavigateType.Top => PushTop(node, type), + NavigateType.Clear => PushClear(node, type), + NavigateType.Pop => Pop(node, type, uri), + NavigateType.HostedItemChange => HostedItemChange(node, type, uri) }; - if (chain.Front != null) - chain.Front.Uri = uri; + if (changes.Front != null) + changes.Front.Uri = uri; - if (chain.Front != null) - AscendingHostVerification(chain.Front, instance); + if (changes.Front != null) + AscendingHostVerification(changes, changes.Front); - return chain; + return changes; } private NavigationStackChanges Pop( NavigationNode node, NavigateType type, - Uri uri, - Func getInstance) + Uri uri) { - if (Current == null) return PushTop(node, type, getInstance); + if (Current == null) return PushTop(node, type); var previous = Current; var list = new List(); @@ -58,15 +59,15 @@ private NavigationStackChanges Pop( list.Add(chain); } - return PushTop(node, type, getInstance); + return PushTop(node, type); } - private NavigationStackChanges PushReplaceRoot(NavigationNode node, NavigateType type, - Func getInstance) + private NavigationStackChanges PushReplaceRoot(NavigationNode node, NavigateType type) { var popList = new List(); var chain = Current; var previous = Current; + var changes = new NavigationStackChanges(); while (chain != null) { @@ -77,47 +78,50 @@ private NavigationStackChanges PushReplaceRoot(NavigationNode node, NavigateType foreach (var pop in popList) pop.Back = null; - Current = new NavigationChain { Node = node, Instance = getInstance(node), Type = type }; - return new NavigationStackChanges() - { - Front = Current, - Previous = previous, - Removed = popList - }; + Current = NewInstanceAndChain(changes, node, type, null); + + changes.Front = Current; + changes.Previous = previous; + changes.Removed = popList; + + return changes; } - private NavigationStackChanges PushNormal(NavigationNode node, NavigateType type, - Func getInstance) + private NavigationStackChanges PushNormal( + NavigationNode node, + NavigateType type) { - Current = new NavigationChain { Node = node, Instance = getInstance(node), Type = type, Back = Current }; - return new NavigationStackChanges() - { - Front = Current, - Previous = Current.Back - }; + var changes = new NavigationStackChanges(); + + Current = NewInstanceAndChain(changes, node, type, Current); + changes.Front = Current; + changes.Previous = Current.Back; + + return changes; } - private NavigationStackChanges PushReplace(NavigationNode node, NavigateType type, - Func getInstance) + private NavigationStackChanges PushReplace(NavigationNode node, NavigateType type) { var pop = Current; + var changes = new NavigationStackChanges(); - Current = new NavigationChain { Node = node, Instance = getInstance(node), Type = type, Back = pop?.Back }; - return new NavigationStackChanges() - { - Previous = pop, - Front = Current, - Removed = pop != null ? new List { pop } : null - }; + Current = NewInstanceAndChain(changes, node, type, pop?.Back); + + changes.Front = Current; + changes.Previous = pop; + changes.Removed = pop != null ? new List { pop } : null; + + return changes; } - private NavigationStackChanges PushTop(NavigationNode node, NavigateType type, - Func getInstance) + private NavigationStackChanges PushTop(NavigationNode node, NavigateType type) { var previousChain = Current; var current = Current; NavigationChain? previous = null; + var changes = new NavigationStackChanges(); + while (current != null) { if (current.Node == node) @@ -141,21 +145,22 @@ private NavigationStackChanges PushTop(NavigationNode node, NavigateType type, current = current.Back; } - Current = new NavigationChain { Node = node, Instance = getInstance(node), Type = type, Back = Current }; - return new NavigationStackChanges() - { - Previous = previousChain, - Front = Current - }; + Current = NewInstanceAndChain(changes, node, type, Current); + + changes.Front = Current; + changes.Previous = previousChain; + + return changes; } - private NavigationStackChanges PushClear(NavigationNode node, NavigateType type, - Func getInstance) + private NavigationStackChanges PushClear(NavigationNode node, NavigateType type) { var removedNodes = new List(); var previousChain = Current; var current = Current; NavigationChain? previous = null; + var changes = new NavigationStackChanges(); + while (current != null) { if (current.Node == node) @@ -178,21 +183,21 @@ private NavigationStackChanges PushClear(NavigationNode node, NavigateType type, current = current.Back; } - Current = new NavigationChain { Node = node, Instance = getInstance(node), Type = type, Back = Current }; - return new NavigationStackChanges - { - Previous = previousChain, - Front = Current, - Removed = removedNodes - }; + Current = NewInstanceAndChain(changes, node, type, Current); + + changes.Front = Current; + changes.Previous = previousChain; + changes.Removed = removedNodes; + + return changes; } private NavigationStackChanges HostedItemChange( NavigationNode node, NavigateType type, - Uri uri, - Func getInstance) + Uri uri) { + var changes = new NavigationStackChanges(); var found = Current?.GetAscendingNodes() .OfType() .SelectMany(h => h.AggregatedNodes) @@ -201,18 +206,11 @@ private NavigationStackChanges HostedItemChange( if (found != null) { Current = found; - return new NavigationStackChanges() { Front = found }; + changes.Front = found; + return changes; } - //return PushNormal(node, type, getInstance); - - Current = new NavigationChain - { - Node = node, - Instance = getInstance(node), - Type = type, - Back = Current - }; + Current = NewInstanceAndChain(changes, node, type, Current); var firstReachHost = default(HostNavigationChain); foreach (var chain in Current.GetAscendingNodes()) @@ -224,7 +222,10 @@ private NavigationStackChanges HostedItemChange( } if (firstReachHost == null) - return new NavigationStackChanges { Front = Current }; + { + changes.Front = Current; + return changes; + } var all = firstReachHost.AggregatedNodes.ToList(); var lastChainUpdated = Current; @@ -238,17 +239,18 @@ private NavigationStackChanges HostedItemChange( } } - return new NavigationStackChanges { Front = Current }; + changes.Front = Current; + return changes; } - private void AscendingHostVerification(NavigationChain chain, Func getInstance) + private void AscendingHostVerification(NavigationStackChanges changes, NavigationChain chain) { var parentNode = chain.Node.Parent; if (parentNode == null) return; if (parentNode.Type == NavigationNodeType.Page) return; if (chain.Back?.Node == parentNode && chain.Back is HostNavigationChain verifyHostChain) { - VerifyHostInitialised(chain, parentNode, verifyHostChain, getInstance); + VerifyHostInitialised(changes, chain, parentNode, verifyHostChain); return; } @@ -258,22 +260,23 @@ private void AscendingHostVerification(NavigationChain chain, Func getInstance) + HostNavigationChain parentChain) { foreach (var hostChildNode in parentNode.Nodes) { @@ -294,11 +297,31 @@ private static void VerifyHostInitialised( hostChildChain.Back = parentChain; hostChildChain.Node = hostChildNode; hostChildChain.Type = NavigateType.Normal; - hostChildChain.Instance = getInstance(hostChildNode); + hostChildChain.Instance = viewLocator.GetView(hostChildNode); hostChildChain.Uri = new Uri(chain.Uri, hostChildNode.Route); hostChildChain.Hosted = true; + changes.NewNavigationChains.Add(hostChildChain); + parentChain.Nodes.Add(hostChildChain); } } + + private NavigationChain NewInstanceAndChain( + NavigationStackChanges changes, + NavigationNode node, + NavigateType type, + NavigationChain? back) + { + var instance = viewLocator.GetView(node); + var chain = new NavigationChain + { + Node = node, + Instance = instance, + Type = type, + Back = back + }; + changes.NewNavigationChains.Add(chain); + return chain; + } } diff --git a/src/AvaloniaInside.Shell/NavigationStackChanges.cs b/src/AvaloniaInside.Shell/NavigationStackChanges.cs index 4f2b440..5a9bcbc 100644 --- a/src/AvaloniaInside.Shell/NavigationStackChanges.cs +++ b/src/AvaloniaInside.Shell/NavigationStackChanges.cs @@ -7,4 +7,5 @@ public class NavigationStackChanges public NavigationChain? Previous { get; set; } public NavigationChain? Front { get; set; } public IList? Removed { get; set; } + public IList NewNavigationChains { get; set; } = []; } diff --git a/src/AvaloniaInside.Shell/Navigator.cs b/src/AvaloniaInside.Shell/Navigator.cs index b43106e..d189083 100644 --- a/src/AvaloniaInside.Shell/Navigator.cs +++ b/src/AvaloniaInside.Shell/Navigator.cs @@ -12,7 +12,7 @@ public partial class Navigator : INavigator private readonly INavigateStrategy _navigateStrategy; private readonly INavigationUpdateStrategy _updateStrategy; private readonly INavigationViewLocator _viewLocator; - private readonly NavigationStack _stack = new(); + private readonly NavigationStack _stack; private readonly Dictionary> _waitingList = new(); private bool _navigating; @@ -36,6 +36,7 @@ public Navigator( _navigateStrategy = navigateStrategy; _updateStrategy = updateStrategy; _viewLocator = viewLocator; + _stack = new(viewLocator); _updateStrategy.HostItemChanged += UpdateStrategyOnHostItemChanged; } @@ -77,7 +78,6 @@ private async Task NotifyAsync( return; } - var instances = new List(); var finalNavigateType = !origin.AbsolutePath.Equals(newUri.AbsolutePath) && Registrar.TryGetNode(origin.AbsolutePath, out var originalNode) ? navigateType ?? originalNode.Navigate @@ -119,19 +119,16 @@ private async Task NotifyAsync( var stackChanges = _stack.Push( node, finalNavigateType, - newUri, - instanceFor => - { - var instance = _viewLocator.GetView(instanceFor); - SetShellToPage(instance); - instances.Add(instance); - return instance; - }); + newUri); + + foreach (var newChain in stackChanges.NewNavigationChains) + { + SetupPage(newChain); + } await _updateStrategy.UpdateChangesAsync( ShellView, stackChanges, - instances, finalNavigateType, argument, hasArgument, @@ -160,10 +157,12 @@ await _updateStrategy.UpdateChangesAsync( _navigating = false; } - private void SetShellToPage(object instance) + private void SetupPage(NavigationChain chain) { - if (instance is Page page) - page.Shell = ShellView; + if (chain.Instance is not Page page) return; + + page.Shell = ShellView; + page.Chain = chain; } private async Task SwitchHostedItem( diff --git a/src/AvaloniaInside.Shell/Page.cs b/src/AvaloniaInside.Shell/Page.cs index b1c28ca..aa01b4a 100644 --- a/src/AvaloniaInside.Shell/Page.cs +++ b/src/AvaloniaInside.Shell/Page.cs @@ -17,7 +17,7 @@ public class Page : UserControl, INavigationLifecycle, INavigatorLifecycle, INav private ContentPresenter? _navigationBarPlaceHolder; private NavigationBar? _navigationBar; - public NavigationBar? NavigationBar => FindNavigationBar(true) ?? Shell?.AttachedNavigationBar; + public NavigationBar? NavigationBar => Shell?.NavigationBar; public NavigationBar? AttachedNavigationBar => _navigationBar; @@ -27,51 +27,52 @@ public ShellView? Shell set => SetValue(ShellProperty, value); } - public INavigator? Navigator => Shell?.Navigator; - - protected override Type StyleKeyOverride => typeof(Page); - - public virtual Task AppearAsync(CancellationToken cancellationToken) => Task.CompletedTask; - public virtual Task ArgumentAsync(object args, CancellationToken cancellationToken) => Task.CompletedTask; - public virtual Task DisappearAsync(CancellationToken cancellationToken) => Task.CompletedTask; - public virtual Task InitialiseAsync(CancellationToken cancellationToken) => Task.CompletedTask; - public virtual Task TerminateAsync(CancellationToken cancellationToken) => Task.CompletedTask; - public virtual Task OnNavigateAsync(NaviagateEventArgs args, CancellationToken cancellationToken) => Task.CompletedTask; - public virtual Task OnNavigatingAsync(NaviagatingEventArgs args, CancellationToken cancellationToken) => Task.CompletedTask; - - protected override void OnApplyTemplate(TemplateAppliedEventArgs e) - { - base.OnApplyTemplate(e); - _navigationBarPlaceHolder = e.NameScope.Find("PART_NavigationBarPlaceHolder"); - _navigationBar = e.NameScope.Find("PART_NavigationBar"); - } - - protected override void OnLoaded(RoutedEventArgs e) - { - base.OnLoaded(e); - - if (_navigationBarPlaceHolder == null) return; - if (FindNavigationBar(true) != null) return; - - _navigationBarPlaceHolder.Content = _navigationBar = new NavigationBar() - { - ShellView = Shell - }; - } - - private NavigationBar? FindNavigationBar(bool includeSelf) - { - if (includeSelf && _navigationBar != null) return _navigationBar; - if (Shell?.AttachedNavigationBar is { } shellAttachedNavigationBar) return shellAttachedNavigationBar; - if (Navigator?.CurrentChain?.GetAscendingNodes() is not { } nodes) return null; - - foreach (var item in nodes) - { - if (!item.Hosted) return null; - if (item.Instance is INavigationBarProvider { AttachedNavigationBar: { } navigationBar }) - return navigationBar; - } - - return null; - } + public INavigator? Navigator => Shell?.Navigator; + + public NavigationChain Chain { get; internal set; } + + protected override Type StyleKeyOverride => typeof(Page); + + public virtual Task AppearAsync(CancellationToken cancellationToken) => Task.CompletedTask; + public virtual Task ArgumentAsync(object args, CancellationToken cancellationToken) => Task.CompletedTask; + public virtual Task DisappearAsync(CancellationToken cancellationToken) => Task.CompletedTask; + public virtual Task InitialiseAsync(CancellationToken cancellationToken) => Task.CompletedTask; + public virtual Task TerminateAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + public virtual Task OnNavigateAsync(NaviagateEventArgs args, CancellationToken cancellationToken) => + Task.CompletedTask; + + public virtual Task OnNavigatingAsync(NaviagatingEventArgs args, CancellationToken cancellationToken) => + Task.CompletedTask; + + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + _navigationBarPlaceHolder = e.NameScope.Find("PART_NavigationBarPlaceHolder"); + } + + protected override void OnLoaded(RoutedEventArgs e) + { + base.OnLoaded(e); + ApplyNavigationBar(); + AttachedNavigationBar?.UpdateView(this); + } + + private void ApplyNavigationBar() + { + if (_navigationBarPlaceHolder == null || _navigationBar != null) + return; + + if (Shell?.NavigationBarAttachType is not ({ } type and not NavigationBarAttachType.ToShell)) + return; + + if ((type == NavigationBarAttachType.ToLastPage && Chain is HostNavigationChain) || + (type == NavigationBarAttachType.ToFirstHostThenPage && Chain?.Back is HostNavigationChain)) + return; + + _navigationBarPlaceHolder.Content = _navigationBar = new NavigationBar + { + ShellView = Shell + }; + } } diff --git a/src/AvaloniaInside.Shell/Platform/PlatformSetup.cs b/src/AvaloniaInside.Shell/Platform/PlatformSetup.cs index 5b06317..947d534 100644 --- a/src/AvaloniaInside.Shell/Platform/PlatformSetup.cs +++ b/src/AvaloniaInside.Shell/Platform/PlatformSetup.cs @@ -65,4 +65,14 @@ public static IPageTransition TransitionForModal return DrillInNavigationTransition.Instance; } } + + public static NavigationBarAttachType NavigationBarAttachType + { + get + { + if (OperatingSystem.IsIOS()) + return NavigationBarAttachType.ToFirstHostThenPage; + return NavigationBarAttachType.ToShell; + } + } } diff --git a/src/AvaloniaInside.Shell/ShellView.cs b/src/AvaloniaInside.Shell/ShellView.cs index 06c8b79..f8cfd5c 100644 --- a/src/AvaloniaInside.Shell/ShellView.cs +++ b/src/AvaloniaInside.Shell/ShellView.cs @@ -1,10 +1,13 @@ using System; +using System.Linq; using System.Threading; using System.Threading.Tasks; using System.Windows.Input; using Avalonia; using Avalonia.Animation; using Avalonia.Controls; +using Avalonia.Controls.Metadata; +using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Interactivity; @@ -15,6 +18,11 @@ namespace AvaloniaInside.Shell; +[TemplatePart("PART_NavigationBarPlaceHolder", typeof(ContentPresenter))] +[TemplatePart("PART_ContentView", typeof(StackContentView))] +[TemplatePart("PART_SplitView", typeof(SplitView))] +[TemplatePart("PART_SideMenu", typeof(SideMenu))] +[TemplatePart("PART_Modal", typeof(StackContentView))] public partial class ShellView : TemplatedControl, INavigationBarProvider { #region Enums @@ -38,13 +46,12 @@ public enum SideMenuBehaveType #region Variables - private readonly bool _isMobile; - private SplitView? _splitView; private StackContentView? _contentView; private NavigationBar? _navigationBar; private StackContentView? _modalView; private SideMenu? _sideMenu; + private ContentPresenter? _navigationBarPlaceHolder; private bool _loadedFlag; private bool _topLevelEventFlag; @@ -53,9 +60,7 @@ public enum SideMenuBehaveType #region Properties - public NavigationBar? NavigationBar => - _navigationBar ?? - (Navigator.CurrentChain?.Instance as Page)?.NavigationBar; + public NavigationBar? NavigationBar => FindNavigationBar(); public NavigationBar? AttachedNavigationBar => _navigationBar; @@ -90,7 +95,7 @@ public ScreenSizeType ScreenSize #region DefaultRoute - public static DirectProperty DefaultRouteProperty = AvaloniaProperty + public static readonly DirectProperty DefaultRouteProperty = AvaloniaProperty .RegisterDirect( nameof(DefaultRoute), o => o.DefaultRoute, @@ -233,6 +238,27 @@ public IPageTransition? ModalPageTransition #endregion + #region NavigationBarAttachType + + /// + /// Defines the property. + /// + public static readonly StyledProperty NavigationBarAttachTypeProperty = + AvaloniaProperty.Register( + nameof(NavigationBarAttachType), + defaultValue: PlatformSetup.NavigationBarAttachType); + + /// + /// Gets or sets the type of attach navigation bar. + /// + public NavigationBarAttachType NavigationBarAttachType + { + get => GetValue(NavigationBarAttachTypeProperty); + set => SetValue(NavigationBarAttachTypeProperty, value); + } + + #endregion + #endregion #region Attached properties @@ -271,15 +297,15 @@ public static void SetEnableSafeAreaForBottom(AvaloniaObject element, bool param public ShellView() { - Navigator = Locator.Current + Navigator = Locator.Current .GetService() ?? throw new ArgumentException("Cannot find INavigationService"); Navigator.RegisterShell(this); BackCommand = ReactiveCommand.CreateFromTask(BackActionAsync); SideMenuCommand = ReactiveCommand.CreateFromTask(MenuActionAsync); - _isMobile = OperatingSystem.IsAndroid() || OperatingSystem.IsIOS(); - if (!_isMobile) + var isMobile = OperatingSystem.IsAndroid() || OperatingSystem.IsIOS(); + if (!isMobile) { Classes.Add("Mobile"); _sideMenuPresented = true; @@ -322,9 +348,9 @@ protected override void OnApplyTemplate(TemplateAppliedEventArgs e) _contentView = e.NameScope.Find("PART_ContentView"); _modalView = e.NameScope.Find("PART_Modal"); _sideMenu = e.NameScope.Find("PART_SideMenu"); + _navigationBarPlaceHolder = e.NameScope.Find("PART_NavigationBarPlaceHolder"); SetupUi(); - } protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) @@ -347,6 +373,7 @@ private void SetupUi() { OnSafeEdgeSetup(); UpdateSideMenu(); + SetupNavigationBar(); if (_navigationBar is { } navigationBar) { @@ -380,12 +407,34 @@ protected virtual void OnSafeEdgeSetup() }); } + private void SetupNavigationBar() + { + if (_navigationBarPlaceHolder != null && + NavigationBarAttachType == NavigationBarAttachType.ToShell && + _navigationBar == null) + { + _navigationBarPlaceHolder.Content = _navigationBar = new NavigationBar + { + ShellView = this + }; + } + } + #endregion #region Services and navigation public INavigator Navigator { get; } + private NavigationBar? FindNavigationBar() + { + if (NavigationBarAttachType == NavigationBarAttachType.ToShell) return _navigationBar; + return Navigator.CurrentChain?.GetAscendingNodes() + .Select(s => s.Instance) + .OfType() + .FirstOrDefault(f => f.AttachedNavigationBar != null)?.AttachedNavigationBar; + } + #endregion #region View Stack Manager @@ -396,6 +445,7 @@ public async Task PushViewAsync( CancellationToken cancellationToken = default) { await (_contentView?.PushViewAsync(view, navigateType, cancellationToken) ?? Task.CompletedTask); + AttachedNavigationBar?.UpdateView(Navigator.CurrentChain?.Instance); SelectSideMenuItem(); UpdateBindings(); UpdateSideMenu(); diff --git a/src/AvaloniaInside.Shell/TabPage.cs b/src/AvaloniaInside.Shell/TabPage.cs new file mode 100644 index 0000000..4b12b3a --- /dev/null +++ b/src/AvaloniaInside.Shell/TabPage.cs @@ -0,0 +1,235 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Metadata; +using Avalonia.Controls.Presenters; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Templates; +using Avalonia.Data; +using Avalonia.Layout; + +namespace AvaloniaInside.Shell; + +[TemplatePart("PART_TabStripPlaceHolder", typeof(ContentPresenter))] +[TemplatePart("PART_Carousel", typeof(Carousel))] +public class TabPage : Page, ISelectableHostedItems +{ + public event EventHandler? SelectionChanged; + + private ContentPresenter? _tabStripPlaceHolder; + + protected override Type StyleKeyOverride => typeof(TabPage); + + public static readonly StyledProperty TabStripTemplateProperty = + AvaloniaProperty.Register(nameof(TabStripTemplate)); + + public IControlTemplate? TabStripTemplate + { + get => GetValue(TabStripTemplateProperty); + set => SetValue(TabStripTemplateProperty, value); + } + + #region ItemTemplate + + public static readonly StyledProperty ItemTemplateProperty = + AvaloniaProperty.Register(nameof(ItemTemplate)); + + public IDataTemplate? ItemTemplate + { + get => GetValue(ItemTemplateProperty); + set => SetValue(ItemTemplateProperty, value); + } + + #endregion + + #region ItemsSource + + public static readonly StyledProperty ItemsSourceProperty = + AvaloniaProperty.Register(nameof(ItemsSource)); + + public IEnumerable? ItemsSource + { + get => GetValue(ItemsSourceProperty); + set => SetValue(ItemsSourceProperty, value); + } + +#pragma warning disable CS8603 // Possible null reference return. + public ItemCollection Items => null; +#pragma warning restore CS8603 // Possible null reference return. + + #endregion + + #region SelectedIndex + + public static readonly DirectProperty SelectedIndexProperty = + AvaloniaProperty.RegisterDirect( + nameof(SelectedIndex), + o => o.SelectedIndex, + (o, v) => o.SelectedIndex = v, + unsetValue: -1, + defaultBindingMode: BindingMode.TwoWay); + + private int _selectedIndex; + + public int SelectedIndex + { + get => _selectedIndex; + set + { + if (ItemsSource is not { } itemsSource) return; + + if (itemsSource.Cast().Skip(value).FirstOrDefault() is not { } found) + throw new IndexOutOfRangeException($"{value} out of index"); + + if (SetAndRaise(SelectedIndexProperty, ref _selectedIndex, value)) + { + SelectedItem = found; + } + } + } + + #endregion + + #region SelectedItem + + public static readonly DirectProperty SelectedItemProperty = + AvaloniaProperty.RegisterDirect( + nameof(SelectedItem), + o => o.SelectedItem, + (o, v) => o.SelectedItem = v, + defaultBindingMode: BindingMode.TwoWay, enableDataValidation: true); + + private object? _selectedItem; + + public object? SelectedItem + { + get => _selectedItem; + set + { + var current = _selectedItem; + if (SetAndRaise(SelectedItemProperty, ref _selectedItem, value)) + { + SelectionChanged?.Invoke( + this, + new SelectionChangedEventArgs( + null, + current == null ? [] : new List { current }, + value == null ? [] : new List { value })); + + AttachedNavigationBar?.UpdateView(Navigator?.CurrentChain?.Instance); + } + } + } + + #endregion + + #region SelectedContentTemplate + + public static readonly DirectProperty SelectedContentTemplateProperty = + AvaloniaProperty.RegisterDirect( + nameof(SelectedContentTemplate), + o => o.SelectedContentTemplate, + (o, v) => o.SelectedContentTemplate = v); + + private IDataTemplate? _selectedContentTemplate; + + public IDataTemplate? SelectedContentTemplate + { + get => _selectedContentTemplate; + set => SetAndRaise(SelectedContentTemplateProperty, ref _selectedContentTemplate, value); + } + + #endregion + + #region TabStripPlacement + + /// + /// Defines the property. + /// + public static readonly StyledProperty TabStripPlacementProperty = + AvaloniaProperty.Register(nameof(TabStripPlacement), defaultValue: Dock.Bottom); + + /// + /// Gets or sets the tabstrip placement of the TabControl. + /// + public Dock TabStripPlacement + { + get => GetValue(TabStripPlacementProperty); + set => SetValue(TabStripPlacementProperty, value); + } + + #endregion + + #region ItemsPanel + + private static readonly FuncTemplate DefaultPanel = + new(() => new StackPanel + { + HorizontalAlignment = HorizontalAlignment.Center, + Orientation = Orientation.Horizontal + }); + + /// + /// Defines the property. + /// + public static readonly StyledProperty> ItemsPanelProperty = + AvaloniaProperty.Register>(nameof(ItemsPanel), DefaultPanel); + + /// + /// Gets or sets the panel used to display the items. + /// + public ITemplate ItemsPanel + { + get => GetValue(ItemsPanelProperty); + set => SetValue(ItemsPanelProperty, value); + } + + #endregion + + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + _tabStripPlaceHolder = e.NameScope.Find("PART_TabStripPlaceHolder"); + + ApplyTabStripTemplate(); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + if (change.Property == TabStripTemplateProperty) + { + ApplyTabStripTemplate(); + } + } + + protected virtual void ApplyTabStripTemplate() + { + if (TabStripTemplate is not { } template || _tabStripPlaceHolder is not { } tabStripPlaceHolder) return; + + //Logger.TryGet(LogEventLevel.Verbose, LogArea.Control)?.Log(this, "Creating control template"); + + if (template.Build(this) is { } templateResult) + { + var (child, nameScope) = templateResult; + ((ISetLogicalParent)child).SetParent(this); + + //HACK using + SetPrivateDateTimePropertyValue(child, "TemplatedParent", this); + child.ApplyTemplate(); + + tabStripPlaceHolder.Content = child; + } + } + + static void SetPrivateDateTimePropertyValue(T member, string propName, object newValue) + { + PropertyInfo propertyInfo = typeof(T).GetProperty(propName); + if (propertyInfo == null) return; + propertyInfo.SetValue(member, newValue); + } +} diff --git a/src/AvaloniaInside.Shell/Theme/Default/Controls.axaml b/src/AvaloniaInside.Shell/Theme/Default/Controls.axaml index 3664cd8..4d20726 100644 --- a/src/AvaloniaInside.Shell/Theme/Default/Controls.axaml +++ b/src/AvaloniaInside.Shell/Theme/Default/Controls.axaml @@ -7,7 +7,7 @@ - + diff --git a/src/AvaloniaInside.Shell/Theme/Default/NavigationBar.axaml b/src/AvaloniaInside.Shell/Theme/Default/NavigationBar.axaml index d0819cf..5afe910 100644 --- a/src/AvaloniaInside.Shell/Theme/Default/NavigationBar.axaml +++ b/src/AvaloniaInside.Shell/Theme/Default/NavigationBar.axaml @@ -29,6 +29,8 @@ + + - - - - - - - - - - - diff --git a/src/Example/ShellExample/ShellExample/Views/HomePage.axaml b/src/Example/ShellExample/ShellExample/Views/HomePage.axaml index a1211b1..34b5bf9 100644 --- a/src/Example/ShellExample/ShellExample/Views/HomePage.axaml +++ b/src/Example/ShellExample/ShellExample/Views/HomePage.axaml @@ -2,9 +2,13 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:i="https://github.com/projektanker/icons.avalonia" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="ShellExample.Views.HomePage" NavigationBar.Header="Home"> + + + diff --git a/src/Example/ShellExample/ShellExample/Views/HomePage.axaml.cs b/src/Example/ShellExample/ShellExample/Views/HomePage.axaml.cs index 0b8d2d1..49b89f5 100644 --- a/src/Example/ShellExample/ShellExample/Views/HomePage.axaml.cs +++ b/src/Example/ShellExample/ShellExample/Views/HomePage.axaml.cs @@ -22,6 +22,4 @@ public override Task InitialiseAsync(CancellationToken cancellationToken) DataContext = new ViewModels.HomePageViewModel(Navigator); return Task.CompletedTask; } - - public string Icon => "fa-solid fa-house"; } diff --git a/src/Example/ShellExample/ShellExample/Views/MainView.axaml b/src/Example/ShellExample/ShellExample/Views/MainView.axaml index d2a6560..5b4e2f5 100644 --- a/src/Example/ShellExample/ShellExample/Views/MainView.axaml +++ b/src/Example/ShellExample/ShellExample/Views/MainView.axaml @@ -30,11 +30,13 @@ LargeScreenSideMenuMode="Inline" LargeScreenSideMenuBehave="Keep" + NavigationBarAttachType="ToFirstHostThenPage" + DefaultPageTransition="{Binding CurrentTransition}"> - + diff --git a/src/Example/ShellExample/ShellExample/Views/PetsTabControlView.axaml b/src/Example/ShellExample/ShellExample/Views/PetsTabControlView.axaml index e0f1804..0eac52d 100644 --- a/src/Example/ShellExample/ShellExample/Views/PetsTabControlView.axaml +++ b/src/Example/ShellExample/ShellExample/Views/PetsTabControlView.axaml @@ -1,31 +1,23 @@ - - + + + + + - - - - + + - - - - - - - + + diff --git a/src/Example/ShellExample/ShellExample/Views/PetsTabControlView.axaml.cs b/src/Example/ShellExample/ShellExample/Views/PetsTabControlView.axaml.cs index fc00ab8..32e362d 100644 --- a/src/Example/ShellExample/ShellExample/Views/PetsTabControlView.axaml.cs +++ b/src/Example/ShellExample/ShellExample/Views/PetsTabControlView.axaml.cs @@ -1,13 +1,12 @@ using System; using Avalonia.Controls; using Avalonia.Markup.Xaml; +using AvaloniaInside.Shell; namespace ShellExample.Views; -public partial class PetsTabControlView : TabControl +public partial class PetsTabControlView : TabPage { - protected override Type StyleKeyOverride => typeof(TabControl); - public PetsTabControlView() { InitializeComponent(); @@ -17,7 +16,5 @@ private void InitializeComponent() { AvaloniaXamlLoader.Load(this); } - - public string Icon => "fa-solid fa-paw"; } diff --git a/src/Example/ShellExample/ShellExample/Views/ProfileView.axaml b/src/Example/ShellExample/ShellExample/Views/ProfileView.axaml index eac5909..154e6f7 100644 --- a/src/Example/ShellExample/ShellExample/Views/ProfileView.axaml +++ b/src/Example/ShellExample/ShellExample/Views/ProfileView.axaml @@ -2,9 +2,14 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:i="https://github.com/projektanker/icons.avalonia" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="ShellExample.Views.ProfileView" NavigationBar.Header="Profile"> + + + + Welcome to Avalonia! diff --git a/src/Example/ShellExample/ShellExample/Views/ProfileView.axaml.cs b/src/Example/ShellExample/ShellExample/Views/ProfileView.axaml.cs index 13238da..bd8afbe 100644 --- a/src/Example/ShellExample/ShellExample/Views/ProfileView.axaml.cs +++ b/src/Example/ShellExample/ShellExample/Views/ProfileView.axaml.cs @@ -14,7 +14,5 @@ private void InitializeComponent() { AvaloniaXamlLoader.Load(this); } - - public string Icon => "fa-solid fa-user"; } diff --git a/src/Example/ShellExample/ShellExample/Views/SettingView.axaml b/src/Example/ShellExample/ShellExample/Views/SettingView.axaml index 426dd3c..47a4324 100644 --- a/src/Example/ShellExample/ShellExample/Views/SettingView.axaml +++ b/src/Example/ShellExample/ShellExample/Views/SettingView.axaml @@ -2,12 +2,17 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:i="https://github.com/projektanker/icons.avalonia" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="ShellExample.Views.SettingView" NavigationBar.Header="Settings"> + + + + - diff --git a/src/Example/ShellExample/ShellExample/Views/SettingView.axaml.cs b/src/Example/ShellExample/ShellExample/Views/SettingView.axaml.cs index f78743a..3bf3e23 100644 --- a/src/Example/ShellExample/ShellExample/Views/SettingView.axaml.cs +++ b/src/Example/ShellExample/ShellExample/Views/SettingView.axaml.cs @@ -27,7 +27,5 @@ private void InitializeComponent() { AvaloniaXamlLoader.Load(this); } - - public string Icon => "fa-solid fa-user"; } diff --git a/src/Example/ShellExample/ShellExample/Views/ShopViews/ProductCatalogView.axaml b/src/Example/ShellExample/ShellExample/Views/ShopViews/ProductCatalogView.axaml index 9b65e52..545ecc2 100644 --- a/src/Example/ShellExample/ShellExample/Views/ShopViews/ProductCatalogView.axaml +++ b/src/Example/ShellExample/ShellExample/Views/ShopViews/ProductCatalogView.axaml @@ -3,12 +3,16 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:converters="clr-namespace:ShellExample.Converters" + xmlns:i="https://github.com/projektanker/icons.avalonia" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="ShellExample.Views.ShopViews.ProductCatalogView" NavigationBar.Header="{Binding Title}"> + + +