ComboBox in DataGrid
This post examines usage of ComboBox in DataGrid. In particular, it shows how to implement foreign key scenarios in lieu of missing SelectedValue property. It also highlights workaround for a bug in RC0 that causes ComboBox dropdown to close immediately in DataGrid.
ComboBox
The ComboBox control is used to present users with a list of values to select from. This can be used to show a simple list of valid values that user can choose from or you might be showing user a complicated type. ComboBox.ItemTemplate can be used visualize the complex item, but you will find that current ComboBox implementation in Silverlight 2 (RC0) is missing SelectedValue and SelectedValuePath property. This is a common requirement in business applications, where in majority of cases foreign keys are used for represents joins/relationships. In these cases you normally use foreign key values to select the item and result of use selection is also saved as foreign key value. However user is shown a friendly name/description in place of foreign key value which might be numeric or guid.
Lets first see how to bind to a simple property, a string list and then we will modify the code to implement foreign key scenario by binding to a complex type .
Simple Data Binding
Create a new Silverlight application project. Also create the corresponding web application to host and test the Silverlight application. Next add System.Windows.Control.Data reference to the Silverlight project.
Add following code to page.xaml
<UserControl x:Class="Silverlight.Page"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:data="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data"
xmlns:src="clr-namespace:Silverlight"
Width="400" Height="300">
<UserControl.Resources>
<src:CityProvider x:Key="cityProvider"/>
</UserControl.Resources>
<Grid x:Name="LayoutRoot" Background="White">
<data:DataGrid x:Name="dataGrid" AutoGenerateColumns="False">
<data:DataGrid.Columns>
<data:DataGridTextColumn Header="Street Name" Binding="{Binding StreetName}"/>
<data:DataGridTemplateColumn Header="City">
<data:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding CityName}" />
</DataTemplate>
</data:DataGridTemplateColumn.CellTemplate>
<data:DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<ComboBox SelectedItem="{Binding CityName, Mode=TwoWay}"
ItemsSource="{Binding CityList, Source={StaticResource cityProvider}}"
/>
</DataTemplate>
</data:DataGridTemplateColumn.CellEditingTemplate>
</data:DataGridTemplateColumn>
<data:DataGridTextColumn Header="Zip Code" Binding="{Binding ZipCode}"/>
</data:DataGrid.Columns>
</data:DataGrid>
</Grid>
</UserControl>
Add following code to page.xaml.cs code behind
public partial class Page : UserControl {
public Page() {
InitializeComponent();
this.Loaded += new RoutedEventHandler(Page_Loaded);
}
void Page_Loaded(object sender, RoutedEventArgs e) {
this.dataGrid.ItemsSource = new List<Address>() { new Address() { StreetName = "Street 1", CityName="City 1", ZipCode = "1"},
new Address() { StreetName = "Street 2", CityName="City 2", ZipCode = "2"},
new Address() { StreetName = "Street 3", CityName="City 3", ZipCode = "3"}
};
}
}
public class Address {
public string StreetName { get; set; }
public string CityName { get; set; }
public string ZipCode { get; set; }
}
public class CityProvider {
public List<string> CityList {
get {
return new List<string> { "City 1", "City 2", "City 3", "City 4" };
}
}
}
Here we are showing user Address business entity in the DataGrid. Address class consists of StreetName, CityName and ZipCode fields. User can change value of CityName using ComboBox, that provides list of valid cities.
ComboBox ItemsSource is bound to CityProvider that provides a list of cities(string list) to select from. ComboBox SelectedItem property is bound to CityName field of the Address business class. This selects proper value in ComboBox when it is shown and allows saving user selection back to the business class.
F5 and test the application. Change value of City field. Notice proper selection of item in ComboBox dropdown after the change.
Foreign Key Data Binding (alternative to missing SelectedValue property)
Now instead of binding to simple string City field, lets say that we are storing CityId (an integer) in Address class in place of City. Here CityId represents the foreign key. Lets modify code to select using CItyId foreign key and also save result of selection as CityId back to Address business class.
Modify Page.xaml as follows
<UserControl x:Class="Silverlight.Page"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:data="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data"
xmlns:src="clr-namespace:Silverlight"
Width="400" Height="300">
<UserControl.Resources>
<src:CityProvider x:Key="cityProvider"/>
</UserControl.Resources>
<Grid x:Name="LayoutRoot" Background="White">
<data:DataGrid x:Name="dataGrid" AutoGenerateColumns="False">
<data:DataGrid.Columns>
<data:DataGridTextColumn Header="Street Name" Binding="{Binding StreetName}"/>
<data:DataGridTemplateColumn Header="City">
<data:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding CityInfo.CityName}" />
</DataTemplate>
</data:DataGridTemplateColumn.CellTemplate>
<data:DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<ComboBox SelectedItem="{Binding CityInfo, Mode=TwoWay}"
ItemsSource="{Binding CityList, Source={StaticResource cityProvider}}"
DisplayMemberPath="CityName"
/>
</DataTemplate>
</data:DataGridTemplateColumn.CellEditingTemplate>
</data:DataGridTemplateColumn>
<data:DataGridTextColumn Header="Zip Code" Binding="{Binding ZipCode}"/>
</data:DataGrid.Columns>
</data:DataGrid>
</Grid>
</UserControl>
Change Page.xaml.cs code behind as:
public partial class Page : UserControl {
public Page() {
InitializeComponent();
this.Loaded += new RoutedEventHandler(Page_Loaded);
}
void Page_Loaded(object sender, RoutedEventArgs e) {
this.dataGrid.ItemsSource = new List<Address>() { new Address() { StreetName = "Street 1", CityId=1, ZipCode = "1"},
new Address() { StreetName = "Street 2", CityId=2, ZipCode = "2"},
new Address() { StreetName = "Street 3", CityId=3, ZipCode = "3"}
};
}
}
public class Address {
public string StreetName { get; set; }
//public string CityName { get; set; }
public int CityId { get; set; }
public string ZipCode { get; set; }
//
private City _cityInfo;
public City CityInfo {
get {
if (null == _cityInfo) {
_cityInfo = new CityProvider().CityList.Where(c => c.CityId == CityId).SingleOrDefault();
}
return _cityInfo;
}
set {
_cityInfo = value;
CityId = _cityInfo.CityId;
}
}
}
public class CityProvider {
public List<City> CityList {
get {
//return new List<string> { "City 1", "City 2", "City 3", "City 4" };
return new List<City> { new City() { CityName ="City 1", CityId=1},
new City() { CityName ="City 2", CityId=2},
new City() { CityName ="City 3", CityId=3},
new City() { CityName ="City 4", CityId=4} };
}
}
}
public class City {
public string CityName { get; set; }
public int CityId { get; set; }
public override bool Equals(object obj) {
if (null == obj) {
return false;
}
return this.CityId == ((City)obj).CityId;
}
public override int GetHashCode() {
return CityName.GetHashCode();
}
}
We have changed CityProvider to return City class that consists of CityName and CityId fields. We have also modified Address business class by removing CityName and adding CityId field to store value of foreign key.
Notice that we have also added new CityInfo property to the Address class. Address class provides this helper property so that we can bind it to ComboBox SelectedItem property. It is this helper property that is key to the data binding in foreign key scenarios. In get method we use current CityId value from Address business class and return an instance of City class. This instance sets the SelectedItem by way of data binding. In set method, we get values from ComboBox selected item and set CityId to the selection. (Demo Note: In production code please consider caching CityList)
Now when you run the application, and change values, proper CityId is saved back to Address class.
RC0 ComboBox Issue
In RC0, there is issue with ComboBox displaying properly in DataGrid. When you try to open ComboBox, it display and then immediately closes. I posted about it here and Lee found a work around. Here is the work around. Use MyComboBox in place of ComboBox in above code.
public class MyComboBox : ComboBox {
public MyComboBox() {
DefaultStyleKey = typeof(ComboBox);
this.Loaded += new RoutedEventHandler(MyComboBox_Loaded);
}
void MyComboBox_Loaded(object sender, RoutedEventArgs e) {
IsDropDownOpen = true;
}
}
Alternatively if you wound rather not inherit and create new control, you can use Attached Property to provide same behavior modification. I have already blogged about using Attached Property to provide service and this uses the same technique to force open ComboBox dropdown
public class ComboBoxService {
public static readonly DependencyProperty ForceOpenProperty =
DependencyProperty.RegisterAttached("ForceOpen", typeof(bool), typeof(ComboBoxService),
new PropertyMetadata(OnForceOpenChanged));
public static bool GetForceOpen(DependencyObject d) {
return (bool)d.GetValue(ForceOpenProperty);
}
public static void SetForceOpen(DependencyObject d, bool value) {
d.SetValue(ForceOpenProperty, value);
}
private static void OnForceOpenChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
ComboBox comboBox = d as ComboBox;
if ((bool)e.OldValue) {
comboBox.Loaded -= new RoutedEventHandler(comboBox_Loaded);
}
if ((bool)e.NewValue) {
comboBox.Loaded +=new RoutedEventHandler(comboBox_Loaded);
}
}
static void comboBox_Loaded(object sender, RoutedEventArgs e) {
ComboBox comboBox = sender as ComboBox;
if (null == comboBox) {
comboBox = e.OriginalSource as ComboBox;
}
//
comboBox.IsDropDownOpen = true;
}
}
Usage:
<ComboBox SelectedItem="{Binding CityInfo, Mode=TwoWay}"
ItemsSource="{Binding CityList, Source={StaticResource cityProvider}}"
DisplayMemberPath="CityName"
src:ComboBoxService.ForceOpen="true"
/>
Source Code: ComboBoxUsage.zip