* iOS] Fix Issue #8529 - Shell BackButtonBehavior crashing on Command property binding on non-Xamarin.Forms.Command ICommand implementations
* Changes after code review
* - add ui test to android
--- /dev/null
+using System;
+using System.Threading.Tasks;
+using System.Windows.Input;
+using Xamarin.Forms.CustomAttributes;
+using Xamarin.Forms.Internals;
+
+#if UITEST
+using Xamarin.Forms.Core.UITests;
+using Xamarin.UITest;
+using NUnit.Framework;
+#endif
+
+namespace Xamarin.Forms.Controls.Issues
+{
+#if UITEST
+ [NUnit.Framework.Category(UITestCategories.Shell)]
+#endif
+ [Preserve(AllMembers = true)]
+ [Issue(IssueTracker.Github, 8529,
+ "[Bug] [Shell] iOS - BackButtonBehavior Command property binding throws InvalidCastException when using a custom command class that implements ICommand",
+ PlatformAffected.iOS)]
+ public class Issue8529 : TestShell
+ {
+ const string ContentPageTitle = "Item1";
+ const string ButtonId = "ButtonId";
+
+ protected override void Init()
+ {
+ CreateContentPage(ContentPageTitle).Content =
+ new StackLayout
+ {
+ Children =
+ {
+ new Button
+ {
+ AutomationId = ButtonId,
+ Text = "Tap to Navigate To the Page With BackButtonBehavior",
+ Command = new Command(NavToBackButtonBehaviorPage)
+ }
+ }
+ };
+ }
+
+ private void NavToBackButtonBehaviorPage()
+ {
+ _ = Shell.Current.Navigation.PushAsync(new Issue8529_1());
+ }
+
+#if UITEST && __SHELL__
+ public void NavigateBack()
+ {
+#if __IOS__
+ RunningApp.Tap(c => c.Marked("BackButtonImage"));
+#else
+ RunningApp.Tap(FlyoutIconAutomationId);
+#endif
+ }
+
+ [Test]
+ public void Issue8529ShellBackButtonBehaviorCommandPropertyCanUseICommand()
+ {
+ RunningApp.WaitForElement(ButtonId, "Timed out waiting for first page.");
+ RunningApp.Tap(ButtonId);
+ RunningApp.WaitForElement("LabelId", "Timed out waiting for the destination page.");
+ NavigateBack();
+ RunningApp.WaitForElement(ButtonId, "Timed out waiting to navigate back to the first page.");
+ }
+#endif
+ }
+}
\ No newline at end of file
--- /dev/null
+<?xml version="1.0" encoding="utf-8" ?>
+<ContentPage
+ x:Class="Xamarin.Forms.Controls.Issues.Issue8529_1"
+ xmlns="http://xamarin.com/schemas/2014/forms"
+ xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
+ Title="BackButtonPage">
+
+ <Shell.BackButtonBehavior>
+ <BackButtonBehavior Command="{Binding BackCommand}">
+ <BackButtonBehavior.IconOverride>
+ <FileImageSource AutomationId="BackButtonImage" File="star-flyout.png" />
+ </BackButtonBehavior.IconOverride>
+ </BackButtonBehavior>
+ </Shell.BackButtonBehavior>
+
+ <StackLayout>
+ <Label AutomationId="LabelId" Text="This page has back button behavior." />
+ </StackLayout>
+
+</ContentPage>
\ No newline at end of file
--- /dev/null
+using Xamarin.Forms.Internals;
+using Xamarin.Forms.Xaml;
+using System;
+using System.Windows.Input;
+using System.Threading.Tasks;
+
+namespace Xamarin.Forms.Controls.Issues
+{
+#if APP
+ [XamlCompilation(XamlCompilationOptions.Compile)]
+#endif
+ public partial class Issue8529_1 : ContentPage
+ {
+ public Issue8529_1()
+ {
+#if APP
+ InitializeComponent();
+#endif
+ BindingContext = new Issue8529ViewModel();
+ }
+
+ [Preserve(AllMembers = true)]
+ public class Issue8529ViewModel
+ {
+ public ICommand BackCommand { get; set; }
+
+ public Issue8529ViewModel()
+ {
+ BackCommand = new Issue8529AsyncCommand(() =>
+ {
+ return Shell.Current.Navigation.PopAsync();
+ });
+ }
+ }
+
+ #region AsyncCommand
+
+ public interface Issue8529IAsyncCommand : ICommand
+ {
+ Task ExecuteAsync();
+ bool CanExecute();
+ }
+
+ public interface Issue8529IAsyncCommand<in T> : ICommand
+ {
+ Task ExecuteAsync(T parameter);
+ bool CanExecute(T parameter);
+ }
+
+ /// <summary>
+ /// Custom command class to demonstrate the crash.
+ /// </summary>
+ public class Issue8529AsyncCommand : Issue8529IAsyncCommand
+ {
+ private bool _isExecuting;
+ private readonly Func<Task> _execute;
+ private readonly Func<bool> _canExecute;
+ public event EventHandler CanExecuteChanged;
+
+ public Issue8529AsyncCommand(Func<Task> execute, Func<bool> canExecute = null)
+ {
+ _execute = execute;
+ _canExecute = canExecute;
+ }
+
+ public bool CanExecute()
+ {
+ return !_isExecuting && (_canExecute?.Invoke() ?? true);
+ }
+
+ public async Task ExecuteAsync()
+ {
+ if (CanExecute())
+ {
+ try
+ {
+ _isExecuting = true;
+ await _execute();
+ }
+ finally
+ {
+ _isExecuting = false;
+ }
+ }
+
+ RaiseCanExecuteChanged();
+ }
+
+ public void RaiseCanExecuteChanged()
+ {
+ CanExecuteChanged?.Invoke(this, EventArgs.Empty);
+ }
+
+ bool ICommand.CanExecute(object parameter)
+ {
+ return CanExecute();
+ }
+
+ void ICommand.Execute(object parameter)
+ {
+ var task = ExecuteAsync();
+ FireAndForgetSafeAsync(task);
+ }
+
+ public async void FireAndForgetSafeAsync(Task task)
+ {
+ try
+ {
+ await task;
+ }
+ catch (Exception)
+ {
+
+ }
+ }
+ }
+
+ public class Issue8529AsyncCommand<T> : Issue8529IAsyncCommand<T>
+ {
+ private bool _isExecuting;
+ private readonly Func<T, Task> _execute;
+ private readonly Func<T, bool> _canExecute;
+ public event EventHandler CanExecuteChanged;
+
+ public Issue8529AsyncCommand(Func<T, Task> execute, Func<T, bool> canExecute = null)
+ {
+ _execute = execute;
+ _canExecute = canExecute;
+ }
+
+ public bool CanExecute(T parameter)
+ {
+ return !_isExecuting && (_canExecute?.Invoke(parameter) ?? true);
+ }
+
+ public async Task ExecuteAsync(T parameter)
+ {
+ if (CanExecute(parameter))
+ {
+ try
+ {
+ _isExecuting = true;
+ await _execute(parameter);
+ }
+ finally
+ {
+ _isExecuting = false;
+ }
+ }
+
+ RaiseCanExecuteChanged();
+ }
+
+ public void RaiseCanExecuteChanged()
+ {
+ CanExecuteChanged?.Invoke(this, EventArgs.Empty);
+ }
+
+ bool ICommand.CanExecute(object parameter)
+ {
+ return parameter == null || CanExecute((T)parameter);
+ }
+
+ void ICommand.Execute(object parameter)
+ {
+ var task = ExecuteAsync((T)parameter);
+ FireAndForgetSafeAsync(task);
+ }
+
+ public async void FireAndForgetSafeAsync(Task task)
+ {
+ try
+ {
+ await task;
+ }
+ catch (Exception)
+ {
+
+ }
+ }
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
</Compile>
<Compile Include="$(MSBuildThisFileDirectory)Issue8167.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Issue8269.cs" />
+ <Compile Include="$(MSBuildThisFileDirectory)Issue8529.cs" />
+ <Compile Include="$(MSBuildThisFileDirectory)Issue8529_1.xaml.cs">
+ <DependentUpon>Issue8529_1.xaml</DependentUpon>
+ <SubType>Code</SubType>
+ </Compile>
<Compile Include="$(MSBuildThisFileDirectory)RefreshViewTests.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Issue7338.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ScrollToGroup.cs" />
<ItemGroup>
<EmbeddedResource Include="$(MSBuildThisFileDirectory)Issue7048.xaml">
<SubType>Designer</SubType>
- <Generator>MSBuild:Compile</Generator>
+ <Generator>MSBuild:UpdateDesignTimeXaml</Generator>
+ </EmbeddedResource>
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="$(MSBuildThisFileDirectory)Issue8529_1.xaml">
+ <Generator>MSBuild:UpdateDesignTimeXaml</Generator>
</EmbeddedResource>
</ItemGroup>
</Project>
\ No newline at end of file
void LeftBarButtonItemHandler(UIViewController controller, bool isRootPage)
{
var behavior = BackButtonBehavior;
- var command = behavior.GetPropertyIfSet(BackButtonBehavior.CommandProperty, new Command(() => OnMenuButtonPressed(this, EventArgs.Empty)));
+ ICommand defaultCommand = new Command(() => OnMenuButtonPressed(this, EventArgs.Empty));
+ var command = behavior.GetPropertyIfSet(BackButtonBehavior.CommandProperty, defaultCommand);
var commandParameter = behavior.GetPropertyIfSet<object>(BackButtonBehavior.CommandParameterProperty, null);
if (command == null && !isRootPage && controller?.ParentViewController is UINavigationController navigationController)