Cannot update subitems ListBox

141 views Asked by At

I have two ListBoxes. One ListBox displays some items. Each item has a list of subitems. The second ListBox displays the subitems of the current item in the first ListBox. A typical item/subitem scenario. When I add a value to the subitem list, I cannot get the second ListBox to update. How can I force the second ListBox to update my subitems?

My simplified XAML and view model are below. Note that my view model inherits from Prism's BindableBase. In my view model I have a method to add a value to the subitems list and update the Items property with RaisePropertyChanged.

<ListBox Name="Items" ItemsSource="{Binding Items}" >
    <ListBox.ItemsPanel>
        <ItemsPanelTemplate>
            <VirtualizingStackPanel />
        </ItemsPanelTemplate>
    </ListBox.ItemsPanel>

    <ListBox.ItemTemplate>
        <DataTemplate>            
                <Label Content="{Binding ItemValue, Mode=TwoWay}" />            
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

<ListBox Name="SubItems" ItemsSource="{Binding Items.CurrentItem.SubItems}" >
    <ListBox.ItemsPanel>
        <ItemsPanelTemplate>
            <VirtualizingStackPanel />
        </ItemsPanelTemplate>
    </ListBox.ItemsPanel>

    <ListBox.ItemTemplate>
        <DataTemplate>
            <Label Content="{Binding}" />
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>
class Item
{
    public int ItemValue { get; set; }
    public List<int> SubItems { get; set; }
}

class MyViewModel : BindableBase
{
    private ObservableCollection<Item> _items = new ObservableCollection<Item>();        

    public MyViewModel()
    {
        List<Item> myItems = new List<Item>() {  /* a bunch of items */ };

        _items = new ObservableCollection<Item>(myItems);
        Items = new ListCollectionView(_items);
    }

    public ICollectionView Items { get; private set; }

    private void AddNewSubItem(object obj)
    {
        Item currentItem = Items.CurrentItem as Item;

        currentItem.SubItems.Add(123);            

        RaisePropertyChanged("Items");
    }
}
2

There are 2 answers

1
thatguy On BEST ANSWER

The issue is that your Item type contains a List<int>, which does not notify on collection changes, because it does not implement the INotifyCollectionChanged interface. Consequently, bindings do not get notified to update their values in the user interface when the collection is modified. Using a collection that implements this interface, e.g. an ObservableCollection<int> will solve the issue.

By the way, the same applies to ItemValue and SubItems properties as well with INotifyPropertyChanged. Both properties have setters, which means they can be reassigned, but the bindings will not get notified then either. Since you use BindableBase, you can use the SetProperty method with backing fields to automatically raise the PropertyChanged event when a property is set.

public class Item : BindableBase
{
   private int _itemValue;
   private ObservableCollection<int> _subItems;

   public int ItemValue
   {
      get => _itemValue;
      set => SetProperty(ref _itemValue, value);
   }

   public ObservableCollection<int> SubItems
   {
      get => _subItems;
      set => SetProperty(ref _subItems, value);
   }
}

A remark about your project. What you try to do is a master-detail view for hierarchical data. Since you already expose an ICollectionView, you do not have to use the CurrentItem explicitly.

When the source is a collection view, the current item can be specified with a slash (/). For example, the clause Path=/ sets the binding to the current item in the view. When the source is a collection, this syntax specifies the current item of the default collection view.

There is a special binding syntax, where you can use a / to indicate the current item, e.g.:

<ListBox Grid.Row="1" Name="SubItems" ItemsSource="{Binding Items/SubItems}">

Additionally a few remarks about your view model code.

  • The _items field initializer is useless, as you reassign the field in the constructor anyway.
  • If you only use the collection view and not _items, you can use a local variable in the constructor.
  • The private setter of Items is not used, you can remove it.
  • Cast the Items.CurrentItem directly to Item to get an invalid cast exception that is more specific that the null reference exception that you would get later when accessing the value from using as and getting null. If you expect that the value could be something else than an Item, you could check for null after as or use pattern matching to handle both cases, success and error (CurrentItem is not of type Item) appropriately.
0
EldHasp On
class Item
{
    public int ItemValue { get; set; }
    public ObservableCollection<int> SubItems { get;}
      = new ObservableCollection<int>();
}
class MyViewModel : BindableBase
{
    publc ObservableCollection<Item> Items {get;}
        = new ObservableCollection<Item>();        

    public MyViewModel()
    {
        List<Item> myItems = new List<Item>() {  /* a bunch of items */ };

        myItems.ForEach(item => Items.Add(item));
    }

    private Item _currentItem;
    private Item CurrentItem
    {
       get => _currentItem;
       set
       {
          if(Equals(_currentItem, value))
            return;

          _currentItem = value;
          RaisePropertyChanged(nameof(CurrentItem));
       }
    }

    private void AddNewSubItem(object obj)
    {
        CurrentItem.SubItems.Add(123);            
    }
}
<ListBox ItemsSource="{Binding Items}" 
         SelectedItem="{Binding CurrentItem}">
  ********
  ********
</ListBox>

<ListBox ItemsSource="{Binding CurrentItem.SubItems}" >
  ********
  ********
</ListBox>