Search code examples
wpfcomboboxitemssourceinotifycollectionchangedupdatesourcetrigger

WPF, Update ComboBox ItemsSource when it's DataContext changes


I have two classes A and B which both implement an interface IThingWithList.

public interface IThingWithList
{
  ObservableCollection<int> TheList;
}

TheList in A contains 1, 2, 3 TheList in B contains 4, 5, 6

I have a controller class which has a list of IThingWithList which contains A and B

public class MyControllerClass
{
  public ObservableCollection<IThingWithList> Things { get; } = new ObservableCollection<IThingWithList>() { A, B };

  public IThingWithList SelectedThing { get; set; }
}

Now, in xaml I have two ComboBoxes as follows

<ComboBox
  ItemsSource="{Binding MyController.Things}"
  SelectedValue="{Binding MyController.SelectedThing, Mode=TwoWay}" />

<ComboBox
  DataContext="{Binding MyController.SelectedThing}"
  ItemsSource="{Binding TheList}" />

The first ComboBox controls which (A or B) is the data context of the second combo box.

Problem:

When I select A or B from the first ComboBox The list items of the second ComboBox are not updated.

What I have tried:

Making both A and B ObservableObjects

Making IThingWithList implement INotifyPropertyChanged

Adding UpdateSourceTrigger to the ItemsSource Bindings

Scouring Google.


Solution

  • Here is how I typically do the ViewModel (in your case "Controller") Base Class in order to get the functionality you are looking for:

    This is the base class that all VMs derive from.

    public class ViewModelBase : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        protected void RaisePropertyChanged([CallerMemberName] string propertyName = "")
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    
        protected void SetAndNotify<T>(ref T property, T value, [CallerMemberName] string propertyName = null)
        {
            if (Equals(property, value))return;
    
            property = value;
            this.OnPropertyChanged(propertyName);
        }
    
    }
    

    Here is how I would adjust your ControllerClass:

    public class MyControllerClass : ViewModelBase
    {
        private ObservableCollection<IThingWithList> _things;
        public ObservableCollection<IThingWithList> Things
        {
            get => _things;
            set { SetAndNotify(ref _things, value); }
        }
    
        private IThingWithList _selectedthing;
        public IThingWithList SelectedThing
        {
            get => _selectedThing;
            set{SetAndNotify(ref _selectedThing, value);}
        }
    }
    

    Now adjust your XAML to have the DataContext of the container set instead of each Control (makes life easier)

    <UserControl xmlns:local="clr-namespace:YourMainNamespace">
    <UserControl.DataContext>
      <local:MyControllerClass/>
    </UserControl.DataContext>
      <StackPanel>
        <ComboBox
          ItemsSource="{Binding Things}"
          SelectedValue="{Binding SelectedThing, Mode=TwoWay}" />
    
        <!-- ComboBox ItemsSource="See next lines" /-->
      </StackPanel>
    </Window>
    

    You can change SelectedThing to be an ObservableCollection or you can have a second object that is the list and updates accordingly:

    //Add into MyControllerClass
    
    public MyInnerThingList => SelectedThing.TheList;
    
    //Edit the SelectedThing to look like:
    private IThingWithList _selectedthing;
    public IThingWithList SelectedThing
    {
        get => _selectedThing;
        set
        {
            SetAndNotify(ref _selectedThing, value);           
            RaisePropertyChanged(nameof(MyInnerThingList));
        }
    }
    

    Then change the binding to: <ComboBox ItemsSource="{Binding MyInnerThingList, Mode=OneWay}" />

    You may also want to add a SelectedMyInnerThing property, but not sure if that is needed.