question

DonRea-6311 avatar image
0 Votes"
DonRea-6311 asked DonRea-6311 commented

Xamarin.Forms: Unable to get a ListView template property to bind outside the list item

I have a ListView bound to a collection of objects (called User, 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 Cell s in the TemplatedItems property and setting IsEnabled on each.

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

 <ListView x:Name="usersListView" 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 a login account). 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 each time the value is set it will be the same for every item in the collection.

I read about relative binding and tried placing the collection of User as a property of a view model class called UsersViewModel and also added a boolean property called IsBluetoothConnected to the view model, and set that class as the BindingContext of the ListView usersListView.

 public class UsersViewModel
 {
     public bool IsBluetoothConnected = false;
     public ObservableCollection<User> Users { get; private set; }
    
     public async System.Threading.Tasks.Task<int> Populate( )
     {
         IList<User> users = await App.DB.GetUsersAsync();
         Users = new ObservableCollection<User>(users.OrderBy(user => user.Username));
         return Users.Count;
     }
 }

Updated XAML:

 <ListView x:Name="usersListView" ItemsSource="{Binding Users}" ItemTapped="item_Tap">
     <ListView.ItemTemplate>
         <DataTemplate>
             <TextCell Text="{Binding Label}">
                 <TextCell.ContextActions>
                     <MenuItem
                         Text="Copy to other device"
                         Clicked="copyMenuItem_Click"
                         IsEnabled="{various binding syntax attempted here, see below}" />
                 </TextCell.ContextActions>
             </TextCell>
         </DataTemplate>
     </ListView.ItemTemplate>
 </ListView>


That's all well and good as far as displaying the users, that still works great, but I'm still not able to get the IsEnabled property to bind. These are the variations of MenuItem.IsEnabled binding syntax I have tried so far, all with the same result: Builds and runs without error, but even though UsersViewModel.IsBluetoothConnected is defined as false for testing, the MenuItem is enabled.

 {Binding Path=BindingContext.IsBluetoothConnected, Source={x:Reference usersListView}}
 {Binding BindingContext.IsBluetoothConnected, Source={x:Reference usersListView}}
 {Binding Path=IsBluetoothConnected, Source={x:Reference usersListView}}
 {Binding IsBluetoothConnected, Source={x:Reference usersListView}}
 {Binding Path=BindingContext.IsBluetoothConnected, Source={RelativeSource AncestorType={x:Type ListView}}}
 {Binding BindingContext.IsBluetoothConnected, Source={RelativeSource AncestorType={x:Type ListView}}}
 {Binding Path=BindingContext.IsBluetoothConnected, Source={RelativeSource Mode=FindAncestor, AncestorType={x:Type ListView}}}
 {Binding BindingContext.IsBluetoothConnected, Source={RelativeSource Mode=FindAncestor, AncestorType={x:Type ListView}}}
 {Binding Path=IsBluetoothConnected, Source={RelativeSource AncestorType={x:Type ListView}}}
 {Binding IsBluetoothConnected, Source={RelativeSource AncestorType={x:Type ListView}}}
 {Binding Path=IsBluetoothConnected, Source={RelativeSource Mode=FindAncestor, AncestorType={x:Type ListView}}}
 {Binding IsBluetoothConnected, Source={RelativeSource Mode=FindAncestor, AncestorType={x:Type ListView}}}
 {Binding Path=BindingContext.IsBluetoothConnected, Source={RelativeSource Mode=FindAncestorBindingContext, AncestorType={x:Type ListView}}}
 {Binding BindingContext.IsBluetoothConnected, Source={RelativeSource Mode=FindAncestorBindingContext, AncestorType={x:Type ListView}}}
 {Binding Path=IsBluetoothConnected, Source={RelativeSource Mode=FindAncestorBindingContext, AncestorType={x:Type ListView}}}
 {Binding IsBluetoothConnected, Source={RelativeSource Mode=FindAncestorBindingContext, AncestorType={x:Type ListView}}}

Clearly there is something I don't yet understand. What do I need to do or change to make this boolean property binding work?











dotnet-xamarin
5 |1600 characters needed characters left characters exceeded

Up to 10 attachments (including images) can be used with a maximum of 3.0 MiB each and 30.0 MiB total.

1 Answer

JarvanZhang-MSFT avatar image
1 Vote"
JarvanZhang-MSFT answered DonRea-6311 commented

Hello,​

Welcome to our Microsoft Q&A platform!

The MenuItem.IsEnabled is for internal use by the Xamarin.Forms platform. Changing the value to false in xaml will cause the below exception.

System.InvalidOperationException: The BindableProperty "IsEnabled" is readonly.

To enable or disable a MenuItem at runtime, try to bind its Command property to an ICommand implementation, and ensure that a canExecute delegate enables and disables the ICommand as appropriate. For more details, you could refer to this doc.

Here is the sample code:

<ListView x:Name="listView" ItemsSource="{Binding DataCollection}">
    <ListView.ItemTemplate>
        <DataTemplate>
            <TextCell Text="{Binding Title}">
                <TextCell.ContextActions>
                    <MenuItem 
                        Text="More" 
                        Command="{Binding BindingContext.ItemCommand}" BindingContext="{x:Reference listView}"/>
                </TextCell.ContextActions>
            </TextCell>
        </DataTemplate>
    </ListView.ItemTemplate>
</ListView>

public partial class TestPage : ContentPage
{
    TestViewModel viewModel;
    public Page1()
    {
        InitializeComponent();

        viewModel = new TestViewModel();
        BindingContext = viewModel;
    }

    private void Button_Clicked(object sender, EventArgs e)
    {
        viewModel.IsConnected = !viewModel.IsConnected;//change the property will disable/enable the menuItem at runtime
    }
}

ViewModel class

public class TestViewModel : INotifyPropertyChanged
{
    public ObservableCollection<TestModel> DataCollection { get; set; }
    public Command ItemCommand { get; set; }

    private bool isConnected;
    public bool IsConnected
    {
        get { return isConnected; }
        set
        {
            if (isConnected != value)
            {
                isConnected = value;
                ItemCommand.ChangeCanExecute();
                NotifyPropertyChanged("IsConnected");
            }
        }
    }

    public TestViewModel()
    {
        DataCollection = new ObservableCollection<TestModel>();

        ItemCommand = new Command(() =>
        {
            App.Current.MainPage.DisplayAlert("title", "menu item is clicked", "ok");
        }, () => IsConnected);
    }

    
    protected virtual void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
    public event PropertyChangedEventHandler PropertyChanged;
}


Best Regards,

Jarvan Zhang


If the response is helpful, please click "Accept Answer" and upvote it.

Note: Please follow the steps in our documentation to enable e-mail notifications if you want to receive the related email notification for this thread.


· 2
5 |1600 characters needed characters left characters exceeded

Up to 10 attachments (including images) can be used with a maximum of 3.0 MiB each and 30.0 MiB total.

OK, this looks like it's putting me on the right track, except how do I know what state ItemCommand is in when we come into IsConnected.Set? I need to make sure the executability matches the boolean value, and just toggling and hoping it's right doesn't seem like a good approach. I've tried calling CanExecute() but it wants a parameter and I see nothing in the doc helping to understand what that parameter should be.


0 Votes 0 ·

OK, I get it: the parameter passed to CanExecute is the same as that passed to the function passed as the execute parameter to the Command constructor, in this case, null.

1 Vote 1 ·