Bug 51147 - Command CanExecute doesn't disable UI element when false
Summary: Command CanExecute doesn't disable UI element when false
Status: CONFIRMED
Alias: None
Product: Forms
Classification: Xamarin
Component: Forms ()
Version: 2.3.5
Hardware: PC Windows
: Normal normal
Target Milestone: ---
Assignee: Paul DiPietro [MSFT]
URL:
Depends on:
Blocks:
 
Reported: 2016-12-29 17:35 UTC by Clint
Modified: 2017-06-21 13:44 UTC (History)
6 users (show)

Tags: ac
Is this bug a regression?: ---
Last known good build:


Attachments
Multiple buttons to same command show weird behavior (899.77 KB, image/png)
2016-12-29 20:00 UTC, Clint
Details


Notice (2018-05-24): bugzilla.xamarin.com is now in read-only mode.

Please join us on Visual Studio Developer Community and in the Xamarin and Mono organizations on GitHub to continue tracking issues. Bugzilla will remain available for reference in read-only mode. We will continue to work on open Bugzilla bugs, copy them to the new locations as needed for follow-up, and add the new items under Related Links.

Our sincere thanks to everyone who has contributed on this bug tracker over the years. Thanks also for your understanding as we make these adjustments and improvements for the future.


Please create a new report for Bug 51147 on Developer Community or GitHub if you have new information to add and do not yet see a matching new report.

If the latest results still closely match this report, you can use the original description:

  • Export the original title and description: Developer Community HTML or GitHub Markdown
  • Copy the title and description into the new report. Adjust them to be up-to-date if needed.
  • Add your new information.

In special cases on GitHub you might also want the comments: GitHub Markdown with public comments

Related Links:
Status:
CONFIRMED

Description Clint 2016-12-29 17:35:29 UTC
If a button is binded to a command and the command is assigned with a CanExecute method..

What should happen:
When the method returns false the button is disabled.
When the method returns true the button is enabled.

What really happens:
Button starts out disabled - Right
When method returns true the button is enabled - Right
When method later returns false the button returns to disabled - FAIL.
Once the button flips to IsEnabled = true it never disabled again no matter how  many times the CanExecute method returns false.

Example button XAML binding the command to 'AcceptScanCommand'
                <!--  #Region Accept button  -->
                <Button HeightRequest="55"
                        Margin="0,7,0,0"
                        VerticalOptions="CenterAndExpand"
                        BackgroundColor="Green"
                        Command="{Binding AcceptScanCommand}"
                        CommandParameter="{Binding SelectedDocument}"
                        FontSize="{StaticResource MediumFontSize}"
                        IsVisible="{Binding AutoAccept,
                                            Converter={StaticResource BoolInvertConverter}}"
                        Text="{StaticResource Accept}"
                        TextColor="{StaticResource PrimaryTextColor}"
                        Style="{StaticResource ButtonStyleLrg}" />
                <!--  #EndRegion Accept button  -->

ViewModel assigning the command an OnExecuted handler and CanExecute handler
            acceptScanCommand = new Command(On_AcceptScanCommand, CanAcceptScanCommand);

The CanExecute handler:
        public ICommand AcceptScanCommand => acceptScanCommand;

        private bool CanAcceptScanCommand(object arg)
        {
            //The rules for being able to manually accept a scan:
            //We have a valid Document
            //We have a valid DocType
            //More rules may follow...

            if (SelectedDocument?.DocumentType == null)
                return false;
            //other rules here


            return true;
        }

When certain properties (such as the DocumentType shown above) are updated they call ReEvaluateCommands()
        void ReEvaluateCommands()
        {
            ((Command)RejectScanCommand)?.ChangeCanExecute();
            ((Command)AcceptScanCommand)?.ChangeCanExecute();
            ((Command)SetDocTypeCommand)?.ChangeCanExecute();
        }

When debugging CanAcceptScanCommand the "return false;" and "return true;" are hit when expected.  However after a return false the button is never disabled as it should be.
Comment 1 Clint 2016-12-29 20:00:41 UTC
Created attachment 19035 [details]
Multiple buttons to same command show weird behavior

When I realized that my [Reject] button binding to a Reject command and CanRejectScanCommand - all done exactly the same as the failing Accept button/command - was working correctly it got me investigating further.

Looking at the attached "2016-12-29_14-38-31.png" you can see something really strange.  All three green [Accept] buttons are copy/paste duplicate of the first one.  If you have the same command assigned to multiple UI elements (such as multiple buttons) one would expect them to all work together.  They should all enable and disable together, right?  Nope.  They enable SEQUENTIALLY each time the 'CanExecute' method returns true.

yep, you read that right.

The first time CanAccept returns true, the first button made gets enabled.  
The second time CanAccept returns true, the second button gets enabled.
The third time CanAccept returns true, the third button gets enabled.

The Accept and Reject commands and all their related bits are identical.  'Accept' was made by taking all the 'Reject' bits and doing a copy/paste/namechange.

But even if you don't accept they are the same, and want to say that the failure I'm seeing is my own coding mistake... that its some difference between the accept and reject codes... There is no way to account for the 3 buttons not reacting together in unison, from the same binding.  There is something weird happening in the ICommand binding when CanExecute is used, particularly if more than one item is bound to the same command.
Comment 2 Clint 2016-12-30 15:42:55 UTC
After more research I discovered the source of this bug: 
If the CommandParameter evaluates to null the change in CanExecute and enabled/disabled of any UI associated with it just doesn't take place.  

Related to bug #46603

NULL is a perfectly valid value, it is something that should be sendable to a converter, and sendable as a CommandParameter


When binding a command to a UI element and specifying a CanExecute method..
The XAML is required to include a CommandParameter whether it will be used by the CanExecuteMethod or not.  EX:
   Command="{Binding ScoobyScanCommand}"
   CommandParameter="{Binding SelectedDocument.DisplayBarcode}"

If the CommandParameter is null, either because it just wasn't included in the XAML or because the binding evaluates to null then the evaluation isn't executed/honored.

Use case:
You want a button enabled when you have a object, and disabled when you don't.

Expectation:
When you set an object to a non-null value the Command CAN execute and the button enables.
When you set an object to null the command CANNOT execute and the button disables.

Actually happening:
When you set the object to a non-null value the button enables.
When you set the object to null the evaluation just doesn't take place, so the button remains enabled.
Comment 3 Paul DiPietro [MSFT] 2017-03-01 16:54:32 UTC
I'll need to discuss this behavior with the engineering team when I get an opportunity to do so.
Comment 4 Philipp Sumi 2017-03-27 09:58:15 UTC
This is definitely a bug. If ICommand.CanExecute is false, a command MUST not fire. I just discovered that this was the source of double-taps just causing my commands to be invoked multiple times despite the command indicating otherwise.
Comment 5 Philipp Sumi 2017-03-27 10:01:25 UTC
Btw, in Button.SendClicked:

    void IButtonController.SendClicked()
    {
      ICommand command = this.Command;
      if (command != null)
        command.Execute(this.CommandParameter);
      
      ...
    }


This should rather be:

    void IButtonController.SendClicked()
    {
      ICommand command = this.Command;
      if (command != null && command.CanExecute(this.CommandParameter))
        command.Execute(this.CommandParameter);

      ...
    }