This is quite a common task/requirement, to have a drop down/combo interface with checkable options inside.
Silverlight is quite good at this due to its databound approach to displaying and manipulating data.
As a preface, lets assume you have wrapped your IEnumerable data in a wrapper class that contains the data item and a 'Selected' property, for example:
public class SelectableObject : ComponentModel.INotifyPropertyChanged
{
private bool _selected;
public bool Selected
{
get { return _selected; }
set
{
_selected = value;
OnPropertyChanged("Selected");
}
}
public object DataItem { get; set; }
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
Now that you have a datasource which has the properties needed to bind a selectable list you can display the data in a modified combo box which is designed to work with this kind of data. I based my combo box on the RadComboBox (prism version), but this can likely be easily translated to any other base implementation:
XAML:
<UserControl x:Class="FQNS.MultiSelectDropDown"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
xmlns:my="clr-namespace:ExternalControls.TelerikForPrism;assembly=ExternalControls.TelerikForPrism"
d:DesignHeight="300" d:DesignWidth="400">
<Grid x:Name="LayoutRoot">
<my:PrismRadComboBox ItemsSource="{Binding}" Name="cmbComboBox" EmptyText="-All-" Margin="0,0,0,2" SelectionChanged="cmbComboBox_SelectionChanged" DropDownClosed="cmbComboBox_DropDownClosed">
<!-- Data Template is defined in code behind-->
</my:PrismRadComboBox>
</Grid>
</UserControl>
Code-behind:
public partial class MultiSelectDropDown : UserControl
{
public MultiSelectDropDown()
{
InitializeComponent();
}
private string _displayMemberPath;
public string DisplayMemberPath
{
get { return _displayMemberPath; }
set
{
_displayMemberPath = value;
cmbComboBox.ItemTemplate = (DataTemplate)XamlReader.Load(@"
<DataTemplate xmlns=""http://schemas.microsoft.com/client/2007"">
<StackPanel Orientation=""Horizontal"">
<CheckBox IsChecked=""{Binding Selected, Mode=TwoWay}""></CheckBox>
<TextBlock Text=""{Binding Path=" + DisplayMemberPath + @"}""></TextBlock>
</StackPanel>
</DataTemplate>");
}
}
private void cmbComboBox_SelectionChanged(object sender, Telerik.Windows.Controls.SelectionChangedEventArgs e)
{
//we actually dont want 'selections' to be made, so always select -1 but tick the selected item
SetItemSelectedProperty(cmbComboBox.SelectedItem, true);
cmbComboBox.SelectedIndex = -1;
}
private void SetComboText()
{
//at this point the data context should be at least IEnumerable
IEnumerable objectList = DataContext as IEnumerable;
if (objectList != null)
{
List<object> selectedItems = (from object o in objectList where GetItemSelectedProperty(o) == true select o).ToList();
switch (selectedItems.Count)
{
case 0:
cmbComboBox.EmptyText = "-All-";
break;
case 1:
cmbComboBox.EmptyText = GetItemDisplayProperty(selectedItems[0]);
break;
default:
cmbComboBox.EmptyText = "Multiple Selections..";
break;
}
}
}
private void SetItemSelectedProperty(object dataItem, bool value)
{
if (dataItem != null)
{
//get the 'Selected' property and confirm its a correct type
PropertyInfo selectedProp = dataItem.GetType().GetProperty("Selected");
if (selectedProp != null && selectedProp.PropertyType == value.GetType())
{
selectedProp.SetValue(dataItem, value, null);
}
}
}
private bool GetItemSelectedProperty(object dataItem)
{
if (dataItem != null)
{
//get the 'Selected' property and confirm its a boolean type
PropertyInfo selectedProp = dataItem.GetType().GetProperty("Selected");
if (selectedProp != null && selectedProp.PropertyType == typeof(bool))
{
return (bool)selectedProp.GetValue(dataItem, null);
}
}
//default to not selected
return false;
}
private string GetItemDisplayProperty(object dataItem)
{
if (dataItem == null)
throw new ArgumentNullException("dataItem", "Data Item cannot be NULL");
//get the 'DisplayMemberPath' property
if (DisplayMemberPath.Contains("."))
{
//child property - iterate the tree
Type currentType = dataItem.GetType();
object currentValue = dataItem;
string[] props = DisplayMemberPath.Split('.');
foreach (string p in props)
{
PropertyInfo thisProp = currentType.GetProperty(p);
if (thisProp != null)
{
currentType = thisProp.PropertyType;
currentValue = thisProp.GetValue(currentValue, null);
}
else
{
break;
}
}
return (currentValue != null ? currentValue.ToString() : dataItem.ToString());
}
else
{
//direct property
PropertyInfo displayProp = dataItem.GetType().GetProperty(DisplayMemberPath);
if (displayProp != null)
{
object propVal = displayProp.GetValue(dataItem, null);
if (propVal != null)
return propVal.ToString();
}
}
//default to return the obejct itself
return dataItem.ToString();
}
private void cmbComboBox_DropDownClosed(object sender, EventArgs e)
{
SetComboText();
}
}
Example Usage:
<my:MultiSelectDropDown DataContext="{Binding MySelectableDataSource, Mode=TwoWay}" DisplayMemberPath="DataItem.Name"></my:MultiSelectDropDown>