[iOS] Fix Issue #8529 - Shell BackButtonBehavior crashing on Command p… (#8544)
authorBrian Runck <brunck@users.noreply.github.com>
Thu, 21 Nov 2019 23:49:05 +0000 (18:49 -0500)
committerShane Neuville <shneuvil@microsoft.com>
Thu, 21 Nov 2019 23:49:05 +0000 (16:49 -0700)
* 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

Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Issue8529.cs [new file with mode: 0644]
Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Issue8529_1.xaml [new file with mode: 0644]
Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Issue8529_1.xaml.cs [new file with mode: 0644]
Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems
Xamarin.Forms.Platform.iOS/Renderers/ShellPageRendererTracker.cs

diff --git a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Issue8529.cs b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Issue8529.cs
new file mode 100644 (file)
index 0000000..f2d6fe6
--- /dev/null
@@ -0,0 +1,70 @@
+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
diff --git a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Issue8529_1.xaml b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Issue8529_1.xaml
new file mode 100644 (file)
index 0000000..b43cea8
--- /dev/null
@@ -0,0 +1,20 @@
+<?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
diff --git a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Issue8529_1.xaml.cs b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Issue8529_1.xaml.cs
new file mode 100644 (file)
index 0000000..d99461b
--- /dev/null
@@ -0,0 +1,185 @@
+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
index d1bfc4a..2884d68 100644 (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
index c794c60..d963c46 100644 (file)
@@ -269,7 +269,8 @@ namespace Xamarin.Forms.Platform.iOS
                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)