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>
Recent Comments