Wednesday, March 25, 2009
« CSLA .NET 3.6.2 Release Candidate availa... | Main | Intro to CSLA .NET video by Andrew Hallm... »

Google tells me that issues with the Silverlight ComboBox control are widespread and well known. It also tells me that there are few solid solutions out there if you want data binding to work with the Silverlight ComboBox in a simple, reliable manner.

Perhaps the best post on the topic is this one, but this particular code example doesn’t work in all cases (specifically not inside a DataTemplate, and as a form loads and gets async data).

After wasting more time than I care to consider, I believe I have a more complete solution. Though it isn’t perfect either, it does appear to work nicely with CSLA .NET NameValueListBase objects, and inside DataTemplate elements, which is really what I was after.

As others have pointed out, the solution is to subclass ComboBox and add SelectedValue and ValueMemberPath properties to the control. The trick is to catch all the edge cases where these new properties, and the pre-existing SelectedItem and Items properties, might change. I may not have them all either, but I think I handle most of them :)

To use the new control, you need to bring in the namespace containing the control, and then use XAML like this:

<this:ComboBox Name="ProductCategoryComboBox"
               ItemsSource="{Binding Source={StaticResource ProductCategories},Path=Data}"
               SelectedValue="{Binding Path=CategoryId, Mode=TwoWay}"
               ValueMemberPath="Key"
               DisplayMemberPath="Value"/>

At least in my code, you must set the ValueMemberPath, though I’m sure the code could be enhanced to avoid that requirement. The bigger thing is that the SelectedValue binding must have Mode=TwoWay or the binding won’t work reliably. Even if you are using the ComboBox for display-only purposes, the Mode must be TwoWay.

Also I should point out that this XAML is using the CslaDataProvider control to get the data for the ItemsSource property. You could use other techniques as well, but I like the data provider model because it shifts all the work into XAML, so there’s no code required in the presentation layer. The ProductCategories resource is a CslaDataProvider control that retrieves a NameValueListBase object containing a list of items with Key and Value properties.

Here’s the control code:

public class ComboBox : System.Windows.Controls.ComboBox
{
  public ComboBox()
  {
    this.Loaded += new RoutedEventHandler(ComboBox_Loaded);
    this.SelectionChanged += new SelectionChangedEventHandler(ComboBox_SelectionChanged);
  }

  void ComboBox_Loaded(object sender, RoutedEventArgs e)
  {
    SetSelectionFromValue();
  }

  private object _selection;

  void ComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
  {
    if (e.AddedItems.Count > 0)
    {
      _selection = e.AddedItems[0];
      SelectedValue = GetMemberValue(_selection);
    }
    else
    {
      _selection = null;
      SelectedValue = null;
    }
  }

  private object GetMemberValue(object item)
  {
    return item.GetType().GetProperty(ValueMemberPath).GetValue(item, null);
  }

  public static DependencyProperty ValueMemberPathProperty =
    DependencyProperty.Register("ValueMemberPath", typeof(string), typeof(InventoryDemo.ComboBox), null);

  public string ValueMemberPath
  {
    get
    {
      return ((string)(base.GetValue(ComboBox.ValueMemberPathProperty)));
    }
    set
    {
      base.SetValue(ComboBox.ValueMemberPathProperty, value);
    }
  }

  public static DependencyProperty SelectedValueProperty =
    DependencyProperty.Register("SelectedValue", typeof(object), typeof(InventoryDemo.ComboBox),
    new PropertyMetadata((o, e) =>
    {
      ((ComboBox)o).SetSelectionFromValue();
    }));

  public object SelectedValue
  {
    get
    {
      return ((object)(base.GetValue(ComboBox.SelectedValueProperty)));
    }
    set
    {
      base.SetValue(ComboBox.SelectedValueProperty, value);
    }
  }

  private void SetSelectionFromValue()
  {
    var value = SelectedValue;
    if (Items.Count > 0 && value != null)
    {
      var sel = (from item in Items
                 where GetMemberValue(item).Equals(value)
                 select item).Single();
      _selection = sel;
      SelectedItem = sel;
    }
  }

  protected override void OnItemsChanged(System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
  {
    base.OnItemsChanged(e);
    SetSelectionFromValue();
  }
}

It seems strange that the standard control doesn’t just do the right thing – but we must live in the world that is, not the world we would like to see…

Friday, March 27, 2009 8:29:14 AM (Central Standard Time, UTC-06:00)
I don't know, Rocky.

... Why use a control this isn't stable? Just because it's Silverlight? I'm not sure I get it. Why use controls that are a hassle to maintain? Is it just because they are "flashy" and cool to look at? Why would I want to jeopardize my applications' credibility and stability for some cool visual affects?

I must be getting old or something...
Mark
Friday, March 27, 2009 8:42:33 AM (Central Standard Time, UTC-06:00)
If you want to use Silverlight (and I do) then the choices are to work with the Microsoft control, or buy a suite of controls from a third-party vendor. And the answer might be to just buy controls from Component One or somebody.

But there's some value to fighting with the Microsoft built-in controls, at least for me, because it puts pressure on Microsoft to fix the darn thing. MOST people coming to a UI environment are going to assume the base controls work right and are useful - and this one doesn't - so it is important to put pressure on Microsoft to fix the problem ASAP.
Wednesday, April 29, 2009 1:21:33 PM (Central Standard Time, UTC-06:00)
In the linq statement in SetSelectionFromValue(), I think you should use .SingleOrDefault() instead of .Single() in the case that you don't want a value to be selected in the combo box by default. Otherwise it throws a 'Sequence contains no elements' exception.
Erik
Comments are closed.