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.

image image

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

Published Sunday, September 28, 2008 9:45 PM by manish.dalal
Filed under: , ,

Comments

# funny wallpaper &raquo; ComboBox in DataGrid

Monday, September 29, 2008 10:24 AM by funny wallpaper » ComboBox in DataGrid

Pingback from  funny wallpaper &raquo; ComboBox in DataGrid

# re: ComboBox in DataGrid

Monday, September 29, 2008 12:19 PM by Rachida Dukes

Thanks for sharing your knowledge. Keep the good work. Thanks again.

Rachida

# re: ComboBox in DataGrid

Monday, September 29, 2008 8:48 PM by DrewPierce

Manish,

very nicely done.

Thanks for the effort. Hopefully we can return the favor sometime. Don't hesitate to ask me about WCF or security if the need does arise.

# re: ComboBox in DataGrid

Tuesday, September 30, 2008 12:58 AM by Gary Hanson

Manish-

Thank you. This was very frustrating.

-Gary

# Silverlight Cream for September 29, 2008 -- #382

Tuesday, September 30, 2008 2:01 AM by Community Blogs

Boyan Mihaylov on SL/Amazon, David Hyde with SL Stock Portfolio, Chris Anderson with SL LOB app, Jesse

# 2008 September 30 - Links for today &laquo; My (almost) Daily Links

Pingback from  2008 September 30 - Links for today &laquo; My (almost) Daily Links

# Silverlight news for September 30, 2008

Tuesday, September 30, 2008 9:45 AM by Silverlight news for September 30, 2008

Pingback from  Silverlight news for September 30, 2008

# rascunho &raquo; Blog Archive &raquo; links for 2008-09-30

Tuesday, September 30, 2008 4:08 PM by rascunho » Blog Archive » links for 2008-09-30

Pingback from  rascunho  &raquo; Blog Archive   &raquo; links for 2008-09-30

# re: ComboBox in DataGrid

Wednesday, October 08, 2008 9:56 AM by Maksim

Nice article!

Could you please explain how to sort by this template column as far as implementation of IComparable in the City class doesn't seems to work. And one more issue is that after starting to edit with a combobox current value isn't selected automatically and is set to blank after pop-up is shown.

# re: ComboBox in DataGrid

Thursday, October 09, 2008 9:04 PM by manish.dalal

Maksim,

Sort works, here is the markup

<data:DataGridTemplateColumn Header="City" SortMemberPath="CityInfo.CityName">

Currenty value seems to work for me, but if it does not work for you, try this markup

<ComboBox ItemsSource="{Binding CityList, Source={StaticResource cityProvider}}"

                                     SelectedItem="{Binding CityInfo, Mode=TwoWay}"

                                     DisplayMemberPath="CityName"

                                     src:ComboBoxService.ForceOpen="true"

                                 />

Make sure ItemSource is before SelectedItem

# Cascading ComboBox

Wednesday, October 22, 2008 11:55 AM by Manish Dalal's blog

In the post ComboBox in DataGrid , we examined the usage of a simple ComboBox, displaying a fixed list

# Silverlight 2.0 Immersivity: Datagrid to ComboBox Data Binding

Friday, October 24, 2008 8:56 AM by Dave Burke

Silverlight 2.0 Immersivity: Datagrid to ComboBox Data Binding

# re: ComboBox in DataGrid

Friday, October 24, 2008 10:27 AM by Mike

Thanks for posting this solution. SelectedValuePath seems like a pretty big oversight. Definitely caught me off guard

# re: ComboBox in DataGrid

Tuesday, November 04, 2008 4:36 PM by Nick

Cool Article ...

but i have the following issues.

Combobox isn't selected automatically and also

after selecting any calue it goes back to blank .

Please help

# re: ComboBox in DataGrid

Wednesday, December 31, 2008 2:11 AM by Sharker Khaleed Mahmud

Hi,

I was going through your article and i was wondering if you could tell me how get the get the combobox in SelectionChanged event in datagrid when a particular row is selected.

private void dataGrid_SelectionChanged(object sender,SelectionChangedEventArgs e)

{

ComboBox selectedRowComboBox = ??

}

Thanks

email: shamrat231@hotmail.com

Sharker Khaleed Mahmud

# re: ComboBox in DataGrid

Sunday, March 29, 2009 6:55 PM by Rontch

How do you get the selected item of the combobox, in the selected row?

The thing is that the combobox is displayed of type DataGridTemplateColumn)

# re: ComboBox in DataGrid

Thursday, May 21, 2009 12:56 AM by Srikanth

hi Manish,

Can i know if this works with Data which is retrieved from database.....

Kindly let me if its possible...Since i have problems doing it using database....

Thanks In Advance

# re: ComboBox in DataGrid

Thursday, May 21, 2009 12:58 AM by Kunal

Hi, I am using the same source code but binding data using WCF service. But During Page Load it is not showing datain cityId coloumn. but when I click on the column combobox appears and on selected item it gets bind. I dont know why this is happening. I am getting list from WCF service and counverting it to List<city>..Please Help

# re: ComboBox in DataGrid

Thursday, May 21, 2009 2:20 PM by Marc

Same here: what if you get your addresses from a server using WCF. The Address classes are generated for you. They can be extended with Extension Methods but you cannot add a property like CityInfo. Does this mean I have to copy the ObservableCollection<Address> to an ObservableCollection<AddressSubClass> so that I have the CityInfo property? Or is there a way to define the CityInfo on the server and then WCF somehow gets it on the client?

# re: ComboBox in DataGrid

Thursday, May 21, 2009 7:51 PM by manish.dalal

@Marc,

Classes that are generated for you are "partial" classes, allowing you to extend them as needed... alternatively you can always use ViewModel for binding  

@Kunal,

How are you loading initial data? Note that there are two parts, Foreign Key and some Description, in addition to ItemsSource. When you get data from server for a particular record, you need to get Id and Description (CityId and CityDescription) and make defult CityInfo object out of it. This will be CityInfo and then load ItemsSource.

@Srikanth,

Code works with database (WCF) service. I will have an example soon for SL3 + RIA Services (it is quite a bit easier!)

# re: ComboBox in DataGrid

Tuesday, May 26, 2009 4:19 PM by Marc

Classes can be extended with methods but not with properties. Properties are essential in your solution.

# re: ComboBox in DataGrid

Tuesday, May 26, 2009 7:38 PM by manish.dalal

@March,

You are probably confusing Extended methods with partial class. Partial class allows you to provide other partial class(es) and compiler will combine them. You can include any property, methods or other attributes!

# re: ComboBox in DataGrid

Tuesday, June 23, 2009 1:08 PM by Marc

Of course! Add partial classes to extend what's generated. Thanks!

# Silverlight 3 ComboBox Control

Friday, July 03, 2009 1:42 PM by Community Blogs

This post outlines technique for displaying ComboBox control with .net RIA services to handle Foreign

# Silverlight 3 ComboBox Control | I love .NET!

Friday, July 03, 2009 4:15 PM by Silverlight 3 ComboBox Control | I love .NET!

Pingback from  Silverlight 3 ComboBox Control | I love .NET!

Leave a Comment

(required) 
(required) 
(optional)
(required)