Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ protected SearchHandler SearchHandler

void AView.IOnClickListener.OnClick(AView v)
{
var backButtonHandler = Shell.GetBackButtonBehavior(Page);
var backButtonHandler = Shell.GetEffectiveBackButtonBehavior(Page);
var isEnabled = backButtonHandler.GetPropertyIfSet(BackButtonBehavior.IsEnabledProperty, true);

if (isEnabled)
Expand Down Expand Up @@ -255,7 +255,7 @@ protected virtual void OnPageChanged(Page oldPage, Page newPage)
if (newPage != null)
{
newPage.PropertyChanged += OnPagePropertyChanged;
_backButtonBehavior = Shell.GetBackButtonBehavior(newPage);
_backButtonBehavior = Shell.GetEffectiveBackButtonBehavior(newPage);

if (_backButtonBehavior != null)
_backButtonBehavior.PropertyChanged += OnBackButtonBehaviorChanged;
Expand Down Expand Up @@ -309,7 +309,7 @@ protected virtual void OnPagePropertyChanged(object sender, PropertyChangedEvent
UpdateNavBarHasShadow(Page);
else if (e.PropertyName == Shell.BackButtonBehaviorProperty.PropertyName)
{
var backButtonHandler = Shell.GetBackButtonBehavior(Page);
var backButtonHandler = Shell.GetEffectiveBackButtonBehavior(Page);

if (_backButtonBehavior != null)
_backButtonBehavior.PropertyChanged -= OnBackButtonBehaviorChanged;
Expand Down Expand Up @@ -407,7 +407,7 @@ protected virtual async void UpdateLeftBarButtonItem(Context context, AToolbar t
drawerLayout.AddDrawerListener(_drawerToggle);
}

var backButtonHandler = Shell.GetBackButtonBehavior(page);
var backButtonHandler = Shell.GetEffectiveBackButtonBehavior(page);
var text = backButtonHandler.GetPropertyIfSet(BackButtonBehavior.TextOverrideProperty, String.Empty);
var command = backButtonHandler.GetPropertyIfSet<ICommand>(BackButtonBehavior.CommandProperty, null);
var backButtonVisibleFromBehavior = backButtonHandler.GetPropertyIfSet(BackButtonBehavior.IsVisibleProperty, true);
Expand Down Expand Up @@ -540,7 +540,7 @@ protected virtual Task UpdateDrawerArrow(Context context, AToolbar toolbar, Draw

protected virtual void UpdateToolbarIconAccessibilityText(AToolbar toolbar, Shell shell)
{
var backButtonHandler = Shell.GetBackButtonBehavior(Page);
var backButtonHandler = Shell.GetEffectiveBackButtonBehavior(Page);
var image = GetFlyoutIcon(backButtonHandler, Page);
var text = backButtonHandler.GetPropertyIfSet(BackButtonBehavior.TextOverrideProperty, String.Empty);
var automationId = image?.AutomationId ?? text;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ protected virtual void OnPagePropertyChanged(object sender, PropertyChangedEvent
#nullable restore
if (e.PropertyName == Shell.BackButtonBehaviorProperty.PropertyName)
{
SetBackButtonBehavior(Shell.GetBackButtonBehavior(Page));
SetBackButtonBehavior(Shell.GetEffectiveBackButtonBehavior(Page));
}
else if (e.PropertyName == Shell.SearchHandlerProperty.PropertyName)
{
Expand Down Expand Up @@ -217,7 +217,7 @@ void UpdateShellToMyPage()
return;
}

SetBackButtonBehavior(Shell.GetBackButtonBehavior(Page));
SetBackButtonBehavior(Shell.GetEffectiveBackButtonBehavior(Page));
SearchHandler = Shell.GetSearchHandler(Page);
UpdateTitleView();
UpdateTitle();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ internal bool SendPop()
{
if (tracker.Value.ViewController == TopViewController)
{
var behavior = Shell.GetBackButtonBehavior(tracker.Value.Page);
var behavior = Shell.GetEffectiveBackButtonBehavior(tracker.Value.Page);
var command = behavior.GetPropertyIfSet<ICommand>(BackButtonBehavior.CommandProperty, null);
var commandParameter = behavior.GetPropertyIfSet<object>(BackButtonBehavior.CommandParameterProperty, null);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,6 @@ internal static void PropagatePropertyChanged(string propertyName, Element eleme

if (propertyName == null || propertyName == Shell.NavBarVisibilityAnimationEnabledProperty.PropertyName)
BaseShellItem.PropagateFromParent(Shell.NavBarVisibilityAnimationEnabledProperty, element);

if (propertyName == null || propertyName == Shell.BackButtonBehaviorProperty.PropertyName)
BaseShellItem.PropagateFromParent(Shell.BackButtonBehaviorProperty, element);

foreach (var child in children.ToArray())
{
Expand Down
30 changes: 29 additions & 1 deletion src/Controls/src/Core/Shell/Shell.cs
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,34 @@ static void OnFlyoutItemIsVisibleChanged(BindableObject bindable, object oldValu
/// <returns>The back button behavior for the object.</returns>
public static BackButtonBehavior GetBackButtonBehavior(BindableObject obj) => (BackButtonBehavior)obj.GetValue(BackButtonBehaviorProperty);

/// <summary>
/// Gets the BackButtonBehavior for the given page, with fallback to Shell if not set on the page.
/// </summary>
internal static BackButtonBehavior GetEffectiveBackButtonBehavior(BindableObject page)
{
if (page == null)
return null;

// First check if the page has its own BackButtonBehavior
var behavior = GetBackButtonBehavior(page);
if (behavior != null)
return behavior;

// Fallback: check if the Shell itself has a BackButtonBehavior
if (page is Element element)
{
var shell = element.FindParentOfType<Shell>();
if (shell != null)
{
behavior = GetBackButtonBehavior(shell);
if (behavior != null)
return behavior;
}
}

return null;
}

/// <summary>
/// Sets the back button behavior when the given <paramref name="obj"/> is presented.
/// </summary>
Expand Down Expand Up @@ -1556,7 +1584,7 @@ internal void SendStructureChanged()
protected override bool OnBackButtonPressed()
{
#if WINDOWS || !PLATFORM
var backButtonBehavior = GetBackButtonBehavior(GetVisiblePage());
var backButtonBehavior = GetEffectiveBackButtonBehavior(GetVisiblePage());
if (backButtonBehavior != null)
{
var command = backButtonBehavior.GetPropertyIfSet<ICommand>(BackButtonBehavior.CommandProperty, null);
Expand Down
2 changes: 1 addition & 1 deletion src/Controls/src/Core/ShellToolbar.cs
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ internal void ApplyChanges()

void UpdateBackbuttonBehavior()
{
var bbb = Shell.GetBackButtonBehavior(_currentPage);
var bbb = Shell.GetEffectiveBackButtonBehavior(_currentPage);

if (bbb == _backButtonBehavior)
return;
Expand Down
174 changes: 174 additions & 0 deletions src/Controls/tests/TestCases.HostApp/Issues/Issue33688.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Input;

namespace Maui.Controls.Sample.Issues;

[Issue(IssueTracker.Github, 33688, "BackButtonBehavior is no longer triggered once a ContentPage contains a CollectionView and the ItemsSource has been changed", PlatformAffected.iOS | PlatformAffected.Android)]
public class Issue33688 : Shell
{
public Issue33688()
{
var mainContent = new ShellContent
{
ContentTemplate = new DataTemplate(() => new Issue33688MainPage()),
Title = "Main",
Route = "main"
};

Items.Add(mainContent);
Routing.RegisterRoute("Issue33688_second", typeof(Issue33688SecondPage));
}
}

file class Issue33688MainPage : ContentPage
{
public Issue33688MainPage()
{
Padding = 24;

var resultLabel = new Label
{
Text = "Waiting for back button...",
AutomationId = "ResultLabel"
};

// Store reference so ViewModel can update it
Issue33688ViewModel.SetResultLabelRef(resultLabel);

Content = new VerticalStackLayout
{
Spacing = 10,
Children =
{
new Label
{
Text = "Tap the button to navigate to a page with a CollectionView. Then press back - the BackButtonBehavior command should fire and update the label below.",
AutomationId = "InstructionLabel"
},
new Button
{
Text = "Navigate to other Page",
AutomationId = "NavigateButton",
Command = new Command(() => Shell.Current.GoToAsync("Issue33688_second"))
},
resultLabel
}
};
}
}

file class Issue33688SecondPage : ContentPage
{
public Issue33688SecondPage()
{
// Use a ViewModel pattern with binding - this is the scenario from the issue
var viewModel = new Issue33688ViewModel();
BindingContext = viewModel;

// BackButtonBehavior with BOUND Command (key to reproduction)
var backButtonBehavior = new BackButtonBehavior();
backButtonBehavior.SetBinding(BackButtonBehavior.CommandProperty, nameof(Issue33688ViewModel.SaveAndNavigateBackCommand));
Shell.SetBackButtonBehavior(this, backButtonBehavior);

var collectionView = new CollectionView
{
AutomationId = "TestCollectionView",
ItemTemplate = new DataTemplate(() =>
{
var label = new Label();
label.SetBinding(Label.TextProperty, "Name");
return label;
})
};
collectionView.SetBinding(CollectionView.ItemsSourceProperty, nameof(Issue33688ViewModel.Items));

var filterButton = new Button
{
Text = "Load Items (triggers bug)",
AutomationId = "FilterButton"
};
filterButton.SetBinding(Button.CommandProperty, nameof(Issue33688ViewModel.FilterCommand));

var statusLabel = new Label
{
Text = "Tap 'Load Items' then press back button.",
AutomationId = "StatusLabel"
};

Content = new VerticalStackLayout
{
Padding = 24,
Spacing = 10,
Children =
{
statusLabel,
filterButton,
collectionView
}
};
}
}

file class Issue33688ViewModel : INotifyPropertyChanged
{
static Label _resultLabelRef = null!;

public static void SetResultLabelRef(Label label) => _resultLabelRef = label;

private ObservableCollection<Issue33688Item> _items = new();

public ObservableCollection<Issue33688Item> Items
{
get => _items;
set
{
if (_items != value)
{
_items = value;
OnPropertyChanged();
}
}
}

public ICommand SaveAndNavigateBackCommand { get; }
public ICommand FilterCommand { get; }

public Issue33688ViewModel()
{
SaveAndNavigateBackCommand = new Command(() =>
{
// Update the result label, then navigate back
if (_resultLabelRef != null)
{
_resultLabelRef.Text = "BackButtonBehavior triggered!";
}
Shell.Current.GoToAsync("..");
});

FilterCommand = new Command(() =>
{
// This is the key action that triggers the bug:
// Setting Items to a new ObservableCollection AFTER the page is displayed
Items = new ObservableCollection<Issue33688Item>
{
new Issue33688Item { Name = "Item 1" },
new Issue33688Item { Name = "Item 2" },
new Issue33688Item { Name = "Item 3" }
};
});
}

public event PropertyChangedEventHandler PropertyChanged = null!;

protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null!)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}

file class Issue33688Item
{
public string Name { get; set; } = string.Empty;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using NUnit.Framework;
using UITest.Appium;
using UITest.Core;

namespace Microsoft.Maui.TestCases.Tests.Issues;

public class Issue33688 : _IssuesUITest
{
public override string Issue => "BackButtonBehavior is no longer triggered once a ContentPage contains a CollectionView and the ItemsSource has been changed";

public Issue33688(TestDevice device) : base(device) { }

[Test]
[Category(UITestCategories.Shell)]
public void BackButtonBehaviorTriggersWithCollectionView()
{
// Wait for main page to load
App.WaitForElement("NavigateButton");

// Navigate to the second page with CollectionView
App.Tap("NavigateButton");

// Wait for the second page to load - use StatusLabel as primary indicator
App.WaitForElement("StatusLabel");

// Find and tap the filter button to load items - this triggers the bug
// (setting ItemsSource to a new ObservableCollection)
App.WaitForElement("FilterButton");
App.Tap("FilterButton");

// Give time for the CollectionView to update
App.WaitForElement("TestCollectionView");

// Press the back button
App.TapBackArrow();

// Wait for navigation back and verify BackButtonBehavior was triggered
App.WaitForElement("ResultLabel");

var resultText = App.FindElement("ResultLabel").GetText();
Assert.That(resultText, Is.EqualTo("BackButtonBehavior triggered!"),
"BackButtonBehavior command should have been executed when pressing back button");
}
}
Loading