19
votes

I have the following in my page:

<ContentPage.ToolbarItems>
    <ToolbarItem Text="Run" Command="{Binding RunCommand}" />
</ContentPage.ToolbarItems>

The command starts an async task. I am trying to disable the control as long as the async task is still running by binding it to a boolean property as follows:

<ContentPage.ToolbarItems>
    <ToolbarItem Text="Run" Command="{Binding RunCommand}" IsEnabled="{Binding MyBoolProperty}" />
</ContentPage.ToolbarItems>

My issue is that there doesn't seem to be a "IsEnabled" property for ToolbarItem. Is there a way to achieve what I am trying to do using Xamarin.Forms?

4
Note, I tried replacing "ToolbarItem" with a "Button" and failed miserably: "Object type Xamarin.Forms.Button cannot be converted to target type: Xamarin.Forms.ToolbarItem".LostBalloon
depending on the expected length of your operation if you run it on the UI thread the button remains in pressed state. it's an ugly hack and you shouldn't use it for network access since that can lock your app up for a very long periodSten Petrov
Perhaps the source was updated, but now there is a IsEnabled property. Though, I can't still change the value once set.testing

4 Answers

20
votes

After the help of William and Xamarin support, I was finally able to find how functionality works.

It is a bit counter intuitive as we expect to Enable/Disable the button (ToolbarItem), but we actually have to manage the state of the command that is bound to the button. Once we understand this pattern, it makes sense.

The Command object of type ICommand, has a CanExecute property (thank you William for pointing it out) Now you don't want to access/use it directly unless it is for actually checking if the command can be executed or not.

Wherever you see fit in your code, to change the state of the command, you need to add the following line:

((Command)_myCommand).ChangeCanExecute();

This line will force the CanExecute property to be re-evaluated for the specified command.

I personally decided to add it where I track the inactivity as it made sense in my application.

public bool Inactive { 
    get { 
        return _inactive;
    } 
    set {
        if (_inactive != value) {
            _inactive = value;
            ((Command)_myCommand).ChangeCanExecute();
            OnPropertyChanged ();
        }
    }
}

In the View, there are no changes to be noted:

<ToolbarItem Text="Run" Command="{Binding MyCommand}" />

Now when you create the Command object is where the big work will be done. We usually use the single argument constructor as it is generally enough and it is where we define what our command does. Interestingly enough, there is a 2 parameter constructor as well where you can provide the function/action that determines the value of the CanExecute property.

_myCommand = new Command (async () => {
                                          Inactive = false;
                                          await Run();
                                          Inactive = true;
                                      },
                                      () => {
                                          return Inactive;
                                      });


public ICommand MyCommand {
    get { 
        return _myCommand;
    }
}

Edit: I know you that technically changing the value of Inactive should happen in Run(), but for demonstration purposes...

2
votes

This example is to remove, not disable but might also be handy.

    ToolbarItem delToolbar;       

    ...

        delToolbar = new ToolbarItem
        {
            Order = ToolbarItemOrder.Primary,                
            Text = "delete",              
            Command = new Command(async () =>
            {                                       
                ToolbarItems.Remove(delToolbar);
            })
        };
        ToolbarItems.Add(delToolbar);
1
votes

What I've learned to do in these situations is as follows:

public Command RunCommand 
{ 
    get { return new Command(async() => await OnRunCommand()); }
}    

private bool _isRunning;

public async Task OnRunCommand() 
{
    if (_isRunning) return;
    _isRunning = true;

    // do stuff

    _isRunning = false;
}

The downside: this leaves the toolbar item in its normal state and users may continue to tap on it.

The upside: this won't allow simultaneous OnRunCommand tasks, which is good.

If you want to pursue disabling the button by showing a disabled image, you should create a renderer.

If you do not want to show the toolbar item while the task is running, consider removing the toolbar item from the page and re-adding it later.

0
votes

LostBalloon's answer is correct in terms of using CanExecute command. But the problem I faced was that the ToolbarItems visual state didn't change. I want the secondary toolbar items (i.e. menu items) gray out when disabled (in other words when CanExecute returns false). Some authors was saying that this is a bug. May be it is a bug, but the following solution works for me.

The result:

enter image description here

To gray out disabled Toolbar items:

Set theme in your Resources/layout/Toolbar.xml file to MyToolbarTheme:

<androidx.appcompat.widget.Toolbar
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/toolbar"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:theme="@style/MyToolbarTheme"
 />

Add the theme MyToolbarTheme in Resources/values/styles.xml:

<style name="MyToolbarTheme" parent="ThemeOverlay.AppCompat.Light">
    <item name="android:textColor">@color/menu_item_color_selector</item>
    </style>

Now create a new file Resources/color/menu_item_color_selector.xml:

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_enabled="true" android:color="#000"/>
    <item android:color="#999"/>
</selector>

Side notes:

  1. You may use different parent themes, for example instead of androidx.appcompat.widget.Toolbar you can use com.google.android.material.appbar.MaterialToolbar. Works either way.

  2. Some authors reference to the attribute actionMenuTextColor for changing menu items font color, but this one didn't work for me (not sure why). Instead I was using android:textColor and it works.

  3. You can use android:background in Toolbar.xml to set the background of the toolbar itself.

  4. You can change the color of the 3 dots by setting colorControlNormal in MyToolbarTheme:

<item name="colorControlNormal">#0FF000</item>
  1. Any Android XML resource file you created in Visual Studio, must have build action AndroidResource and Custom Tool field set to MSBuild:UpdateGeneratedFiles (set it is in property window of the file)

  2. In addition to #5, the Toolbar.xml file must be referenced in MainActivity.cs:

protected override void OnCreate(Bundle bundle)
{
    ToolbarResource = Resource.Layout.Toolbar;
    base.OnCreate(bundle);
    ...
}
  1. Make sure you are using android:theme tag in Toolbar.xml, not android:popupTheme. Not obviously why, but the popupTheme didn't work for me.