From ff980aeba2d23eb52a26c8d946c6854aad4275fc Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 18 Mar 2026 14:39:19 +0500 Subject: [PATCH 01/14] Restructure VisualLayerManager (#20905) * refactor: Replace IsPopup with Enable*Layer properties on VisualLayerManager - Remove IsPopup from VisualLayerManager, add granular Enable*Layer properties: EnableAdornerLayer (default true), EnableOverlayLayer (default false), EnablePopupOverlayLayer (internal, default false), EnableTextSelectorLayer (default false) - Add PART_VisualLayerManager template part to TopLevel with protected property - Window and EmbeddableControlRoot override OnApplyTemplate to enable overlay, popup overlay, and text selector layers - OverlayLayer is now wrapped in a Panel with a dedicated AdornerLayer sibling - AdornerLayer.GetAdornerLayer checks for OverlayLayer's dedicated AdornerLayer - Update all 8 XAML templates (both themes) to name PART_VisualLayerManager and remove IsPopup="True" from PopupRoot/OverlayPopupHost Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add XML doc to VisualLayerManager * Also search for AdornerLayer from TopLevel --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Julien Lebosquain --- api/Avalonia.nupkg.xml | 24 ++++++++ .../Embedding/EmbeddableControlRoot.cs | 7 +++ .../Primitives/AdornerLayer.cs | 28 ++++++++- .../Primitives/OverlayLayer.cs | 5 ++ .../Primitives/VisualLayerManager.cs | 58 +++++++++++++++---- src/Avalonia.Controls/TopLevel.cs | 16 +++++ src/Avalonia.Controls/Window.cs | 7 +++ .../Controls/EmbeddableControlRoot.xaml | 2 +- .../Controls/OverlayPopupHost.xaml | 2 +- .../Controls/PopupRoot.xaml | 2 +- .../Controls/Window.xaml | 2 +- .../Controls/EmbeddableControlRoot.xaml | 2 +- .../Controls/OverlayPopupHost.xaml | 2 +- .../Controls/PopupRoot.xaml | 2 +- .../Controls/Window.xaml | 2 +- .../Primitives/VisualLayerManagerTests.cs | 37 ++++++++++++ 16 files changed, 178 insertions(+), 20 deletions(-) create mode 100644 tests/Avalonia.Controls.UnitTests/Primitives/VisualLayerManagerTests.cs diff --git a/api/Avalonia.nupkg.xml b/api/Avalonia.nupkg.xml index 8e6173a6cb..63e8234919 100644 --- a/api/Avalonia.nupkg.xml +++ b/api/Avalonia.nupkg.xml @@ -2269,6 +2269,12 @@ baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + CP0002 + M:Avalonia.Controls.Primitives.VisualLayerManager.get_IsPopup + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + CP0002 M:Avalonia.Controls.Primitives.VisualLayerManager.get_LightDismissOverlayLayer @@ -2287,6 +2293,12 @@ baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + CP0002 + M:Avalonia.Controls.Primitives.VisualLayerManager.set_IsPopup(System.Boolean) + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + CP0002 M:Avalonia.Controls.Screens.ScreenFromWindow(Avalonia.Platform.IWindowBaseImpl) @@ -3871,6 +3883,12 @@ baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + CP0002 + M:Avalonia.Controls.Primitives.VisualLayerManager.get_IsPopup + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + CP0002 M:Avalonia.Controls.Primitives.VisualLayerManager.get_LightDismissOverlayLayer @@ -3889,6 +3907,12 @@ baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + CP0002 + M:Avalonia.Controls.Primitives.VisualLayerManager.set_IsPopup(System.Boolean) + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + CP0002 M:Avalonia.Controls.Screens.ScreenFromWindow(Avalonia.Platform.IWindowBaseImpl) diff --git a/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs b/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs index d4e5488019..59718c3e3f 100644 --- a/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs +++ b/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs @@ -4,6 +4,7 @@ using Avalonia.Automation.Peers; using Avalonia.Controls.Automation; using Avalonia.Controls.Automation.Peers; using Avalonia.Controls.Platform; +using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Platform; @@ -54,6 +55,12 @@ namespace Avalonia.Controls.Embedding protected override Type StyleKeyOverride => typeof(EmbeddableControlRoot); + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + EnableVisualLayerManagerLayers(); + } + protected override AutomationPeer OnCreateAutomationPeer() { return new EmbeddableControlRootAutomationPeer(this); diff --git a/src/Avalonia.Controls/Primitives/AdornerLayer.cs b/src/Avalonia.Controls/Primitives/AdornerLayer.cs index 8d3d97b94f..1c8b24f627 100644 --- a/src/Avalonia.Controls/Primitives/AdornerLayer.cs +++ b/src/Avalonia.Controls/Primitives/AdornerLayer.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Specialized; +using System.Linq; using Avalonia.Input.TextInput; using Avalonia.Media; using Avalonia.Reactive; @@ -71,7 +72,32 @@ namespace Avalonia.Controls.Primitives public static AdornerLayer? GetAdornerLayer(Visual visual) { - return visual.FindAncestorOfType()?.AdornerLayer; + // Check if the visual is inside an OverlayLayer with a dedicated AdornerLayer + foreach (var ancestor in visual.GetVisualAncestors()) + { + if (GetDirectAdornerLayer(ancestor) is { } adornerLayer) + return adornerLayer; + } + + if (TopLevel.GetTopLevel(visual) is { } topLevel) + { + foreach (var descendant in topLevel.GetVisualDescendants()) + { + if (GetDirectAdornerLayer(descendant) is { } adornerLayer) + return adornerLayer; + } + } + + return null; + + static AdornerLayer? GetDirectAdornerLayer(Visual visual) + { + if (visual is OverlayLayer { AdornerLayer: { } adornerLayer }) + return adornerLayer; + if (visual is VisualLayerManager vlm) + return vlm.AdornerLayer; + return null; + } } public static bool GetIsClipEnabled(Visual adorner) diff --git a/src/Avalonia.Controls/Primitives/OverlayLayer.cs b/src/Avalonia.Controls/Primitives/OverlayLayer.cs index 3337288a13..a9d9b072f2 100644 --- a/src/Avalonia.Controls/Primitives/OverlayLayer.cs +++ b/src/Avalonia.Controls/Primitives/OverlayLayer.cs @@ -13,6 +13,11 @@ namespace Avalonia.Controls.Primitives public Size AvailableSize { get; private set; } + /// + /// Gets the dedicated adorner layer for this overlay layer. + /// + internal AdornerLayer? AdornerLayer { get; set; } + internal OverlayLayer() { } diff --git a/src/Avalonia.Controls/Primitives/VisualLayerManager.cs b/src/Avalonia.Controls/Primitives/VisualLayerManager.cs index 6630f1e09c..eb912d2cf8 100644 --- a/src/Avalonia.Controls/Primitives/VisualLayerManager.cs +++ b/src/Avalonia.Controls/Primitives/VisualLayerManager.cs @@ -3,6 +3,9 @@ using Avalonia.LogicalTree; namespace Avalonia.Controls.Primitives { + /// + /// A control that manages multiple layers such as adorners, overlays, text selectors, and popups. + /// public sealed class VisualLayerManager : Decorator { private const int AdornerZIndex = int.MaxValue - 100; @@ -13,13 +16,37 @@ namespace Avalonia.Controls.Primitives private ILogicalRoot? _logicalRoot; private readonly List _layers = new(); - - public bool IsPopup { get; set; } - - internal AdornerLayer AdornerLayer + private OverlayLayer? _overlayLayer; + + /// + /// Gets or sets a value indicating whether an is + /// created for this . When enabled, the adorner layer is added to the + /// visual tree, providing a dedicated layer for rendering adorners. + /// + public bool EnableAdornerLayer { get; set; } = true; + + /// + /// Gets or sets a value indicating whether an is + /// created for this . When enabled, the overlay layer is added to the + /// visual tree, providing a dedicated layer for rendering overlay visuals. + /// + public bool EnableOverlayLayer { get; set; } + + internal bool EnablePopupOverlayLayer { get; set; } + + /// + /// Gets or sets a value indicating whether a is + /// created for this . When enabled, the overlay layer is added to the + /// visual tree, providing a dedicated layer for rendering text selection handles. + /// + public bool EnableTextSelectorLayer { get; set; } + + internal AdornerLayer? AdornerLayer { get { + if (!EnableAdornerLayer) + return null; var rv = FindLayer(); if (rv == null) AddLayer(rv = new AdornerLayer(), AdornerZIndex); @@ -31,7 +58,7 @@ namespace Avalonia.Controls.Primitives { get { - if (IsPopup) + if (!EnablePopupOverlayLayer) return null; var rv = FindLayer(); if (rv == null) @@ -44,12 +71,21 @@ namespace Avalonia.Controls.Primitives { get { - if (IsPopup) + if (!EnableOverlayLayer) return null; - var rv = FindLayer(); - if (rv == null) - AddLayer(rv = new OverlayLayer(), OverlayZIndex); - return rv; + if (_overlayLayer == null) + { + _overlayLayer = new OverlayLayer(); + var adorner = new AdornerLayer(); + _overlayLayer.AdornerLayer = adorner; + + var panel = new Panel(); + panel.Children.Add(_overlayLayer); + panel.Children.Add(adorner); + + AddLayer(panel, OverlayZIndex); + } + return _overlayLayer; } } @@ -57,7 +93,7 @@ namespace Avalonia.Controls.Primitives { get { - if (IsPopup) + if (!EnableTextSelectorLayer) return null; var rv = FindLayer(); if (rv == null) diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 8556d03d91..ceb9590564 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -38,6 +38,7 @@ namespace Avalonia.Controls /// tracking the widget's . /// [TemplatePart("PART_TransparencyFallback", typeof(Border))] + [TemplatePart("PART_VisualLayerManager", typeof(VisualLayerManager))] public abstract class TopLevel : ContentControl, ICloseable, IStyleHost, @@ -125,6 +126,7 @@ namespace Avalonia.Controls private Size? _frameSize; private WindowTransparencyLevel _actualTransparencyLevel; private Border? _transparencyFallbackBorder; + private VisualLayerManager? _visualLayerManager; private TargetWeakEventSubscriber? _resourcesChangesSubscriber; private IStorageProvider? _storageProvider; private Screens? _screens; @@ -133,6 +135,18 @@ namespace Avalonia.Controls internal TopLevelHost TopLevelHost => _topLevelHost; internal new PresentationSource PresentationSource => _source; internal IInputRoot InputRoot => _source; + + private protected VisualLayerManager? VisualLayerManager => _visualLayerManager; + + private protected void EnableVisualLayerManagerLayers() + { + if (_visualLayerManager is { } vlm) + { + vlm.EnableOverlayLayer = true; + vlm.EnablePopupOverlayLayer = true; + vlm.EnableTextSelectorLayer = true; + } + } /// /// Initializes static members of the class. @@ -723,6 +737,8 @@ namespace Avalonia.Controls { base.OnApplyTemplate(e); + _visualLayerManager = e.NameScope.Find("PART_VisualLayerManager"); + if (PlatformImpl is null) return; diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index db3ec6a077..e7a4ce953e 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Avalonia.Automation.Peers; using Avalonia.Controls.Chrome; using Avalonia.Controls.Platform; +using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Layout; @@ -795,6 +796,12 @@ namespace Avalonia.Controls ShowCore(null, false); } + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + EnableVisualLayerManagerLayers(); + } + protected override void IsVisibleChanged(AvaloniaPropertyChangedEventArgs e) { if (!IgnoreVisibilityChanges) diff --git a/src/Avalonia.Themes.Fluent/Controls/EmbeddableControlRoot.xaml b/src/Avalonia.Themes.Fluent/Controls/EmbeddableControlRoot.xaml index 1fc931db36..2ea83ec6a9 100644 --- a/src/Avalonia.Themes.Fluent/Controls/EmbeddableControlRoot.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/EmbeddableControlRoot.xaml @@ -12,7 +12,7 @@ - + - + - + - + - + - + - + - + Date: Wed, 18 Mar 2026 14:13:52 +0100 Subject: [PATCH 02/14] Added more Carousel samples (#20932) * Added all the Carousel samples * Fixes * Updated sample * More changes --- samples/ControlCatalog/MainView.xaml | 6 +- .../Pages/CarouselDemoPage.xaml | 11 + .../Pages/CarouselDemoPage.xaml.cs | 53 ++ .../CarouselCustomizationPage.xaml | 119 ++++ .../CarouselCustomizationPage.xaml.cs | 48 ++ .../CarouselPage/CarouselDataBindingPage.xaml | 60 ++ .../CarouselDataBindingPage.xaml.cs | 95 +++ .../CarouselPage/CarouselGalleryAppPage.xaml | 557 ++++++++++++++++++ .../CarouselGalleryAppPage.xaml.cs | 101 ++++ .../CarouselPage/CarouselGesturesPage.xaml | 93 +++ .../CarouselPage/CarouselGesturesPage.xaml.cs | 59 ++ .../CarouselGettingStartedPage.xaml | 74 +++ .../CarouselGettingStartedPage.xaml.cs | 40 ++ .../CarouselPage/CarouselMultiItemPage.xaml | 140 +++++ .../CarouselMultiItemPage.xaml.cs | 47 ++ .../CarouselPage/CarouselTransitionsPage.xaml | 97 +++ .../CarouselTransitionsPage.xaml.cs | 66 +++ .../CarouselPage/CarouselVerticalPage.xaml | 132 +++++ .../CarouselPage/CarouselVerticalPage.xaml.cs | 39 ++ .../VirtualizingCarouselPanel.cs | 14 +- 20 files changed, 1848 insertions(+), 3 deletions(-) create mode 100644 samples/ControlCatalog/Pages/CarouselDemoPage.xaml create mode 100644 samples/ControlCatalog/Pages/CarouselDemoPage.xaml.cs create mode 100644 samples/ControlCatalog/Pages/CarouselPage/CarouselCustomizationPage.xaml create mode 100644 samples/ControlCatalog/Pages/CarouselPage/CarouselCustomizationPage.xaml.cs create mode 100644 samples/ControlCatalog/Pages/CarouselPage/CarouselDataBindingPage.xaml create mode 100644 samples/ControlCatalog/Pages/CarouselPage/CarouselDataBindingPage.xaml.cs create mode 100644 samples/ControlCatalog/Pages/CarouselPage/CarouselGalleryAppPage.xaml create mode 100644 samples/ControlCatalog/Pages/CarouselPage/CarouselGalleryAppPage.xaml.cs create mode 100644 samples/ControlCatalog/Pages/CarouselPage/CarouselGesturesPage.xaml create mode 100644 samples/ControlCatalog/Pages/CarouselPage/CarouselGesturesPage.xaml.cs create mode 100644 samples/ControlCatalog/Pages/CarouselPage/CarouselGettingStartedPage.xaml create mode 100644 samples/ControlCatalog/Pages/CarouselPage/CarouselGettingStartedPage.xaml.cs create mode 100644 samples/ControlCatalog/Pages/CarouselPage/CarouselMultiItemPage.xaml create mode 100644 samples/ControlCatalog/Pages/CarouselPage/CarouselMultiItemPage.xaml.cs create mode 100644 samples/ControlCatalog/Pages/CarouselPage/CarouselTransitionsPage.xaml create mode 100644 samples/ControlCatalog/Pages/CarouselPage/CarouselTransitionsPage.xaml.cs create mode 100644 samples/ControlCatalog/Pages/CarouselPage/CarouselVerticalPage.xaml create mode 100644 samples/ControlCatalog/Pages/CarouselPage/CarouselVerticalPage.xaml.cs diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index b6249fe17f..2a0de7a114 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -54,8 +54,10 @@ ScrollViewer.VerticalScrollBarVisibility="Disabled"> - - + + diff --git a/samples/ControlCatalog/Pages/CarouselDemoPage.xaml b/samples/ControlCatalog/Pages/CarouselDemoPage.xaml new file mode 100644 index 0000000000..df4317fcad --- /dev/null +++ b/samples/ControlCatalog/Pages/CarouselDemoPage.xaml @@ -0,0 +1,11 @@ + + + + + + + diff --git a/samples/ControlCatalog/Pages/CarouselDemoPage.xaml.cs b/samples/ControlCatalog/Pages/CarouselDemoPage.xaml.cs new file mode 100644 index 0000000000..64753b9fc4 --- /dev/null +++ b/samples/ControlCatalog/Pages/CarouselDemoPage.xaml.cs @@ -0,0 +1,53 @@ +using System; +using Avalonia.Controls; +using Avalonia.Interactivity; + +namespace ControlCatalog.Pages +{ + public partial class CarouselDemoPage : UserControl + { + private static readonly (string Group, string Title, string Description, Func Factory)[] Demos = + { + // Overview + ("Overview", "Getting Started", + "Basic Carousel with image items and previous/next navigation buttons.", + () => new CarouselGettingStartedPage()), + + // Features + ("Features", "Transitions", + "Configure page transitions: PageSlide, CrossFade, 3D Rotation, or None.", + () => new CarouselTransitionsPage()), + ("Features", "Customization", + "Adjust orientation and transition type to tailor the carousel layout.", + () => new CarouselCustomizationPage()), + ("Features", "Gestures & Keyboard", + "Navigate items via swipe gesture and arrow keys. Toggle each input mode on and off.", + () => new CarouselGesturesPage()), + ("Features", "Vertical Orientation", + "Carousel with Orientation set to Vertical, navigated with Up/Down keys, swipe, or buttons.", + () => new CarouselVerticalPage()), + ("Features", "Multi-Item Peek", + "Adjust ViewportFraction to show multiple items simultaneously with adjacent cards peeking.", + () => new CarouselMultiItemPage()), + ("Features", "Data Binding", + "Bind Carousel to an ObservableCollection and add, remove, or shuffle items at runtime.", + () => new CarouselDataBindingPage()), + + // Showcases + ("Showcases", "Curated Gallery", + "Editorial art gallery app with DrawerPage navigation, hero Carousel with PipsPager dots, and a horizontal peek carousel for collection highlights.", + () => new CarouselGalleryAppPage()), + }; + + public CarouselDemoPage() + { + InitializeComponent(); + Loaded += OnLoaded; + } + + private async void OnLoaded(object? sender, RoutedEventArgs e) + { + await SampleNav.PushAsync(NavigationDemoHelper.CreateGalleryHomePage(SampleNav, Demos), null); + } + } +} diff --git a/samples/ControlCatalog/Pages/CarouselPage/CarouselCustomizationPage.xaml b/samples/ControlCatalog/Pages/CarouselPage/CarouselCustomizationPage.xaml new file mode 100644 index 0000000000..add442e7a1 --- /dev/null +++ b/samples/ControlCatalog/Pages/CarouselPage/CarouselCustomizationPage.xaml @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + M12 10.9c-.61 0-1.1.49-1.1 1.1s.49 1.1 1.1 1.1c.61 0 1.1-.49 1.1-1.1s-.49-1.1-1.1-1.1zM12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm2.19 12.19L6 18l3.81-8.19L18 6l-3.81 8.19z + + + + + + + + + + + + M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z + + + + + + + + + + + + M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/CarouselPage/SanctuaryMainPage.xaml.cs b/samples/ControlCatalog/Pages/CarouselPage/SanctuaryMainPage.xaml.cs new file mode 100644 index 0000000000..05a4097a46 --- /dev/null +++ b/samples/ControlCatalog/Pages/CarouselPage/SanctuaryMainPage.xaml.cs @@ -0,0 +1,11 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages; + +public partial class SanctuaryMainPage : UserControl +{ + public SanctuaryMainPage() + { + InitializeComponent(); + } +} diff --git a/samples/ControlCatalog/Pages/CarouselPage/SanctuaryShowcasePage.xaml b/samples/ControlCatalog/Pages/CarouselPage/SanctuaryShowcasePage.xaml new file mode 100644 index 0000000000..50864e5e57 --- /dev/null +++ b/samples/ControlCatalog/Pages/CarouselPage/SanctuaryShowcasePage.xaml @@ -0,0 +1,335 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/CarouselPage/SanctuaryShowcasePage.xaml.cs b/samples/ControlCatalog/Pages/CarouselPage/SanctuaryShowcasePage.xaml.cs new file mode 100644 index 0000000000..be91370691 --- /dev/null +++ b/samples/ControlCatalog/Pages/CarouselPage/SanctuaryShowcasePage.xaml.cs @@ -0,0 +1,70 @@ +using System.Linq; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Layout; +using Avalonia.Media; +using Avalonia.VisualTree; + +namespace ControlCatalog.Pages; + +public partial class SanctuaryShowcasePage : UserControl +{ + public SanctuaryShowcasePage() + { + InitializeComponent(); + } + + private void OnPage1CTA(object? sender, RoutedEventArgs e) + { + DemoCarousel.SelectedIndex = 1; + } + + private void OnPage2CTA(object? sender, RoutedEventArgs e) + { + DemoCarousel.SelectedIndex = 2; + } + + private async void OnPage3CTA(object? sender, RoutedEventArgs e) + { + var nav = this.FindAncestorOfType(); + if (nav == null) + return; + + var carouselWrapper = nav.NavigationStack.LastOrDefault(); + + var headerGrid = new Grid { ColumnDefinitions = new ColumnDefinitions("*, Auto") }; + headerGrid.Children.Add(new TextBlock + { + Text = "Sanctuary", + VerticalAlignment = VerticalAlignment.Center + }); + var closeIcon = Geometry.Parse( + "M4.397 4.397a1 1 0 0 1 1.414 0L12 10.585l6.19-6.188a1 1 0 0 1 1.414 1.414L13.413 12l6.19 6.189a1 1 0 0 1-1.414 1.414L12 13.413l-6.189 6.19a1 1 0 0 1-1.414-1.414L10.585 12 4.397 5.811a1 1 0 0 1 0-1.414z"); + var closeBtn = new Button + { + Content = new PathIcon { Data = closeIcon }, + Background = Brushes.Transparent, + BorderThickness = new Thickness(0), + Padding = new Thickness(8, 4), + VerticalAlignment = VerticalAlignment.Center + }; + Grid.SetColumn(closeBtn, 1); + headerGrid.Children.Add(closeBtn); + closeBtn.Click += async (_, _) => await nav.PopAsync(null); + + var mainPage = new ContentPage + { + Header = headerGrid, + Content = new SanctuaryMainPage() + }; + NavigationPage.SetHasBackButton(mainPage, false); + + await nav.PushAsync(mainPage); + + if (carouselWrapper != null) + { + nav.RemovePage(carouselWrapper); + } + } +} diff --git a/samples/ControlCatalog/Pages/CommandBar/CommandBarEventsPage.xaml b/samples/ControlCatalog/Pages/CommandBar/CommandBarEventsPage.xaml new file mode 100644 index 0000000000..8dbc44e19b --- /dev/null +++ b/samples/ControlCatalog/Pages/CommandBar/CommandBarEventsPage.xaml @@ -0,0 +1,95 @@ + + + M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z + M15,9H5V5H15M12,19A3,3 0 0,1 9,16A3,3 0 0,1 12,13A3,3 0 0,1 15,16A3,3 0 0,1 12,19M17,3H5C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V7L17,3Z + M18,16.08C17.24,16.08 16.56,16.38 16.04,16.85L8.91,12.7C8.96,12.47 9,12.24 9,12C9,11.76 8.96,11.53 8.91,11.3L15.96,7.19C16.5,7.69 17.21,8 18,8A3,3 0 0,0 21,5A3,3 0 0,0 18,2A3,3 0 0,0 15,5C15,5.24 15.04,5.47 15.09,5.7L8.04,9.81C7.5,9.31 6.79,9 6,9A3,3 0 0,0 3,12A3,3 0 0,0 6,15C6.79,15 7.5,14.69 8.04,14.19L15.16,18.34C15.11,18.55 15.08,18.77 15.08,19C15.08,20.61 16.39,21.91 18,21.91C19.61,21.91 20.92,20.61 20.92,19C20.92,17.39 19.61,16.08 18,16.08Z + M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20M16,11V18.1L13.9,16L11.1,18.8L8.3,16L11.1,13.2L9,11.1L16,11Z + M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/DrawerPage/DrawerPageBreakpointPage.xaml.cs b/samples/ControlCatalog/Pages/DrawerPage/DrawerPageBreakpointPage.xaml.cs new file mode 100644 index 0000000000..0da2eb2ed7 --- /dev/null +++ b/samples/ControlCatalog/Pages/DrawerPage/DrawerPageBreakpointPage.xaml.cs @@ -0,0 +1,84 @@ +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Interactivity; + +namespace ControlCatalog.Pages +{ + public partial class DrawerPageBreakpointPage : UserControl + { + private bool _isLoaded; + + public DrawerPageBreakpointPage() + { + InitializeComponent(); + } + + protected override void OnLoaded(RoutedEventArgs e) + { + base.OnLoaded(e); + _isLoaded = true; + DemoDrawer.PropertyChanged += OnDrawerPropertyChanged; + UpdateStatus(); + } + + protected override void OnUnloaded(RoutedEventArgs e) + { + base.OnUnloaded(e); + DemoDrawer.PropertyChanged -= OnDrawerPropertyChanged; + } + + private void OnDrawerPropertyChanged(object? sender, Avalonia.AvaloniaPropertyChangedEventArgs e) + { + if (e.Property == DrawerPage.BoundsProperty) + UpdateStatus(); + } + + private void OnBreakpointChanged(object? sender, RangeBaseValueChangedEventArgs e) + { + if (!_isLoaded) + return; + var value = (int)e.NewValue; + DemoDrawer.DrawerBreakpointLength = value; + BreakpointText.Text = value.ToString(); + UpdateStatus(); + } + + private void OnLayoutChanged(object? sender, SelectionChangedEventArgs e) + { + if (!_isLoaded) + return; + DemoDrawer.DrawerLayoutBehavior = LayoutCombo.SelectedIndex switch + { + 0 => DrawerLayoutBehavior.Split, + 1 => DrawerLayoutBehavior.CompactInline, + 2 => DrawerLayoutBehavior.CompactOverlay, + _ => DrawerLayoutBehavior.Split + }; + UpdateStatus(); + } + + private void OnMenuItemClick(object? sender, RoutedEventArgs e) + { + if (!_isLoaded || sender is not Button button) + return; + var item = button.Tag?.ToString() ?? "Home"; + DetailTitleText.Text = item; + DetailPage.Header = item; + if (DemoDrawer.DrawerLayoutBehavior != DrawerLayoutBehavior.Split) + DemoDrawer.IsOpen = false; + } + + private void UpdateStatus() + { + var isVertical = DemoDrawer.DrawerPlacement == DrawerPlacement.Top || + DemoDrawer.DrawerPlacement == DrawerPlacement.Bottom; + var length = isVertical ? DemoDrawer.Bounds.Height : DemoDrawer.Bounds.Width; + var breakpoint = DemoDrawer.DrawerBreakpointLength; + WidthText.Text = $"{(isVertical ? "Height" : "Width")}: {(int)length} px"; + var isOverlay = breakpoint > 0 && length > 0 && length < breakpoint; + ModeText.Text = isOverlay ? + "Mode: Overlay (below breakpoint)" : + $"Mode: {DemoDrawer.DrawerLayoutBehavior} (above breakpoint)"; + } + } +} diff --git a/samples/ControlCatalog/Pages/NavigationDemoPage.xaml.cs b/samples/ControlCatalog/Pages/NavigationDemoPage.xaml.cs index b4ec1503bd..d65a43a6ad 100644 --- a/samples/ControlCatalog/Pages/NavigationDemoPage.xaml.cs +++ b/samples/ControlCatalog/Pages/NavigationDemoPage.xaml.cs @@ -28,6 +28,9 @@ namespace ControlCatalog.Pages // Data ("Data", "Pass Data", "Pass data during navigation via constructor arguments or DataContext.", () => new NavigationPagePassDataPage()), + ("Data", "MVVM Navigation", + "Keep navigation decisions in view models by routing NavigationPage push and pop operations through a small INavigationService.", + () => new NavigationPageMvvmPage()), // Features ("Features", "Attached Methods", diff --git a/samples/ControlCatalog/Pages/NavigationPage/LAvenirAppPage.xaml.cs b/samples/ControlCatalog/Pages/NavigationPage/LAvenirAppPage.xaml.cs index 6c4a67a473..beb0b2dccb 100644 --- a/samples/ControlCatalog/Pages/NavigationPage/LAvenirAppPage.xaml.cs +++ b/samples/ControlCatalog/Pages/NavigationPage/LAvenirAppPage.xaml.cs @@ -59,6 +59,26 @@ public partial class LAvenirAppPage : UserControl _infoPanel.IsVisible = Bounds.Width >= 650; } + void ApplyRootNavigationBarAppearance() + { + if (_navPage == null) + return; + + _navPage.Background = new SolidColorBrush(BgLight); + _navPage.Resources["NavigationBarBackground"] = new SolidColorBrush(BgLight); + _navPage.Resources["NavigationBarForeground"] = new SolidColorBrush(TextDark); + } + + void ApplyDetailNavigationBarAppearance() + { + if (_navPage == null) + return; + + _navPage.Background = new SolidColorBrush(BgDark); + _navPage.Resources["NavigationBarBackground"] = new SolidColorBrush(BgDark); + _navPage.Resources["NavigationBarForeground"] = Brushes.White; + } + TabbedPage BuildMenuTabbedPage() { var tp = new TabbedPage @@ -92,6 +112,7 @@ public partial class LAvenirAppPage : UserControl VerticalAlignment = VerticalAlignment.Center, TextAlignment = TextAlignment.Center, }; + ApplyRootNavigationBarAppearance(); NavigationPage.SetTopCommandBar(tp, new Button { @@ -119,7 +140,7 @@ public partial class LAvenirAppPage : UserControl Content = menuView, Background = new SolidColorBrush(BgLight), Header = "Menu", - Icon = "M11 9H9V2H7v7H5V2H3v7c0 2.12 1.66 3.84 3.75 3.97V22h2.5v-9.03C11.34 12.84 13 11.12 13 9V2h-2v7zm5-3v8h2.5v8H21V2c-2.76 0-5 2.24-5 4z", + Icon = Geometry.Parse("M11 9H9V2H7v7H5V2H3v7c0 2.12 1.66 3.84 3.75 3.97V22h2.5v-9.03C11.34 12.84 13 11.12 13 9V2h-2v7zm5-3v8h2.5v8H21V2c-2.76 0-5 2.24-5 4z"), }; var reservationsPage = new ContentPage @@ -127,7 +148,7 @@ public partial class LAvenirAppPage : UserControl Content = new LAvenirReservationsView(), Background = new SolidColorBrush(BgLight), Header = "Reservations", - Icon = "M19 3h-1V1h-2v2H8V1H6v2H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V8h14v11zM9 10H7v2h2v-2zm4 0h-2v2h2v-2zm4 0h-2v2h2v-2z", + Icon = Geometry.Parse("M19 3h-1V1h-2v2H8V1H6v2H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V8h14v11zM9 10H7v2h2v-2zm4 0h-2v2h2v-2zm4 0h-2v2h2v-2z"), }; var profilePage = new ContentPage @@ -135,7 +156,7 @@ public partial class LAvenirAppPage : UserControl Content = new LAvenirProfileView(), Background = new SolidColorBrush(BgLight), Header = "Profile", - Icon = "M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 3c1.66 0 3 1.34 3 3s-1.34 3-3 3-3-1.34-3-3 1.34-3 3-3zm0 14.2c-2.5 0-4.71-1.28-6-3.22.03-1.99 4-3.08 6-3.08 1.99 0 5.97 1.09 6 3.08-1.29 1.94-3.5 3.22-6 3.22z", + Icon = Geometry.Parse("M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 3c1.66 0 3 1.34 3 3s-1.34 3-3 3-3-1.34-3-3 1.34-3 3-3zm0 14.2c-2.5 0-4.71-1.28-6-3.22.03-1.99 4-3.08 6-3.08 1.99 0 5.97 1.09 6 3.08-1.29 1.94-3.5 3.22-6 3.22z"), }; tp.Pages = new ObservableCollection { menuPage, reservationsPage, profilePage }; @@ -144,7 +165,8 @@ public partial class LAvenirAppPage : UserControl async void PushDishDetail(string name, string price, string description, string imageFile) { - if (_navPage == null) return; + if (_navPage == null) + return; var detail = new ContentPage { @@ -153,22 +175,19 @@ public partial class LAvenirAppPage : UserControl Header = name, }; NavigationPage.SetBottomCommandBar(detail, BuildFloatingBar(price)); - - _navPage.Background = new SolidColorBrush(BgDark); - _navPage.Resources["NavigationBarBackground"] = new SolidColorBrush(BgDark); - _navPage.Resources["NavigationBarForeground"] = Brushes.White; - - detail.NavigatedFrom += (_, _) => + detail.Navigating += args => { - if (_navPage != null) - { - _navPage.Background = new SolidColorBrush(BgLight); - _navPage.Resources["NavigationBarBackground"] = new SolidColorBrush(BgLight); - _navPage.Resources["NavigationBarForeground"] = new SolidColorBrush(TextDark); - } + if (args.NavigationType == NavigationType.Pop) + ApplyRootNavigationBarAppearance(); + + return Task.CompletedTask; }; + ApplyDetailNavigationBarAppearance(); await _navPage.PushAsync(detail); + + if (!ReferenceEquals(_navPage.CurrentPage, detail)) + ApplyRootNavigationBarAppearance(); } Border BuildFloatingBar(string price) diff --git a/samples/ControlCatalog/Pages/NavigationPage/NavigationPageAppearancePage.xaml.cs b/samples/ControlCatalog/Pages/NavigationPage/NavigationPageAppearancePage.xaml.cs index 52e667b0bf..147dbe1f75 100644 --- a/samples/ControlCatalog/Pages/NavigationPage/NavigationPageAppearancePage.xaml.cs +++ b/samples/ControlCatalog/Pages/NavigationPage/NavigationPageAppearancePage.xaml.cs @@ -8,6 +8,7 @@ namespace ControlCatalog.Pages { public partial class NavigationPageAppearancePage : UserControl { + private bool _initialized; private int _pageCount; private int _backButtonStyle; @@ -19,6 +20,10 @@ namespace ControlCatalog.Pages private async void OnLoaded(object? sender, RoutedEventArgs e) { + if (_initialized) + return; + + _initialized = true; await DemoNav.PushAsync(NavigationDemoHelper.MakePage("Appearance", "Change bar properties using the options panel.", 0), null); } diff --git a/samples/ControlCatalog/Pages/NavigationPage/NavigationPageAttachedMethodsPage.xaml.cs b/samples/ControlCatalog/Pages/NavigationPage/NavigationPageAttachedMethodsPage.xaml.cs index 5a868046f3..01aef5385b 100644 --- a/samples/ControlCatalog/Pages/NavigationPage/NavigationPageAttachedMethodsPage.xaml.cs +++ b/samples/ControlCatalog/Pages/NavigationPage/NavigationPageAttachedMethodsPage.xaml.cs @@ -8,6 +8,7 @@ namespace ControlCatalog.Pages { public partial class NavigationPageAttachedMethodsPage : UserControl { + private bool _initialized; private int _pageCount; public NavigationPageAttachedMethodsPage() @@ -18,6 +19,10 @@ namespace ControlCatalog.Pages private async void OnLoaded(object? sender, RoutedEventArgs e) { + if (_initialized) + return; + + _initialized = true; await DemoNav.PushAsync(new ContentPage { Header = "Root Page", diff --git a/samples/ControlCatalog/Pages/NavigationPage/NavigationPageBackButtonPage.xaml.cs b/samples/ControlCatalog/Pages/NavigationPage/NavigationPageBackButtonPage.xaml.cs index 347b8e8010..5dd438750f 100644 --- a/samples/ControlCatalog/Pages/NavigationPage/NavigationPageBackButtonPage.xaml.cs +++ b/samples/ControlCatalog/Pages/NavigationPage/NavigationPageBackButtonPage.xaml.cs @@ -8,6 +8,7 @@ namespace ControlCatalog.Pages { public partial class NavigationPageBackButtonPage : UserControl { + private bool _initialized; private int _pushCount; public NavigationPageBackButtonPage() @@ -18,6 +19,10 @@ namespace ControlCatalog.Pages private async void OnLoaded(object? sender, RoutedEventArgs e) { + if (_initialized) + return; + + _initialized = true; DemoNav.Pushed += (s, ev) => AddLog($"Pushed: \"{ev.Page?.Header}\""); DemoNav.Popped += (s, ev) => AddLog($"Popped: \"{ev.Page?.Header}\""); diff --git a/samples/ControlCatalog/Pages/NavigationPage/NavigationPageEventsPage.xaml b/samples/ControlCatalog/Pages/NavigationPage/NavigationPageEventsPage.xaml index 74d0e58371..904d4310cc 100644 --- a/samples/ControlCatalog/Pages/NavigationPage/NavigationPageEventsPage.xaml +++ b/samples/ControlCatalog/Pages/NavigationPage/NavigationPageEventsPage.xaml @@ -51,6 +51,13 @@ + + + + + + diff --git a/samples/ControlCatalog/Pages/NavigationPage/NavigationPageEventsPage.xaml.cs b/samples/ControlCatalog/Pages/NavigationPage/NavigationPageEventsPage.xaml.cs index faa47f6eda..c1c439f6a4 100644 --- a/samples/ControlCatalog/Pages/NavigationPage/NavigationPageEventsPage.xaml.cs +++ b/samples/ControlCatalog/Pages/NavigationPage/NavigationPageEventsPage.xaml.cs @@ -7,6 +7,7 @@ namespace ControlCatalog.Pages { public partial class NavigationPageEventsPage : UserControl { + private bool _initialized; private int _pageCount; public NavigationPageEventsPage() @@ -17,6 +18,10 @@ namespace ControlCatalog.Pages private async void OnLoaded(object? sender, RoutedEventArgs e) { + if (_initialized) + return; + + _initialized = true; DemoNav.Pushed += (s, ev) => AddLog($"Pushed → {ev.Page?.Header}"); DemoNav.Popped += (s, ev) => AddLog($"Popped ← {ev.Page?.Header}"); DemoNav.PoppedToRoot += (s, ev) => AddLog("PoppedToRoot"); diff --git a/samples/ControlCatalog/Pages/NavigationPage/NavigationPageFirstLookPage.xaml.cs b/samples/ControlCatalog/Pages/NavigationPage/NavigationPageFirstLookPage.xaml.cs index 32b2e8927d..f9a6f9aa41 100644 --- a/samples/ControlCatalog/Pages/NavigationPage/NavigationPageFirstLookPage.xaml.cs +++ b/samples/ControlCatalog/Pages/NavigationPage/NavigationPageFirstLookPage.xaml.cs @@ -6,6 +6,7 @@ namespace ControlCatalog.Pages { public partial class NavigationPageFirstLookPage : UserControl { + private bool _initialized; private int _pageCount; public NavigationPageFirstLookPage() @@ -16,6 +17,10 @@ namespace ControlCatalog.Pages private async void OnLoaded(object? sender, RoutedEventArgs e) { + if (_initialized) + return; + + _initialized = true; await DemoNav.PushAsync(NavigationDemoHelper.MakePage("Home", "Welcome!\nUse the buttons to push and pop pages.", 0), null); UpdateStatus(); } diff --git a/samples/ControlCatalog/Pages/NavigationPage/NavigationPageGesturePage.xaml.cs b/samples/ControlCatalog/Pages/NavigationPage/NavigationPageGesturePage.xaml.cs index c18cfebc7e..e185208119 100644 --- a/samples/ControlCatalog/Pages/NavigationPage/NavigationPageGesturePage.xaml.cs +++ b/samples/ControlCatalog/Pages/NavigationPage/NavigationPageGesturePage.xaml.cs @@ -7,6 +7,8 @@ namespace ControlCatalog.Pages { public partial class NavigationPageGesturePage : UserControl { + private bool _initialized; + public NavigationPageGesturePage() { InitializeComponent(); @@ -16,6 +18,10 @@ namespace ControlCatalog.Pages private async void OnLoaded(object? sender, RoutedEventArgs e) { + if (_initialized) + return; + + _initialized = true; await DemoNav.PushAsync(NavigationDemoHelper.MakePage("Page 1", "← Drag from the left edge to go back", 0), null); await DemoNav.PushAsync(NavigationDemoHelper.MakePage("Page 2", "← Drag from the left edge to go back", 1), null); await DemoNav.PushAsync(NavigationDemoHelper.MakePage("Page 3", "← Drag from the left edge to go back", 2), null); diff --git a/samples/ControlCatalog/Pages/NavigationPage/NavigationPageInteractiveHeaderPage.xaml.cs b/samples/ControlCatalog/Pages/NavigationPage/NavigationPageInteractiveHeaderPage.xaml.cs index 1dc724128b..6a56beabe4 100644 --- a/samples/ControlCatalog/Pages/NavigationPage/NavigationPageInteractiveHeaderPage.xaml.cs +++ b/samples/ControlCatalog/Pages/NavigationPage/NavigationPageInteractiveHeaderPage.xaml.cs @@ -37,6 +37,7 @@ namespace ControlCatalog.Pages ]; private readonly ObservableCollection _filteredItems = new(AllContacts); + private bool _initialized; private string _searchText = ""; public NavigationPageInteractiveHeaderPage() @@ -47,6 +48,10 @@ namespace ControlCatalog.Pages private async void OnLoaded(object? sender, RoutedEventArgs e) { + if (_initialized) + return; + + _initialized = true; var headerGrid = new Grid { ColumnDefinitions = new ColumnDefinitions("*, Auto"), diff --git a/samples/ControlCatalog/Pages/NavigationPage/NavigationPageModalPage.xaml.cs b/samples/ControlCatalog/Pages/NavigationPage/NavigationPageModalPage.xaml.cs index 1dd717234e..81ed6d5c1f 100644 --- a/samples/ControlCatalog/Pages/NavigationPage/NavigationPageModalPage.xaml.cs +++ b/samples/ControlCatalog/Pages/NavigationPage/NavigationPageModalPage.xaml.cs @@ -7,6 +7,7 @@ namespace ControlCatalog.Pages { public partial class NavigationPageModalPage : UserControl { + private bool _initialized; private int _modalCount; public NavigationPageModalPage() @@ -17,6 +18,10 @@ namespace ControlCatalog.Pages private async void OnLoaded(object? sender, RoutedEventArgs e) { + if (_initialized) + return; + + _initialized = true; await DemoNav.PushAsync(NavigationDemoHelper.MakePage("Home", "Use Push Modal to show a modal on top.", 0), null); } diff --git a/samples/ControlCatalog/Pages/NavigationPage/NavigationPageModalTransitionsPage.xaml.cs b/samples/ControlCatalog/Pages/NavigationPage/NavigationPageModalTransitionsPage.xaml.cs index ac4e8c985d..2c77798570 100644 --- a/samples/ControlCatalog/Pages/NavigationPage/NavigationPageModalTransitionsPage.xaml.cs +++ b/samples/ControlCatalog/Pages/NavigationPage/NavigationPageModalTransitionsPage.xaml.cs @@ -20,6 +20,7 @@ namespace ControlCatalog.Pages ]; private int _modalCount; + private bool _initialized; public NavigationPageModalTransitionsPage() { @@ -29,6 +30,13 @@ namespace ControlCatalog.Pages private async void OnLoaded(object? sender, RoutedEventArgs e) { + if (_initialized) + { + UpdateTransition(); + return; + } + + _initialized = true; await DemoNav.PushAsync(new ContentPage { Header = "Modal Transitions", diff --git a/samples/ControlCatalog/Pages/NavigationPage/NavigationPageMvvmNavigation.cs b/samples/ControlCatalog/Pages/NavigationPage/NavigationPageMvvmNavigation.cs new file mode 100644 index 0000000000..c6262ca9f4 --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationPage/NavigationPageMvvmNavigation.cs @@ -0,0 +1,95 @@ +using System; +using System.Threading.Tasks; +using Avalonia.Controls; +using MiniMvvm; + +namespace ControlCatalog.Pages +{ + internal interface ISampleNavigationService + { + event EventHandler? StateChanged; + + Task NavigateToAsync(ViewModelBase viewModel); + + Task GoBackAsync(); + + Task PopToRootAsync(); + } + + internal interface ISamplePageFactory + { + ContentPage CreatePage(ViewModelBase viewModel); + } + + internal sealed class NavigationStateChangedEventArgs : EventArgs + { + public NavigationStateChangedEventArgs(string currentPageHeader, int navigationDepth, string lastAction) + { + CurrentPageHeader = currentPageHeader; + NavigationDepth = navigationDepth; + LastAction = lastAction; + } + + public string CurrentPageHeader { get; } + + public int NavigationDepth { get; } + + public string LastAction { get; } + } + + internal sealed class SampleNavigationService : ISampleNavigationService + { + private readonly NavigationPage _navigationPage; + private readonly ISamplePageFactory _pageFactory; + + public SampleNavigationService(NavigationPage navigationPage, ISamplePageFactory pageFactory) + { + _navigationPage = navigationPage; + _pageFactory = pageFactory; + + _navigationPage.Pushed += (_, e) => PublishState($"Pushed {e.Page?.Header}"); + _navigationPage.Popped += (_, e) => PublishState($"Popped {e.Page?.Header}"); + _navigationPage.PoppedToRoot += (_, _) => PublishState("Popped to root"); + } + + public event EventHandler? StateChanged; + + public async Task NavigateToAsync(ViewModelBase viewModel) + { + var page = _pageFactory.CreatePage(viewModel); + await _navigationPage.PushAsync(page); + } + + public async Task GoBackAsync() + { + if (_navigationPage.NavigationStack.Count <= 1) + { + PublishState("Already at the root page"); + return; + } + + await _navigationPage.PopAsync(); + } + + public async Task PopToRootAsync() + { + if (_navigationPage.NavigationStack.Count <= 1) + { + PublishState("Already at the root page"); + return; + } + + await _navigationPage.PopToRootAsync(); + } + + private void PublishState(string lastAction) + { + var header = _navigationPage.CurrentPage?.Header?.ToString() ?? "None"; + + StateChanged?.Invoke(this, new NavigationStateChangedEventArgs( + header, + _navigationPage.NavigationStack.Count, + lastAction)); + } + } +} diff --git a/samples/ControlCatalog/Pages/NavigationPage/NavigationPageMvvmPage.xaml b/samples/ControlCatalog/Pages/NavigationPage/NavigationPageMvvmPage.xaml new file mode 100644 index 0000000000..7204b78a4d --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationPage/NavigationPageMvvmPage.xaml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + +