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?...