Wednesday, January 9, 2013

WPF Simple ComboBox Implementation



Let us start with creating very simple combo box. In the following example, 

  • In stage 1: Let us use combo box items as inline with the control. 
  • In stage 2: Let us then replace these by placing them into window’s resource section.
  • In stage 3: Let us bind to C# objects.
  • In stage 4: Let us use DataContext
  • In stage 5: Let us use DataTemplate.
  • In stage 6: Let us Sort and Filter.
  • In stage 7: Let us use Value Converters  
  • In Stage 8: Let us use XmlDataProvider to load combo bos items list.
  • In stage 9: Let us use Triggers
  • In stage 10: Let us use Styles


Note, when creating combo box in WPF, we do not need to do it following each of these steps, however these steps are examined here to demonstrate implementation of a combo box in any WPF application, starting with the simplest way and then moving towards more advance implementation.



 



Stage 1

ComboBox items are inline.


<Window x:Class="SimpleComboBoxExample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">   
    <Grid>
        <ComboBox Height="23" HorizontalAlignment="Left" Margin="161,49,0,0" Name="comboBox1"
                  VerticalAlignment="Top"
                  Width="156"                                 
                  >           
            <ComboBox.Items>
                <ComboBoxItem>Payroll</ComboBoxItem>
                <ComboBoxItem>Trading</ComboBoxItem>
                <ComboBoxItem>Sales</ComboBoxItem>
            </ComboBox.Items>
        </ComboBox>
    </Grid>
</Window>




Stage

ComboBox items are in window’s resource .


<Window x:Class="SimpleComboBoxExample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:col="clr-namespace:System.Collections;assembly=mscorlib"
        xmlns:sys="clr-namespace:System;assembly=mscorlib"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>      
        <col:ArrayList x:Key="myArray">
            <sys:String>Payroll</sys:String>
            <sys:String>Trading</sys:String>
            <sys:String>Sales</sys:String>
        </col:ArrayList>
    </Window.Resources>
   
    <Grid>
        <ComboBox Height="23" HorizontalAlignment="Left" Margin="161,49,0,0" Name="comboBox1"
                  VerticalAlignment="Top"
                  Width="156"                  
                  ItemsSource="{StaticResource myArray}"                 
                  >           
        </ComboBox>
    </Grid>
</Window>






Stage 3

ComboBox items are implemented using list of department objects.


Instead of list of static items (as in the above examples), let us implement combo box items bound from a list of C# objects. In this case let’s first create a class call “Department” and another class called “Departments” with a public property called “AllDepartments” which we’ll bind to ItemsSource property of the ComboBox.



using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace SimpleComboBoxExample
{
    public class Departments
    {

        public List<Department> AllDepartments
        {
            get
            {
                return new List<Department>()
                {
                    new Department{Name = "Payroll", NumEmp=100, Location="New York"},
                    new Department {Name="Trading", NumEmp=500, Location="New York"},
                    new Department{Name = "Sales", NumEmp=1000, Location="Chicago"},
                };
            }
        }
    }

    public class Department
    {
        private string _name;
        private int _numEmp;
        private string _location;

        public string Location
        {
            get { return _location; }
            set { _location = value; }
        }

        public int NumEmp
        {
            get { return _numEmp; }
            set { _numEmp = value; }
        }

        public string Name
        {
            get { return _name; }
            set { _name = value; }
        }
    }
}




In XAML, we can create an instance of “Departments” class within the resource section. Then we add xaml code to bind the “AllDepartments” property to ItemsSource of ComboBox. Notice also the “DisplayMemberPath” property is important, otherwise display member of combo box item will not show properly.



<Window x:Class="SimpleComboBoxExample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:SimpleComboBoxExample"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <local:Departments x:Key="depts">           
        </local:Departments>
    </Window.Resources>
    <Grid>
        <ComboBox Height="23" HorizontalAlignment="Left" Margin="161,49,0,0" Name="comboBox1"
                  VerticalAlignment="Top"
                  Width="156" 
                  ItemsSource="{Binding Source={StaticResource ResourceKey=depts}, Path=AllDepartments}"
                  DisplayMemberPath="Name"
                  >           
        </ComboBox>
    </Grid>
</Window>





Stage 4

ComboBox items are implemented using DataContext





Instead of creating instance of “Departments” class within resources section, let us create an instance of “Departments” class with code behind (i.e. in MainWindow.xaml.cs class) and set to the DataContext of this window.






public partial class MainWindow : Window

    {

        public MainWindow()

        {

            InitializeComponent();



            var depts = new Departments();

            this.DataContext = depts.AllDepartments;

        }

    }







Now let us use this data context for binding in xaml code.







<Window x:Class="SimpleComboBoxExample.MainWindow"

        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:SimpleComboBoxExample"
        Title="MainWindow" Height="350" Width="525">   
    <Grid>
        <ComboBox Height="23" HorizontalAlignment="Left" Margin="161,49,0,0" Name="comboBox1"
                  VerticalAlignment="Top"
                  Width="156" 
                  ItemsSource="{Binding}"
                  DisplayMemberPath="Name"
                  >           
        </ComboBox>
    </Grid>
</Window>
  



Stage 5

ComboBox items are displayed using DataTemplate




ComboBox's ItemSource is still bound to DataContext (as in method 4 above). Let us now use DataTemplate to format the display of each item in the combo box's drop down.


 


Inline DataTemplate:


<ComboBox Height="23" HorizontalAlignment="Left" Margin="161,49,0,0" Name="comboBox1"
                  VerticalAlignment="Top"
                  Width="156" 
                  ItemsSource="{Binding}"                 
                  > 
            <ComboBox.ItemTemplate>
                <DataTemplate>
                    <Grid>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="60"></ColumnDefinition>
                            <ColumnDefinition Width="60"></ColumnDefinition>
                            <ColumnDefinition Width="*"></ColumnDefinition>
                        </Grid.ColumnDefinitions>
                        <TextBlock Grid.Column="0" Text="{Binding Path=Name}"></TextBlock>
                        <TextBlock Grid.Column="1" Text="{Binding Path=NumEmp}"></TextBlock>
                        <TextBlock Grid.Column="2" Text="{Binding Path=Location}"></TextBlock>
                    </Grid>
                </DataTemplate>
            </ComboBox.ItemTemplate>
        </ComboBox>


DataTemplate as resource:



<Window.Resources>

        <DataTemplate x:Key="myTemplt">

            <Grid>

                <Grid.ColumnDefinitions>

                    <ColumnDefinition Width="60"></ColumnDefinition>

                    <ColumnDefinition Width="60"></ColumnDefinition>

                    <ColumnDefinition Width="*"></ColumnDefinition>

                </Grid.ColumnDefinitions>

                <TextBlock Grid.Column="0" Text="{Binding Path=Name}"></TextBlock>

                <TextBlock Grid.Column="1" Text="{Binding Path=NumEmp}"></TextBlock>

                <TextBlock Grid.Column="2" Text="{Binding Path=Location}"></TextBlock>

            </Grid>

        </DataTemplate>

    </Window.Resources>

    <Grid>

        <ComboBox Height="23" HorizontalAlignment="Left" Margin="161,49,0,0" Name="comboBox1"

                  VerticalAlignment="Top"

                  Width="156" 
                  ItemsSource="{Binding}" 
                  ItemTemplate="{StaticResource myTemplt}"
                  > 
        </ComboBox>
    </Grid>






Stage 6 

Sort and Filter added for ComboBox items drop down list.



XAML stays as is as in the above example (method 5). We just get the view for bound collection and apply sort and filter to that. For this we add the following code (in bold) in the code behind class.



public MainWindow()
        {
            InitializeComponent();

            var depts = new Departments();
            this.DataContext = depts.AllDepartments;

            ListCollectionView view = (ListCollectionView) CollectionViewSource.GetDefaultView(this.DataContext);
            view.SortDescriptions.Add(new SortDescription("Location", ListSortDirection.Ascending));
        }



See how the dropdown items are now sorted by “Location” column in alphabetically ascending order.




In order to add filter we just add the following line of code as well and see how only items with employee count less than 800 is being displayed on the drop down list of the combo box.



view.Filter = ((object item) => { return ((Department)item).NumEmp < 800; });
 






Stage 7 

Value Converter added to ComboBox items to display different foreground color based on employee count.



(First of all let us reset the filter from previous example so all items in the drop down) //view.Filter = ((object item) => { return ((Department)item).NumEmp < 800; }); // commented out for now


In order to demonstrate the use data value converter, let us use value converter where based on employee count, which is of type integer, let us return value for property “Foreground” which is of System.Data.Media.Color type. So let us add new class “EmpCountConverter.cs” which implements IValueConverter interface.


namespace SimpleComboBoxExample
{
    public class EmpCountConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            return int.Parse(value.ToString()) > 800 ? new SolidColorBrush(Colors.Red) : new SolidColorBrush(Colors.SaddleBrown);
        }

        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
}


Inside XAML code, let us update the to hook up this value converter as follows.


<Window.Resources>
        <local:EmpCountConverter x:Key="empCountConvert">
            </local:EmpCountConverter>
        <DataTemplate x:Key="myTemplt">
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="60"></ColumnDefinition>
                    <ColumnDefinition Width="60"></ColumnDefinition>
                    <ColumnDefinition Width="*"></ColumnDefinition>
                </Grid.ColumnDefinitions>
                <TextBlock Grid.Column="0" Text="{Binding Path=Name}"></TextBlock>
                <TextBlock Grid.Column="1"
                           Text="{Binding Path=NumEmp}"
                           Foreground="{Binding Path=NumEmp, Converter={StaticResource empCountConvert}}"
                           ></TextBlock>
                <TextBlock Grid.Column="2" Text="{Binding Path=Location}"></TextBlock>
            </Grid>
        </DataTemplate>
    </Window.Resources>









Stage 8:    

Using XmlDataProvider for the ComboBox items list


 
Let us first create an XML file called Departments.xml as below:


<Departments>
       <Department Name="Payroll" NumEmp="100" Location="New York"></Department>
       <Department Name="Trading" NumEmp="500" Location="New York"></Department>
       <Department Name="Sales" NumEmp="1000" Location="Chicago"></Department>
</Departments>


In our XAML code let us start fresh and add XmlDataProvider in resource section and indicate that as the DataContext for the Grid which contains the ComboBox. IntemsSource will be the “{Binding}” like before, however notice, the DisplayMemberPath=@Name. Here @ sign is in front "Name" because “Name” is an attribute in the “Department” node in the Departments.xml file.


<Window x:Class="SimpleXmlDataSourceExample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <XmlDataProvider x:Key="depts" Source="Departments.xml"
XPath="/Departments/Department">           
        </XmlDataProvider>
    </Window.Resources>

    <Grid DataContext="{StaticResource depts}">
        <ComboBox Height="23" HorizontalAlignment="Left" Margin="161,49,0,0"
Name="comboBox1"
                  VerticalAlignment="Top"
                  Width="156" 
                  ItemsSource="{Binding}" 
                  DisplayMemberPath="@Name"
                  >
        </ComboBox>
    </Grid>
</Window>


When we run this application, we’ll see the following comb box being displayed like before (in stage 1).

  

Now let us put back DataTemplate, ValueConverter and Sorting back this example like before while we are using XmlDataProvider.

<Window x:Class="SimpleXmlDataSourceExample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:SimpleXmlDataSourceExample"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <XmlDataProvider x:Key="depts" Source="Departments.xml" XPath="/Departments/Department">           
        </XmlDataProvider>
        <local:EmpCountConverter x:Key="empCountConvert">
        </local:EmpCountConverter>
        <DataTemplate x:Key="myTemplt">
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="60"></ColumnDefinition>
                    <ColumnDefinition Width="60"></ColumnDefinition>
                    <ColumnDefinition Width="*"></ColumnDefinition>
                </Grid.ColumnDefinitions>
                <TextBlock Grid.Column="0" Text="{Binding XPath=@Name}"></TextBlock>
                <TextBlock Grid.Column="1"
                           Text="{Binding XPath=@NumEmp}"
                           Foreground="{Binding XPath=@NumEmp, Converter={StaticResource empCountConvert}}"
                           ></TextBlock>
                <TextBlock Grid.Column="2" Text="{Binding XPath=@Location}"></TextBlock>
            </Grid>
        </DataTemplate>
    </Window.Resources>
    <Grid Name="myGrid" DataContext="{StaticResource depts}">
        <ComboBox Height="23" HorizontalAlignment="Left" Margin="161,49,0,0" Name="comboBox1"
                  VerticalAlignment="Top"
                  Width="156" 
                  ItemsSource="{Binding}" 
                  ItemTemplate="{StaticResource myTemplt}"
                  >          
        </ComboBox>
    </Grid>
</Window>



Notice in the code behind the sort functionality needs to be added after the xml data is loaded as combo box itels list. Therefore the sorting code (via CollectionViewSource) has been moved under event handler method for combo box’s “Loaded” event.

    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            this.comboBox1.Loaded += new RoutedEventHandler(ComboBoxLoaded);          
        }

        private void ComboBoxLoaded(object sender, RoutedEventArgs e)
        {
            ListCollectionView view =
(ListCollectionView)CollectionViewSource.GetDefaultView(
((XmlDataProvider)this.myGrid.DataContext).Data);
            view.SortDescriptions.Add(new SortDescription("Location",
ListSortDirection.Ascending));
        }      
    }
  
Therefore, when we run it will look like the following screen shot.

2 comments: