1

In attempting to bind a IsSelectable INPC Property via a setter in a Listview based on this answer. The said answer is for a ComboBoxItem which works great, im trying to refactor for a ListViewItem which is not working.

Setting IsSelectable true or false on any item in the ViewModel.ItemsOC doenst work as the list view item is still selectable. What am I missing?

XAML

<ListView
    ItemsSource="{x:Bind ViewModel.ItemsOC, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"      
    SelectedItem="{x:Bind ViewModel.SelectedItem, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
    SelectionChanged="LV_Item_SelectionChanged">

    <ListView.Resources>
        <Style BasedOn="{StaticResource DefaultListViewItemStyle}" TargetType="ListViewItem">
            <Setter Property="cus_ctrls:LVI_BindingHelper.IsEnable" Value="IsSelectable" />
        </Style>
    </ListView.Resources>

    <ListView.ItemTemplate>
        <DataTemplate>

            <TextBlock Text="{Binding Name, UpdateSourceTrigger=PropertyChanged}" />  
      
        </DataTemplate>
    </ListView.ItemTemplate>
</ListView>

Binding Helper

public class LVI_BindingHelper
{
    public static string GetIsEnable(DependencyObject obj)
    {
        return (string)obj.GetValue(IsEnableProperty);
    }
    public static void SetIsEnable(DependencyObject obj, string value)
    {
        obj.SetValue(IsEnableProperty, value);
    }

    // Using a DependencyProperty as the backing store for IsEnable.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty IsEnableProperty =
        DependencyProperty.RegisterAttached("IsEnable", typeof(string), typeof(LVI_BindingHelper), new PropertyMetadata(null, GridBindingPathPropertyChanged));

    private static void GridBindingPathPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
    {
        var propertyPath = e.NewValue as string;
        if (propertyPath != null)
        {
            var bindingProperty =
                e.Property == IsEnableProperty
                ? ListViewItem.IsEnabledProperty
                : null;
            BindingOperations.SetBinding(
                obj,
                bindingProperty,
                new Binding { Path = new PropertyPath(propertyPath) });
        }
    }
}

ViewModel

bool _isSelectable;
public bool IsSelectable
{
    get => _isSelectable;
    set => Set(ref _isSelectable, value);
}
tinmac
  • 2,357
  • 3
  • 25
  • 41

2 Answers2

2

A plain binding should do. Check this sample code:

Item.cs

public partial class Item : ObservableObject
{
    [ObservableProperty]
    private string name = string.Empty;

    [ObservableProperty]
    private bool isSelectable;
}

MainPageViewModel.cs

public partial class  MainPageViewModel : ObservableObject
{
    [ObservableProperty]
    private ObservableCollection<Item> items = new();

    [ObservableProperty]
    private Item? selectedItem;

    public MainPageViewModel()
    {
        for (int i = 0; i < 100; i++)
        {
            Items.Add(new Item
            {
                Name = $"Item {i}",
                IsSelectable = i % 5 is not 0,
            });
        }
    }

    [RelayCommand]
    private void MakeAllItemsSelectable()
    {
        foreach (Item item in Items)
        {
              item.IsSelectable = true;
        }
    }
}

MainPage.xaml.cs

public sealed partial class MainPage : Page
{
    public MainPage()
    {
        this.InitializeComponent();
    }

    public MainPageViewModel ViewModel { get; } = new();
}

MainPage.xaml

<Grid RowDefinitions="Auto,*">
    <Button
        Grid.Row="0"
        Command="{x:Bind ViewModel.MakeAllItemsSelectableCommand}"
        Content="Click" />
    <ListView
        Grid.Row="1"
        ItemsSource="{x:Bind ViewModel.Items, Mode=OneWay}"
        SelectedItem="{x:Bind ViewModel.SelectedItem, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
        <ListView.ItemTemplate>
            <DataTemplate x:DataType="local:Item">
                <ListViewItem IsEnabled="{x:Bind IsSelectable, Mode=OneWay}">
                    <TextBlock Text="{Binding Name, UpdateSourceTrigger=PropertyChanged}" />
                </ListViewItem>
            </DataTemplate>
        </ListView.ItemTemplate>
    </ListView>
</Grid>

NOTE

I'm using the CommunitToolkit.Mvvm NuGet package for the MVVM pattern.

UPDATE

Since it seems that you can't touch the ItemTemplate, I guess you'll need to use the AttachedProperty anyway. I confirmed that the code below works:

ListViewItemIBindingHelper.cs

public class ListViewItemIBindingHelper
{
    public static string GetIsEnabled(DependencyObject obj)
        => (string)obj.GetValue(IsEnabledProperty);

    public static void SetIsEnabled(DependencyObject obj, string value)
        => obj.SetValue(IsEnabledProperty, value);

    public static readonly DependencyProperty IsEnabledProperty =
        DependencyProperty.RegisterAttached(
            "IsEnabled",
            typeof(string),
            typeof(ListViewItemIBindingHelper),
            new PropertyMetadata(string.Empty, OnIsEnabledPropertyChanged));

    private static void OnIsEnabledPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (d is not ListViewItem listViewItem ||
            e.NewValue is not string bindingPath)
        {
            return;
        }

        listViewItem.RegisterPropertyChangedCallback(
            ListViewItem.ContentProperty,
            (_, _) =>
            {
                if (listViewItem.Content is not Item item)
                {
                    return;
                }

                listViewItem.SetBinding(
                    ListViewItem.IsEnabledProperty,
                    new Binding
                    {
                        Source = item,
                        Path = new PropertyPath(bindingPath),
                    });
            });
    }
}
<ListView ItemsSource="{x:Bind ViewModel.Items, Mode=OneWay}">
    <ListView.ItemContainerStyle>
        <Style TargetType="ListViewItem">
            <Setter Property="local:ListViewItemIBindingHelper.IsEnabled" Value="IsSelectable" />
        </Style>
    </ListView.ItemContainerStyle>
</ListView>
Andrew KeepCoding
  • 7,040
  • 2
  • 14
  • 21
  • Thank you so much for this answer. Unfortunatley the ItemTemplate is off somewhere in a resourses file that im not allowed to change. App is being ported from WPF hence the setter issue. Im looking for a way to drop in some helpers & small amout of xaml as there are lots of ListViews to refactor. – tinmac Aug 17 '23 at 08:45
  • You mean you can't change `Item` class? or you really mean the `ListView.ItemTemplate`? – Andrew KeepCoding Aug 17 '23 at 08:48
  • `ListView.ItemTemplate = StaticResource {lv_aux_qwop_1877}` originally ommited to keep example more readable as originally I was hoping for help with the binding helper but must thankyou again for your answer, im certain it will help others, sadly not me this time. – tinmac Aug 17 '23 at 17:33
  • Updated my answer using the `AttachedProperty`. – Andrew KeepCoding Aug 18 '23 at 08:14
  • Thankyou it works. Appreciate all your help – tinmac Aug 18 '23 at 08:36
1

For each Item (ViewModel) in ItemsSource, a ListViewItem is created as followed: (at least if ListViewItem isn't part of the itemtemplate)

  • ListViewItem.Content = Item (ViewModel)
  • ListViewItem.ContentTemplate = ListView.ItemTemplate

The root elements created from the ItemTemplate will get their DataContext from ListViewItem's Content property, making them and all children data bindable.

But ListViewItem itself has no DataContext set, just Content. ComboBoxItem's do get both DataContext and Content set to Item (ViewModel), that's why it works for ComboBoxItem.

Though it can't bind with a path alone, it can still bind using a relative source, change the last part of your binding code inside your BindingHelper to:

  // bind ListViewItem.IsEnabled to ListViewItem(self) -> Content(=ViewModel) -> propertyPath(=IsSelectable)
  BindingOperations.SetBinding(
      obj, bindingProperty,
      new Binding {
    RelativeSource = new RelativeSource() { Mode=RelativeSourceMode.Self }, 
    Path = new PropertyPath("Content." + propertyPath) });
user7994388
  • 448
  • 3
  • 6
  • Thankyou it works. Thanks also for the explanation on how the internals work & difference between ComboBox and Listiew, very helpful to know. – tinmac Aug 18 '23 at 08:32