Search code examples
c#xamlxamarin.formsxaml-binding

Is it possible to bind the MenuItem.IsEnabled property to a different Context?


I have a ListView bound to a collection of objects (called Users, in this case), and the template includes a ContextActions menu. One of the menu items needs to be enabled or disabled depending on a condition having nothing directly to do with the items in the view (whether or not there's a Bluetooth connection to a certain kind of peripheral). What I'm doing right now is iterating the Cells in the TemplatedItems property and setting IsEnabled on each.

Here's the XAML for the ListView, stripped down to the parts that matter for my question:

<ListView ItemsSource="{Binding .}" ItemTapped="item_Tap">
    <ListView.ItemTemplate>
        <DataTemplate>
            <TextCell Text="{Binding Label}">
                <TextCell.ContextActions>
                    <MenuItem
                        Text="Copy to other device"
                        ClassId="copyMenuItem"
                        Clicked="copyMenuItem_Click" />
                </TextCell.ContextActions>
            </TextCell>
        </DataTemplate>
    </ListView.ItemTemplate>
</ListView>

Here's how I'm setting the property values now:

foreach (Cell cell in usersListView.TemplatedItems)
{
    foreach (MenuItem item in cell.ContextActions)
    {
        if ("copyMenuItem" == item.ClassId)
        {
            item.IsEnabled = isBluetoothConnected;
        }
    }
}

That works, but i don't like it. It's obviously out of line with the whole idea of data-bound views. I'd much rather have a boolean value that I can bind to the IsEnabled property, but it doesn't make sense from an object design point of view to add that to the User object; it has nothing to do with what that class is about (representing login accounts). I thought of wrapping User in some local class that exists just to tape this boolean property onto it, but that feels strange also since the value will always be the same for every item in the collection. Is there some other way to bind the MenuItem.IsEnabled property?


Solution

  • It turns out that this case of BindableProperty is, in fact, not bindable: https://learn.microsoft.com/en-us/xamarin/xamarin-forms/user-interface/menuitem#enable-or-disable-a-menuitem-at-runtime

    One must add a Command property to the MenuItem and assign a BindingContext to that, and set its executability. Here's the latest version of my code, which does work:

    <MenuItem
        Text="Copy to other device"
        Clicked="copyMenuItem_Click"
        BindingContext="{x:Reference usersListView}"
        Command="{Binding BindingContext.CopyCommand}" />
    
    public class UsersViewModel
    {
        public Command CopyCommand { get; set; }
        public bool IsBluetoothConnected
        {
            get { return isBluetoothConnected; }
            set
            {
                isBluetoothConnected = value;
                if (CopyCommand.CanExecute(null) != value)
                {
                    CopyCommand.ChangeCanExecute();
                }
            }
        }
        public ObservableCollection<User> Users { get; private set; }
    
        private bool isBluetoothConnected;
    
        public async System.Threading.Tasks.Task<int> Populate( )
        {
            CopyCommand = new Command(( ) => { return; }, ( ) => IsBluetoothConnected); // execute parameter is a no-op since I really just want the canExecute parameter
            IList<User> users = await App.DB.GetUsersAsync();
            Users = new ObservableCollection<User>(users.OrderBy(user => user.Username));
            return Users.Count;
        }
    }
    

    I'm still not entirely happy with this; it contaminates the view model with the concerns of a specific view. I'm going to see if I can separate the Command from the view model. But it does accomplish my primary goal, bringing this UI implementation into the data binding paradigm.