Silverlight: ComboBox SelectedItem
Posted
Fri, Dec 4 2009 18:08
by
Deborah Kurata
When last we saw our Silverlight ComboBox in this prior post, it was correctly populating, but as we paged through the records in our DataForm, the SelectedItem was not set correctly.

In XAML:
<ComboBox ItemsSource=
"{Binding Data, Source={StaticResource CustomerTypeSource}}"
DisplayMemberPath="CodeText"
SelectedItem="{Binding CustomerTypeId, Mode=TwoWay}"/>
Regardless of the customer type for a Customer, in this example the SelectedItem is always set to the first item on the list.
The basic problem is that the data bound to the ComboBox has both a display member (the CodeText) and a value member (the CodeId). We want to bind the Customer object's CustomerTypeId property to the Code object's Code Id.
The ComboBox has a DisplayMemberPath property so the ComboBox does display the CodeText properly. It also has a SelectedItem property, which is expecting a Code object. But the Binding statement does not allow us to specify a Code object, only a property name (CustomerTypeId in this case).
Because the ComboBox does not have a ValueMemberPath property, there is no easy way to tell the control that it should map the CustomerTypeId to the CodeId. But there is a hard way using value converters.
A value converter is basically what it sounds like: it converts one value to another value. Use a value converter any time that you want to reformat or change a value in any way.
For the ComboBox SelectedItem to work correctly, we need to "convert" the CustomerTypeId to an appropriate Code object. Basically we need to use the CustomerTypeId to find and return the Code object with a matching CodeId.
Building a converter is not very hard once you know the basics. Just build a class that implements IValueConverter. Then write the code in the associated Convert and ConvertBack methods. I added this class directly to my Silverlight project.
NOTE: Be sure to import the System.Windows.Data namespace.
In C#:
using System.Windows.Controls;
using System.Windows.Data;
namespace SLCSharp
{
public class CustomerTypeIdConverter : IValueConverter
{
public DomainDataSource ItemsSource { get; set; }
public object Convert(object value,
System.Type targetType,
object parameter,
System.Globalization.CultureInfo culture)
{
// Set a default return value
int custTypeId = (int)value;
BoCSharp.Code returnValue = null;
// Look for the value in the list of items
foreach (var item in ItemsSource.Data)
{
BoCSharp.Code codeObject = (BoCSharp.Code)item;
if (codeObject.CodeId == custTypeId)
{
returnValue = codeObject;
break;
}
}
return returnValue;
}
public object ConvertBack(object value,
System.Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
// Set a default return value
BoCSharp.Code codeObject = (BoCSharp.Code)value;
int returnValue = 0;
if (codeObject != null)
{
returnValue = codeObject.CodeId;
}
return returnValue;
}
}
}
In VB:
Imports System.Windows.Data
Public Class CustomerTypeIdConverter
Implements IValueConverter
Private _ItemsSource As DomainDataSource
Public Property ItemsSource() As DomainDataSource
Get
Return _ItemsSource
End Get
Set(ByVal value As DomainDataSource)
_ItemsSource = value
End Set
End Property
Public Function Convert(ByVal value As Object, _
ByVal targetType As System.Type, _
ByVal parameter As Object, _
ByVal culture As System.Globalization.CultureInfo) _
As Object _
Implements System.Windows.Data.IValueConverter.Convert
' Set a default return value
Dim custTypeId As Integer = CType(value, Integer)
Dim returnValue As BoVB.Code = Nothing
' Look for the value in the list of items
For Each item In ItemsSource.Data
Dim codeObject As BoVB.Code = DirectCast(item, BoVB.Code)
If codeObject.CodeId = custTypeId Then
returnValue = codeObject
Exit For
End If
Next
Return returnValue
End Function
Public Function ConvertBack(ByVal value As Object, _
ByVal targetType As System.Type, _
ByVal parameter As Object, _
ByVal culture As System.Globalization.CultureInfo) _
As Object _
Implements System.Windows.Data.IValueConverter.ConvertBack
' Set a default return value
Dim codeObject As BoVB.Code = DirectCast(value, BoVB.Code)
Dim returnValue As Integer = 0
If codeObject IsNot Nothing Then
returnValue = codeObject.CodeId
End If
Return returnValue
End Function
End Class
This code defines a property for the DomainDataSource. This allows you to pass in the Codes data source so you can find the appropriate Code object based on the CustomerTypeId.
The Convert function "converts" the CustomerTypeId to the associated Code object. When the ComboBox SelectedItem is set to a particular CustomerTypeId, the CustomerTypeId is passed to the Convert function. The code in the Convert function first converts the passed in value to an integer. It then loops through the ItemsSource to find the Code object with a CodeId that matches the passed in CustomerTypeId. It then returns the found Code object.
NOTE: Instead of the loop, you could use LINQ instead:
In C#:
var returnValue = ItemsSource.Data.Cast<BoCSharp.Code>().Where(
item => item.CodeId == custTypeId).FirstOrDefault();
In VB:
Dim returnValue = ItemsSource.Data.Cast(Of BoVB.Code). _
Where(Function(item) item.CodeId = custTypeId).FirstOrDefault
The ConvertBack function converts back from a Code object to a CustomerTypeId. This one is easy. As long as a valid Code object is based in, the CustomerTypeId is just the Code object's Code Id.
There is one more required step: you need to modify the XAML to define the converter as a resource and associate it with the ComboBox.
In the UserControl.Resources section, add the following to define the value converter:
In XAML:
<UserControl.Resources>
<local:CustomerTypeIdConverter x:Key="CustomerTypeIdConverter"
ItemsSource="{StaticResource CustomerTypeSource}"/>
</UserControl.Resources>
Notice how this sets the ItemsSource property to the CustomerTypeSource, which contains our customer type codes.
Then replace the ComboBox item in the DataForm with this:
In XAML:
<ComboBox ItemsSource=
"{Binding Data, Source={StaticResource CustomerTypeSource}}"
DisplayMemberPath="CodeText"
SelectedItem="{Binding CustomerTypeId, Mode=TwoWay,
Converter={StaticResource CustomerTypeIdConverter}}"/>
This references the converter from the UserControl resources.
Voila! Paging through the DataForm now shows the correct values!
Dang it! It works until you hit the back button.
Then all of the values are off by one (the value from the prior Customer).
| This MUST be a bug in the DataForm. BUMMER! |
Now what?
You have several choices:
1) Remove the pager control.
In a "real" application, you don't want your users to page through 500 customers. Rather you will have a selection box or search feature for the user to find the customer to edit. Without the pager control, this technique works great!
2) Subclass the ComboBox control and add your own SelectedValue.
Rocky shows how to do this on his blog here.
3) Buy a suite of Silverlight controls that includes a ComboBox from a third party vendor.
In my application, I went with Option #1. I have a DataGrid that displays data for the customers and allows searching and sorting. Double-clicking on in customer in the grid row then displays this DataForm (WITHOUT the pager control). All is well.
Enjoy!