Implementing SelectedValue with the Silverlight ComboBox

Here’s the scenario … You have a Customers collection and an Orders collection. In the Orders collection, an Order has a CustomerID property; this is the key to an item in the Customers collection. Your goal is a ComboBox that updates the CustomerID property of a Customer, but interacts with the end user in terms of customer names. If that doesn’t make sense, code & pictures help …

Here’s how you can accomplish this in markup in WPF (the l:Collection here is just a Collection<object>):

<Grid>

<Grid.Resources>

<l:Collection x:Key="Customers"> <!-- Collection<object> -->

<l:Customer ID="1" CustomerName="Wilma" />

<l:Customer ID="2" CustomerName="Betty" />

<l:Customer ID="3" CustomerName="Fred" />

<l:Customer ID="4" CustomerName="Barney" />

</l:Collection>

<l:Collection x:Key="Orders"> <!-- Collection<object> -->

<l:Order Description="Magic carpet" CustomerID="1" />

<l:Order Description="Blue suede shoes" CustomerID="4" />

<l:Order Description="Hanna Montana wig" CustomerID="1" />

</l:Collection>

</Grid.Resources>

<StackPanel>

<!-- Show a customer ID -->

<StackPanel Orientation="Horizontal">

<TextBlock Text="Customer ID:" Margin="5"/>

<TextBlock Name="TextBlock1" Margin="5" Text="1"/>

</StackPanel>

<!-- Pick the customer ID that will be shown above -->

<ComboBox ItemsSource="{StaticResource Customers}"

SelectedValuePath="ID"

SelectedValue="{Binding Text, ElementName=TextBlock1}"

DisplayMemberPath="CustomerName" />

</StackPanel>

</Grid>

More on that ComboBox after these pictures,

Initially we get this:

clip_image002

… and here I am opening the ComboBox, and changing the customer to Betty (her CustomerID is 2):

clip_image004

So that changed the “Customer ID” from 1 to 2, but I (the end user) interacted only with customer names via the ComboBox.

Now let’s analyze that ComboBox markup:

<ComboBox ItemsSource="{StaticResource Customers}"

SelectedValuePath="ID"

SelectedValue="{Binding Text, ElementName=TextBlock1}"

DisplayMemberPath="CustomerName" />

The ItemsSource is set to the Customers list. So when you open the ComboBox, you’ll see customers. Specifically, for each customer in that list, you’ll see the Customer.CustomerName, because the DisplayMemberPath is set to the “CustomerName” property.

Now whatever is selected in the ComboBox, I want it to show up in TextBlock1. If I just bind SelectedItem to TextBlock1, I’d be sending the whole Customer object over, but really I just want the TextBlock.Text to show the Customer.CustomerID. So, instead of binding SelectedItem to the TextBlock, I bind SelectedValue. SelectedValue behaves the same as SelectedItem, though, until you set a SelectedValuePath. When you set SelectedValuePath, then SelectedValue becomes that path into the SelectedItem. For example, if your SelectedItem is a Customer, and your SelectedValuePath is “ID”, then SelectedValue is going to be Customer.ID, which is just what we want.

(Note in this example I defaulted TextBlock1.Text to ‘1’. Otherwise the SelectedValue binding gets confused. Alternatively, I could have set the Binding.Mode to ‘OneWayToSource’.)

Next let’s do this with the Silverlight ComboBox, because it doesn’t have the SelectedValue or SelectedValuePath properties. But we can still get it to work with a little bit of code.

Update: The Silverlight 4 Beta does now have SelectedValue and SelectedValuePath properties.

Change the markup to this:

<ComboBox ItemsSource="{StaticResource Customers}"

DisplayMemberPath="CustomerName"

SelectionChanged="ComboBox_SelectionChanged" />

… and write a little code:

private void ComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)

{

var comboBox = sender as ComboBox;

TextBlock1.Text = (comboBox.SelectedItem as Customer).ID.ToString();

}

