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: ,

Tuesday, February 17, 2009 

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

Update: see follow up post to see how I got the cascading ComboBoxes to work: I did mange to get a 'working' solution for cascading ComboBoxes - see the solution here: Silverlight 2: MVVM, Databinding & Cascading ComboBoxes - Part 2

I'm presently working on a Silverlight 2 application at work, and really enjoying the getting into the new technology. Hitting a few issues along the way that are going to make good blog articles when I find the time. This article intends to be the first in a series to demonstrate some of the issues I'm discovering.

Issue no. 1: Databinding Cascading ComboBoxes to a ViewModel.

If you've been following Silverlight 2 developer blogs you'll notice Model-View-ViewModel (MVVM) is a popular separation pattern for UI. Luckily Craig of ConceptDev has already pulled together a good list of links for those wanting to read up on MVVM: Silverlight + Model-View-ViewModel (MVVM). My first introduction to MVVM was the Divelog app from Jonas Follesø. The Divelog app is a good introduction to several concepts: silverlight unit tests, dependency injection, command pattern and MVVM

The actual problem. Let's say we are building an app of all the cities you've visited around the world. The Model classes involved: A Country class contains a collection of Cities, and a Visit class contains references to a Country and a City. The PageViewModel contains: Countries collection of all countries, Visits collection of all visit, and a SelectedVisit property.

The databinding seems simple...

...but clicking on the 2nd visit causes the city on the first option to disappear. See a demo of the issue: here.

Anything related to databinding issues is best understood by attaching some 'do nothing' Converter classes. Inside the Converter I've added debugging statements to try work out what order things are happening. The debugging output from the Converters looks like this:

The statements after the red line were created after the second option was clicked. This is how I interpreted the output after the red line:

  1. The ItemSelected property of the countries ComboBox is set to 'United States'
  2. The ItemSource property of the cities ComboBox is populated with the cities belonging to the 'United States'
  3. The interesting bit: ConvertBack is telling us that the city selected in the first option (previously Melbourne) is now null!

My theory: in step number 2 we are changing the contents of a ComboBox still bound to the 'Melbourne' visit. The databinding between the ComboBox and Visit.City fires, cannot find a city in the Visit.Country.Cities and interprets this as null.

The fix.... comes in my next blog post. I've experimented with two approaches for the issue. Approach 1: binding the city ComboBox to a different collection which is temporarily populated with the ALL the possible cities when the PageViewModel.SelectedVisit changes. Approach 2: again populate the city ComboBox will all possible cities - and use databinding to modify the city's visibility.

Grab the source code for this demo: CitiesVisited_step1.zip

Labels: ,