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; } }
public
class
Country
{
string
Name {
get
;
set
}
CountryCode {
ICollection<City> Cities {
City
Country Country {
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;
System.ComponentModel;
namespace
Mm.CascadingComboBoxes
ViewModel : INotifyPropertyChanged
ViewModel() {
this
.Countries =
new
List<Country>()
Country(){ Name =
"United Kingdom"
, CountryCode =
"GB"
,
Cities =
List<City>()
City(){ Name =
"London"
},
"Birmingham"
"Glasgow"
}},
"USA"
"US"
"Los Angeles"
"New York"
"Washington"
"Sweden"
"SE"
"Stockholm"
"Göteborg"
"Malmö"
}}
};
//set default country selection:
.SelectedCountry =
.Countries[0];
IList<Country> Countries {
private
Country _selectedCountry;
Country SelectedCountry {
return
_selectedCountry;
_selectedCountry = value;
OnPropertyChanged(
"SelectedCountry"
);
.Cities = _selectedCountry.Cities;
"Cities"
City SelectedCity {
#region INotifyPropertyChanged Members
event
PropertyChangedEventHandler PropertyChanged;
void
name) {
PropertyChangedEventHandler handler = PropertyChanged;
if
(handler !=
null
)
handler(
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:
ObservableCollection<Country> Countries {
/* WARNING: The following code causes the CollectionChanged
event to get fired multiple times! */
.Cities.Clear();
foreach
(City city
in
_selectedCountry.Cities)
.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:
System.Collections.ObjectModel;
System.Collections.Specialized;
CustomObservableCollection<T> : ObservableCollection<T>
CustomObservableCollection()
:
base
() {
CustomObservableCollection(IEnumerable<T> collection)
(collection) {
CustomObservableCollection(List<T> list)
(list) {
Repopulate(IEnumerable<T> collection) {
.Items.Clear();
(var item
collection)
.Items.Add(item);
.OnPropertyChanged(
PropertyChangedEventArgs(
"Count"
));
"Item[]"
.OnCollectionChanged(
NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
ViewModel
...
.Cities =
CustomObservableCollection<City>();
CustomObservableCollection<City> Cities {
.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}"
/>
"{Binding Cities}"
"{Binding SelectedCity}"
"0 5 0 0"
</
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:
"{Binding SelectedCountry.Cities}"
.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?...