diff --git a/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Controls/Given_Button.cs b/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Controls/Given_Button.cs index d37d1d67fb5c..b56948707b50 100644 --- a/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Controls/Given_Button.cs +++ b/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Controls/Given_Button.cs @@ -16,6 +16,7 @@ using Microsoft.UI.Xaml.Markup; using Microsoft.UI.Xaml.Controls.Primitives; using Color = Windows.UI.Color; +using Microsoft.UI.Xaml.Data; #if HAS_UNO_WINUI || WINAPPSDK || WINUI using Colors = Microsoft.UI.Colors; @@ -317,6 +318,26 @@ public async Task When_Button_Flyout_TemplateBinding() } #endif + [TestMethod] + public async Task When_Command_CanExecute_Throws() + { + // Here we are testing against a bug where + // a data-bound ICommand that throws in its CanExecute + // can cause the binding to reset to its FallbackValue. + var vm = new + { + UnstableCommand = new DelegateCommand(x => throw new Exception("fail...")), + }; + + var sut = new Button(); + sut.SetBinding(Button.CommandProperty, new Binding { Path = new(nameof(vm.UnstableCommand)), FallbackValue = new NoopCommand() }); + sut.DataContext = vm; + + await UITestHelper.Load(sut, x => x.IsLoaded); + + Assert.AreEqual(vm.UnstableCommand, sut.Command, "Binding did not set the proper value."); + } + private async Task RunIsExecutingCommandCommon(IsExecutingCommand command) { void FocusManager_LosingFocus(object sender, LosingFocusEventArgs e) @@ -421,5 +442,26 @@ public void Execute(object parameter) CanExecuteChanged?.Invoke(this, EventArgs.Empty); } } + + public class DelegateCommand : ICommand + { + private readonly Func canExecuteImpl; + private readonly Action executeImpl; + + public event EventHandler CanExecuteChanged; + + public DelegateCommand(Func canExecute = null, Action execute = null) + { + this.canExecuteImpl = canExecute; + this.executeImpl = execute; + } + + public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, default); + + public bool CanExecute(object parameter) => canExecuteImpl?.Invoke(parameter) ?? true; + public void Execute(object parameter) => executeImpl?.Invoke(parameter); + } + + public class NoopCommand() : DelegateCommand(null, null) { } } } diff --git a/src/Uno.UI/UI/Xaml/Controls/Primitives/ButtonBase/ButtonBase.mux.cs b/src/Uno.UI/UI/Xaml/Controls/Primitives/ButtonBase/ButtonBase.mux.cs index 5f6aef4e150c..9a5c4b7354b4 100644 --- a/src/Uno.UI/UI/Xaml/Controls/Primitives/ButtonBase/ButtonBase.mux.cs +++ b/src/Uno.UI/UI/Xaml/Controls/Primitives/ButtonBase/ButtonBase.mux.cs @@ -7,6 +7,7 @@ using System; using System.Windows.Input; using Uno.Disposables; +using Uno.Foundation.Logging; using Uno.UI.Xaml.Core; using Windows.Devices.Input; using Windows.Foundation; @@ -86,7 +87,7 @@ internal override void OnPropertyChanged2(DependencyPropertyChangedEventArgs arg } else if (args.Property == CommandParameterProperty) { - UpdateCanExecute(); + UpdateCanExecuteSafe(); } else if (args.Property == VisibilityProperty) { @@ -273,7 +274,7 @@ void CanExecuteChangedHandler(object? sender, object args) } // Coerce the button enabled state with the CanExecute state of the command. - UpdateCanExecute(); + UpdateCanExecuteSafe(); } /// @@ -297,6 +298,24 @@ private void UpdateCanExecute() SuppressIsEnabled(suppress); } + private void UpdateCanExecuteSafe() + { + // uno specific workaround: + // If Button::Command binding produces an ICommand value that throws Exception in its CanExecute, + // this value will be canceled and replaced by the Binding::FallbackValue. + try + { + UpdateCanExecute(); + } + catch (Exception e) + { + if (this.Log().IsEnabled(LogLevel.Error)) + { + this.Log().Error($"Failed to update CanExecute", e); + } + } + } + /// /// Executes ButtonBase.Command. ///