« Home | Silverlight 2: MVVM, Databinding & Cascading Combo... » | Google App Engine - Tetris Challenge » | IIS, SSL and Host-Headers » | Amazon EC2 - Now with Windows Server 2003! » | Playing with JQuery and ASP.NET MVC » | Converting Timezones in .net » | ASP.NET Defending Against Form Hackers » | Release Branches and Bugfixes with Subversion » | Amazon Elastic Compute Cloud - EC2 » | PropertyInfo.GetCustomAttributes() Doesn't Return ... » 

Wednesday, February 18, 2009 

Silverlight 2: MVVM, Databinding & Cascading ComboBoxes - Part 2

So I've finally got a fix for this issue. The solution does feel like a bit of a hack - but I can understand why I need to go down this path. Hopefully one of the people who read this post will be inspired to come up with a more elegant solution!

See a demo of 'cascading ComboBoxes in Silverlight 2', and get the source here: CitiesVisited_step3.zip.

I finished the last post with two possible solutions. The first option was to bind the 'cities' ComboBox to a collection containing a union of all the possible cities - and toggling a visibility property as the country changes. I attempted to code this, but it had two strange side effects. The text part of the drop down, e.g. just where it has 'Melbourne', became unresponsive to mouse clicks. And the ComboBoxListItems with Visibility toggles to collapsed still appeared in the ComboBox empty and a few pixels tall.

The second option was two combine the previous/new lists of cities just at the time when Visit.City is being databound, then removed all previous cities. This approach I did get to work!

The PageViewModel gets two new properties BindCity and BindableCities bound to the cities ComboBox. BindCity is just a wrapper for _SelectedVisit.City, this exists so I can control when the UI is notified about the city change.

public City BindCity
{
    get { return _SelectedVisit.City; }
    set
    {
        _SelectedVisit.City = value;
        OnPropertyChanged("BindCity");
    }
}


public ObservableCollection<City> BindableCities
{
    get { return _BindableCities; }
}
<ComboBox 
    ItemsSource="{Binding BindableCities}" 
    SelectedItem="{Binding BindCity, Mode=TwoWay}" 
    DisplayMemberPath="Name"/>

The SelectedVisit setter is now doing a lot more. There are two scenarios where I need to update the BindableCities collection: when the SelectedVisit is changed (by clicking on a left-hand visit), and when the Country ComboBox is changed. When a new SelectedVisit is set by the databinding, the SwitchBindableCities method populates the BindableCities with the combination of cities. Here I'm also attaching to the PropertyChanged event of the SelectedVisit - to populate the BindableCities when the Country ComboBox changes.

public Visit SelectedVisit
{
    get { return _SelectedVisit; }
    set
    {
        if (_SelectedVisit != null)
        {
            _SelectedVisit.PropertyChanged -= new PropertyChangedEventHandler(_SelectedVisit_PropertyChanged);
        }
        _SelectedVisit = value;
        _SelectedVisit.PropertyChanged += new PropertyChangedEventHandler(_SelectedVisit_PropertyChanged);

        SwitchBindableCities(true);
        OnPropertyChanged("SelectedVisit");
    }
}

void _SelectedVisit_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
    // the country property of the visit has changed
    // update the bindable cities
    if (e.PropertyName == "Country")
    {
        SwitchBindableCities(false);
    }
}

Where it all happens. The SwitchBindableCities method:

  1. First 'if': we've arrived here from someone changing a Country drop down, set the _SelectedVisit.City to null. E.g someone changing from Australia/Melbourne to United States. The City=Melbourne gets set to null.
  2. Second 'if': we've arrived here from someone toggling between Visits. If the BindableCities collection already contains what we want - it is save to allow the data binding to happen, then we can exit with the BindableCities untouched.
  3. The remainder of the method changes the list of cities. First it stores the list of cities presently in BindableCities. Adds the 'new' cities to the collection. Then raises the property changed on BindCity. The UI now does the binding when the BindableCities collection contains the UNION of cities. Now its safe to remove the original list of cities from BindableCities.
private void SwitchBindableCities(bool visitChanged)
{
    if (!visitChanged)
    {
        // changed the country, and the visit city isn't in that
        // country - so set the city to null
        _SelectedVisit.City = null;
    }

    if (visitChanged && _BindableCities.Contains(_SelectedVisit.City))
    {
        // the selected vist city IS in the selected country
        // and already in the bindable cities
        OnPropertyChanged("BindCity");
        return;
    }

    List<City> toBeRemoved = _BindableCities.ToList();
    foreach (City c in _SelectedVisit.Country.Cities)
    {
        _BindableCities.Add(c);
    }

    // tell the UI that the bind city has changed
    OnPropertyChanged("BindCity");

    foreach (City c in toBeRemoved)
    {
        _BindableCities.Remove(c);
    }
}

Labels: ,