It’d be nice if we could do this all in markup, though, and in fact there have been some posts on the Silverlight forums to do just that with a Binding value converter. Also, I wish I could have this feature on a TextBlock too, not just on ComboBox. So below is a handy value converter that can be used with a Binding. (I created this on the Silverlight 3 beta, but most of this is applicable to Silverlight 2 as well.) The idea of this is to mimic the ComboBox mechanism. First, here’s an example of the converter being created:

<Grid.Resources>

<l:Collection x:Key="Customers"> <!-- Collection<object> -->

...

</l:Collection>

<l:Collection x:Key="Orders"> <!-- Collection<object> -->

...

</l:Collection>

<l:ValueToItemConverter x:Key="CustomerID2Name"

ItemsSource="{StaticResource Customers}"

ValuePath="ID"

DisplayMemberPath="CustomerName" />

</Grid.Resources>

… notice that it looks much like the ComboBox. The only thing it doesn’t have is a counterpart to the SelectedValue property, because the value is what comes into the IValueConverter methods. Now let’s use it in a trivial master/detail:

<Grid >

<Grid.ColumnDefinitions>

<ColumnDefinition/>

<ColumnDefinition/>

</Grid.ColumnDefinitions>

<!-- List of Orders -->

<ListBox ItemsSource="{StaticResource Orders}" Name="ListBox1">

<ListBox.ItemTemplate>

<DataTemplate>

<TextBlock Text="{Binding Description}" />

</DataTemplate>

</ListBox.ItemTemplate>

</ListBox>

<!-- Detail for the selected Order -->

<StackPanel Grid.Column="1"

DataContext="{Binding SelectedItem, ElementName=ListBox1}">

<TextBlock Text="{Binding Description}" />

<!-- Order.CustomerID convertered to a Customer.CustomerName -->

<TextBlock Text="{Binding CustomerID,

Converter={StaticResource CustomerID2Name}}" />

</StackPanel>

</Grid>

… which gives us this (here I’ve selected the first order):

clip_image006

The key here is that in the detail, rather than showing CustomerID 1 from the Order, we see “Wilma”.

 

 

And finally, here’s an example using this value converter in the column of a DataGrid.  Again, the items are Orders, and the column shows a CustomerID as a CustomerName, using a TextBlock when the cell isn’t being edited, and as a ComboBox when it is. 

 

First, create a version of the value converter that doesn’t have DisplayMemberPath set (because ComboBox has its own DisplayMemberPath property):

 

<l:ValueToItemConverter x:Key="CustomerID2Customer"

               ItemsSource="{StaticResource Customers}"

               ValuePath="ID" />

 

… and then the DataGrid itself:

 

<d:DataGrid AutoGenerateColumns="False"

           ItemsSource="{StaticResource Orders}">

    <d:DataGrid.Columns>

        <d:DataGridTextColumn Binding="{Binding Description}" />

       

        <d:DataGridTemplateColumn>

           

            <d:DataGridTemplateColumn.CellTemplate>

                <DataTemplate>

                    <TextBlock Text="{Binding CustomerID,

                                              Converter={StaticResource CustomerID2Name}}"

                              Margin="4"/>

                </DataTemplate>

            </d:DataGridTemplateColumn.CellTemplate>

           

            <d:DataGridTemplateColumn.CellEditingTemplate>

                <DataTemplate>

     <ComboBox

                         DisplayMemberPath="CustomerName"

                         ItemsSource="{StaticResource Customers}"

                         SelectedItem="{Binding CustomerID, Mode=TwoWay,

                                          Converter={StaticResource CustomerID2Customer}}" />

                </DataTemplate>

            </d:DataGridTemplateColumn.CellEditingTemplate>

           

        </d:DataGridTemplateColumn>

    </d:DataGrid.Columns>

</d:DataGrid>

 

 

 

SelectedValue.zip