Share via


Cascading ComboBoxes in WPF using MVVM

When a user is selecting an item from a cascading ComboBox, another ComboBox gets automatically populated with items based on the selection in the first one. This article is about how you can implement this behaviour in a WPF application using the MVVM (Model-View-ViewModel) pattern.

Assume that you have the following two classes which may represent entities in an Entity Framework-based application or some other domain objects, and where there is a one-to-many relationship between a country and a city meaning a city must belong to a single country and a country can have several cities:

 

      public class  Country
      {  
                  public string  Name {  
                    get      ;      
                    set      ;      
                  }      
              
                  public string  CountryCode {  
                    get      ;      
                    set      ;      
                  }      
              
                  public ICollection<City> Cities {  
                    get      ;      
                    set      ;      
                  }      
      }  
              
      public class  City
      {  
                  public string  Name {  
                    get      ;      
                    set      ;      
                  }      
              
                  public Country Country {  
                    get      ;      
                    set      ;      
                  }      
      }  

The view model – an instance of this class is set as the DataContext for the window in which the ComboBoxes will be displayed – will have properties with public getters that return collections of countries and cities respectively. It will also have properties for keeping track of the currently selected items in the two collections. Note that the setter of the SelectedCountry property is responsible for populating the Cities collection based on the selection in the country ComboBox.

using System.Collections.Generic;
using System.ComponentModel;
  
namespace Mm.CascadingComboBoxes
{
  public class  ViewModel : INotifyPropertyChanged
  {
    public ViewModel() {
      this.Countries = new  List<Country>()
      {
        new Country(){ Name = "United Kingdom", CountryCode = "GB",
        Cities = new  List<City>()
        {
          new City(){ Name = "London" },
          new City(){ Name = "Birmingham" },
          new City(){ Name = "Glasgow" }
        }},
        new Country(){ Name = "USA", CountryCode = "US",
        Cities = new  List<City>()
        {
          new City(){ Name = "Los Angeles" },
          new City(){ Name = "New York" },
          new City(){ Name = "Washington" }
        }},
        new Country(){ Name = "Sweden", CountryCode = "SE",
        Cities = new  List<City>()
        {
          new City(){ Name = "Stockholm" },
          new City(){ Name = "Göteborg" },
          new City(){ Name = "Malmö" }
        }}
      };
  
      //set default country selection:
      this.SelectedCountry = this.Countries[0];
    }
  
    public IList<Country> Countries {
      get;
      private set;
    }
  
    public ICollection<City> Cities {
      get;
      private set;
    }
  
    private Country _selectedCountry;
    public Country SelectedCountry {
      get {
        return _selectedCountry;
      }
      set {
        _selectedCountry = value;
        OnPropertyChanged("SelectedCountry");
        this.Cities = _selectedCountry.Cities;
        OnPropertyChanged("Cities");
      }
    }
  
    public City SelectedCity {
      get;
      set;
    }
  
    #region INotifyPropertyChanged Members
    public event  PropertyChangedEventHandler PropertyChanged;
    private void  OnPropertyChanged(string name) {
      PropertyChangedEventHandler handler = PropertyChanged;
      if (handler != null)
        handler(this, new  PropertyChangedEventArgs(name));
    }
    #endregion
  }
}

The view model above implements the System.ComponentModel.INotifyPropertyChanged interface to be able to automatically notify the view when the collection of cities has been updated. An alternative approach to raising the PropertyChanged event when this happens is to use a collection that implements the System.Collections.Specialized.INotifyCollectionChanged interface. WPF provides the System.Collections.ObjectModel.ObservableCollection<T> class for this but using it like below would cause the CollectionChanged event to get fired multiple times when replacing all items in the collection:

public ObservableCollection<Country> Countries {
  get;
  private set;
}
  
private Country _selectedCountry;
public Country SelectedCountry {
   get {
    return _selectedCountry;
   }
   set {
    _selectedCountry = value;
    /* WARNING: The following code causes the CollectionChanged 
         event to get fired multiple times! */
    this.Cities.Clear();
    foreach (City city in _selectedCountry.Cities)
      this.Cities.Add(city);
  }
}

To avoid this you can implement your own subclass of ObservableCollection<T> to extend it with a method for replacing all items and raise the event only once and then use this custom class as the return type for the collection of cities in the view model:

using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Collections.Specialized;
  
namespace Mm.CascadingComboBoxes
{
  public class  CustomObservableCollection<T> : ObservableCollection<T>
  {
    public CustomObservableCollection()
      : base() {
    }
  
    public CustomObservableCollection(IEnumerable<T> collection)
      : base(collection) {
    }
  
    public CustomObservableCollection(List<T> list)
      : base(list) {
    }
  
    public void  Repopulate(IEnumerable<T> collection) {
      this.Items.Clear();
      foreach (var item in collection)
        this.Items.Add(item);
  
      this.OnPropertyChanged(new PropertyChangedEventArgs("Count"));
      this.OnPropertyChanged(new PropertyChangedEventArgs("Item[]"));
      this.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
    }
  }
}
  
namespace Mm.CascadingComboBoxes
{
  public class  ViewModel
  {
    public ViewModel() {
      this.Countries = new  List<Country>()
      {
        ...
      };
  
      this.Cities = new  CustomObservableCollection<City>();
      ...
    }
  
    public CustomObservableCollection<City> Cities {
      get;
      private set;
    }
  
    private Country _selectedCountry;
    public Country SelectedCountry {
      get {
        return _selectedCountry;
      }
      set {
        _selectedCountry = value;
        this.Cities.Repopulate(_selectedCountry.Cities);
      }
    }
    ...
  }
}

Regardless of which of these approaches you choose to use, the XAML markup for the view looks the same. The ItemsSource properties of the ComboBoxes are bound to the collection properties of the view model and the SelectedItem property of the cascading ComboBox with the country names is bound to the SelectedCountry property of the view model:

<Window x:Class="Mm.CascadingComboBoxes.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Choose your city..." Height="150" Width="350">
  <StackPanel Margin="10">
    <ComboBox ItemsSource="{Binding Countries}"
              DisplayMemberPath="Name"
              SelectedItem="{Binding SelectedCountry}" />
      
    <ComboBox ItemsSource="{Binding Cities}"
              DisplayMemberPath="Name"
              SelectedItem="{Binding SelectedCity}"
              Margin="0 5 0 0"/>
  </StackPanel>
</Window>

http://magnusmontin.files.wordpress.com/2013/06/comboboxes.png

Note that in this particular example, since a country object always has access to its related cities and the view model doesn’t need to call out to a service or business layer to get the child objects, you could in fact omit the collection of cities from the view model if you use the class that implements the INotifyPropertyChanged interface and bind the ComboBox that displays the cities in the view directly to the Cities collection of the selected country object:

<ComboBox ItemsSource="{Binding SelectedCountry.Cities}"
          DisplayMemberPath="Name"
          SelectedItem="{Binding SelectedCity}"
          Margin="0 5 0 0"/>

.The solution described in this article was partly derived from my answer in the following thread on the MSDN forums: Help in synchronizing three Combo boxes?...