Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to display a different value for dropdown list values/selected item in a WPF ComboBox?

I have a WPF combobox bound to a list of items with long descriptions.

The type bound to the ComboBox has both short and long description as properties. Currently, I am binding to the full description.

comboBox.DisplayMemberPath = "FullDescription";

How to ensure that when the item is selected and displayed as a single item in the combobox, it will be displayed as a value of the ShortDescription property while the dropdown will display FullDescription?

like image 815
Marek Avatar asked Oct 22 '10 10:10

Marek


People also ask

What happens when we select a value from a ComboBox in WPF?

Selected and Current Item Text property of ComboBox represents the text of the current selected item in a ComboBox. SelectedItem represents the first item in the currently selected items in a ComboBox. SelectedValue represents the value of the currently selected item in a ComboBox.

What is ComboBox ItemTemplate?

Data binding the ComboBox Each item, as defined by the ItemTemplate, consists of a StackPanel with a Rectangle and a TextBlock, each bound to the color value.

What is ComboBox in WPF?

Advertisements. A combobox is a selection control that combines a non-editable textbox and a drop-down listbox that allows users to select an item from a list. It either displays the current selection or is empty if there is no selected item.


4 Answers

Update 2011-11-14

I recently came upon the same requirement again and I wasn't very happy with the solution I posted below. Here is a nicer way to get the same behavior without re-templating the ComboBoxItem. It uses a DataTemplateSelector

First, specify the regular DataTemplate, the dropdown DataTemplate and the ComboBoxItemTemplateSelector in the resources for the ComboBox. Then reference the ComboBoxItemTemplateSelector as a DynamicResource for ItemTemplateSelector

<ComboBox ...
          ItemTemplateSelector="{DynamicResource itemTemplateSelector}">
    <ComboBox.Resources>
        <DataTemplate x:Key="selectedTemplate">
            <TextBlock Text="{Binding Path=ShortDescription}"/>
        </DataTemplate>
        <DataTemplate x:Key="dropDownTemplate">
            <TextBlock Text="{Binding Path=FullDescription}"/>
        </DataTemplate>
        <local:ComboBoxItemTemplateSelector
            x:Key="itemTemplateSelector"
            SelectedTemplate="{StaticResource selectedTemplate}"
            DropDownTemplate="{StaticResource dropDownTemplate}"/>
    </ComboBox.Resources>
</ComboBox>

ComboBoxItemTemplateSelector checks if the container is the child of a ComboBoxItem, if it is, then we are dealing with a dropdown item, otherwise it is the item in the ComboBox.

public class ComboBoxItemTemplateSelector : DataTemplateSelector
{
    public DataTemplate DropDownTemplate
    {
        get;
        set;
    }
    public DataTemplate SelectedTemplate
    {
        get;
        set;
    }
    public override DataTemplate SelectTemplate(object item, DependencyObject container)
    {
        ComboBoxItem comboBoxItem = VisualTreeHelpers.GetVisualParent<ComboBoxItem>(container);
        if (comboBoxItem != null)
        {
            return DropDownTemplate;
        }
        return SelectedTemplate;
    }
}

GetVisualParent

public static T GetVisualParent<T>(object childObject) where T : Visual
{
    DependencyObject child = childObject as DependencyObject;
    while ((child != null) && !(child is T))
    {
        child = VisualTreeHelper.GetParent(child);
    }
    return child as T;
}

Old solution, requires re-templating of ComboBoxItem

<SolidColorBrush x:Key="SelectedBackgroundBrush" Color="#DDD" />
<SolidColorBrush x:Key="DisabledForegroundBrush" Color="#888" />

<ControlTemplate x:Key="FullDescriptionTemplate" TargetType="ComboBoxItem">
    <Border Name="Border" Padding="2" SnapsToDevicePixels="true">
        <StackPanel>
            <TextBlock Text="{Binding Path=FullDescription}"/>
        </StackPanel>
    </Border>
    <ControlTemplate.Triggers>
        <Trigger Property="IsHighlighted" Value="true">
            <Setter TargetName="Border" Property="Background" Value="{StaticResource SelectedBackgroundBrush}"/>
        </Trigger>
        <Trigger Property="IsEnabled" Value="false">
            <Setter Property="Foreground" Value="{StaticResource DisabledForegroundBrush}"/>
        </Trigger>
    </ControlTemplate.Triggers>
</ControlTemplate>

<ComboBox Name="c_comboBox" ItemsSource="{Binding}">
    <ComboBox.ItemTemplate>
        <DataTemplate>
            <TextBlock Text="{Binding Path=ShortDescription}"/>
        </DataTemplate>
    </ComboBox.ItemTemplate>
    <ComboBox.ItemContainerStyle>
        <Style TargetType="{x:Type ComboBoxItem}">
            <Setter Property="Template" Value="{StaticResource FullDescriptionTemplate}" />
        </Style>
    </ComboBox.ItemContainerStyle>
</ComboBox>

This results in the following behavior

alt text

like image 137
Fredrik Hedblad Avatar answered Sep 20 '22 14:09

Fredrik Hedblad


It doesn't seem to work for me now, but this one does:

public class ComboBoxItemTemplateSelector : DataTemplateSelector {
  public DataTemplate SelectedTemplate { get; set; }
  public DataTemplate DropDownTemplate { get; set; }

  public override DataTemplate SelectTemplate(object item, DependencyObject container) {
    var presenter = (ContentPresenter)container;
    return (presenter.TemplatedParent is ComboBox) ? SelectedTemplate : DropDownTemplate;
  }
}
like image 23
Gábor Avatar answered Sep 16 '22 14:09

Gábor


I modified this custom rounded WPF ComboBox to display a different value from the item selected as well as change the color for each item.

Custom ComboBox

First you need to create the structure:

//Structure
public class COMBOITEM
{
    string _ITEM_NAME;
    string _ITEM_SHORT_NAME;
    Brush _ITEM_COLOR;

    public string ITEM_NAME
    {
        get { return _ITEM_NAME; }
        set { _ITEM_NAME = value; }
    }

    public string ITEM_SHORT_NAME
    {
        get { return _ITEM_SHORT_NAME; }
        set { _ITEM_SHORT_NAME = value; }
    }

    public Brush ITEM_COLOR
    {
        get { return _ITEM_COLOR; }
        set { _ITEM_COLOR = value; }
    }
}

Initialize the structure, fill it with data and bind to ComboBox:

private void Load_Data()
{
    Brush Normal_Blue = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#FF1F4E79"));
    //Load first entry
    ObservableCollection<COMBOITEM> _Line_Data = new ObservableCollection<COMBOITEM>();
    _Line_Data.Add(new COMBOITEM() { ITEM_NAME = "Line Number 1", ITEM_SHORT_NAME = "LN 1", ITEM_COLOR = Normal_Blue });

    //Load Test Data
    for (int i = 2; i < 10; i++)
    {
        _Line_Data.Add(new COMBOITEM()
        {
            ITEM_NAME = "Line Number " + i.ToString(),
            ITEM_SHORT_NAME = "LN " + i.ToString(),
            ITEM_COLOR = (i % 2 == 0) ? new SolidColorBrush(Colors.Green) : new SolidColorBrush(Colors.Red) //This just changes color
        });
    }

    //Bind data to combobox
    cb_Test.ItemsSource = _Line_Data;
}

Now place the ComboBox in your design. To use it as a normal ComboBox, remove DisplayMemberPath and rename "ColorComboBoxItem" to "CustomComboBoxItem":

<ComboBox x:Name="cb_Test" FontSize="36" Padding="1,0" MinWidth="100" MaxWidth="400" Margin="5,53,10,207" FontFamily="Calibri" Background="#FFBFBFBF" Foreground="#FF1F4E79" BorderBrush="#FF1F4E79" VerticalContentAlignment="Center" TabIndex="5" IsSynchronizedWithCurrentItem="False"
            Style="{DynamicResource RoundedComboBox}" 
            ItemContainerStyle="{DynamicResource ColorComboBoxItem}" 
            DisplayMemberPath="ITEM_SHORT_NAME" />

Now add the following styles/template to App.xaml Application.Resources:

<!-- Rounded ComboBox Button -->
<Style x:Key="ComboBoxToggleButton" TargetType="ToggleButton">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="ToggleButton">
                <Grid>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition />
                        <ColumnDefinition Width="32" />
                    </Grid.ColumnDefinitions>
                    <Border
                    x:Name="Border"
                    Grid.ColumnSpan="2"
                    CornerRadius="8"
                    Background="{TemplateBinding Background}"
                    BorderBrush="#FF1F4E79"
                    BorderThickness="2" 
                />

                    <Path
                    x:Name="Arrow"
                    Grid.Column="1"    
                    Fill="{TemplateBinding Foreground}"
                    Stroke="{TemplateBinding Foreground}"
                    HorizontalAlignment="Center"
                    VerticalAlignment="Center"
                    Data="M 0 0 L 4 4 L 8 0 Z"/>
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

<ControlTemplate x:Key="ComboBoxTextBox" TargetType="TextBox">
    <Border x:Name="PART_ContentHost" Focusable="True" />
</ControlTemplate>

<!-- ComboBox Template -->
<Style x:Key="RoundedComboBox" TargetType="{x:Type ComboBox}">
    <Setter Property="Foreground" Value="#333" />
    <Setter Property="BorderBrush" Value="Gray" />
    <Setter Property="Background" Value="White" />
    <Setter Property="SnapsToDevicePixels" Value="true"/>
    <Setter Property="OverridesDefaultStyle" Value="true"/>
    <Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Auto"/>
    <Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto"/>
    <Setter Property="ScrollViewer.CanContentScroll" Value="true"/>
    <Setter Property="FontSize" Value="13" />
    <Setter Property="MinWidth" Value="150"/>
    <Setter Property="MinHeight" Value="35"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="ComboBox">
                <Grid>
                    <ToggleButton
                    Cursor="Hand"
                    Name="ToggleButton"
                    BorderBrush="{TemplateBinding BorderBrush}"
                    Background="{TemplateBinding Background}"
                    Foreground="{TemplateBinding Foreground}"
                    Style="{StaticResource ComboBoxToggleButton}"
                    Grid.Column="2"
                    Focusable="false"
                    IsChecked="{Binding Path=IsDropDownOpen,Mode=TwoWay,RelativeSource={RelativeSource TemplatedParent}}"
                    ClickMode="Press"/>

                    <ContentPresenter
                    Name="ContentSite"
                    IsHitTestVisible="False"
                    Content="{TemplateBinding SelectionBoxItem}"
                    ContentTemplate="{TemplateBinding SelectionBoxItemTemplate}"
                    ContentTemplateSelector="{TemplateBinding ItemTemplateSelector}"
                    Margin="10,3,30,3"
                    VerticalAlignment="Center"
                    HorizontalAlignment="Left" />
                    <TextBox x:Name="PART_EditableTextBox"
                    Style="{x:Null}"
                    Template="{StaticResource ComboBoxTextBox}"
                    HorizontalAlignment="Left"
                    VerticalAlignment="Center"
                    Margin="3,3,23,3"
                    Focusable="True"                               
                    Visibility="Hidden"
                    IsReadOnly="{TemplateBinding IsReadOnly}"/>
                    <Popup
                    Name="Popup"
                    Placement="Bottom"
                    IsOpen="{TemplateBinding IsDropDownOpen}"
                    AllowsTransparency="True"
                    Focusable="False"
                    PopupAnimation="Slide">
                        <Grid
                        Name="DropDown"
                        SnapsToDevicePixels="True"               
                        MinWidth="{TemplateBinding ActualWidth}"
                        MaxHeight="{TemplateBinding MaxDropDownHeight}">
                            <Border
                            CornerRadius="10"
                            x:Name="DropDownBorder"
                            Background="#FFBFBFBF"
                            BorderThickness="2"
                            BorderBrush="#FF1F4E79"
                            />
                            <ScrollViewer Margin="4,6,4,6" SnapsToDevicePixels="True">
                                <StackPanel IsItemsHost="True" KeyboardNavigation.DirectionalNavigation="Contained" />
                            </ScrollViewer>
                        </Grid>
                    </Popup>

                </Grid>
                <ControlTemplate.Triggers>
                    <Trigger Property="HasItems" Value="false">
                        <Setter TargetName="DropDownBorder" Property="MinHeight" Value="95"/>
                    </Trigger>
                    <Trigger Property="IsGrouping" Value="true">
                        <Setter Property="ScrollViewer.CanContentScroll" Value="false"/>
                    </Trigger>
                    <Trigger Property="IsEditable" Value="true">
                        <Setter Property="IsTabStop" Value="false"/>
                        <Setter TargetName="PART_EditableTextBox" Property="Visibility" Value="Visible"/>
                        <Setter TargetName="ContentSite" Property="Visibility" Value="Hidden"/>
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
    <Style.Triggers>
    </Style.Triggers>
</Style>

<!--This style uses the normal items.add function-->
<Style x:Key="CustomComboBoxItem" TargetType="{x:Type ComboBoxItem}">
    <Setter Property="SnapsToDevicePixels" Value="true" />
    <Setter Property="HorizontalAlignment" Value="Stretch" />
    <Setter Property="VerticalAlignment" Value="Stretch" />
    <Setter Property="FontSize" Value="30" />
    <Setter Property="OverridesDefaultStyle" Value="true"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="ComboBoxItem">
                <Border
                Name="Border"
                Padding="5"
                Margin="2"
                BorderThickness="2,0,0,0"
                CornerRadius="0"
                Background="Transparent"
                BorderBrush="Transparent">
                    <TextBlock TextAlignment="Left">
                    <ContentPresenter />
                    </TextBlock>
                </Border>
                <ControlTemplate.Triggers>
                    <Trigger Property="IsHighlighted" Value="true">
                        <Setter TargetName="Border" Property="BorderBrush" Value="#FF3737CB"/>
                        <Setter TargetName="Border" Property="Background" Value="#FF6ACDEA"/>
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

<!--This style uses the structure to fill items and set the item color-->
<Style x:Key="ColorComboBoxItem" TargetType="{x:Type ComboBoxItem}">
    <Setter Property="SnapsToDevicePixels" Value="true" />
    <Setter Property="HorizontalAlignment" Value="Stretch" />
    <Setter Property="VerticalAlignment" Value="Stretch" />
    <Setter Property="FontSize" Value="30" />
    <Setter Property="OverridesDefaultStyle" Value="true"/>
    <Setter Property="Foreground" Value="{Binding ITEM_COLOR}" />
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="ComboBoxItem">
                <Border
                    Name="Border"
                    Padding="5"
                    Margin="2"
                    BorderThickness="2,0,0,0"
                    CornerRadius="0"
                    Background="Transparent"
                    BorderBrush="Transparent">
                    <TextBlock Text="{Binding ITEM_NAME}" TextAlignment="Left">
                    </TextBlock>
                </Border>
                <ControlTemplate.Triggers>
                    <Trigger Property="IsHighlighted" Value="true">
                        <Setter TargetName="Border" Property="BorderBrush" Value="#FF3737CB"/>
                        <Setter TargetName="Border" Property="Background" Value="#FF6ACDEA"/>
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

I hope this helps..

like image 30
Renier Avatar answered Sep 16 '22 14:09

Renier


This solution is for WPF + MVVM.

Some of the other solutions work, and some of them do not. The problem with some other solutions are that if they do not work, it's sometimes difficult to debug why it is not working, especially if one is not experienced with WPF.

In my opinion, it's preferable to use strings for the bindings, and convert to an enum in C# which means everything is easier to troubleshoot.

You might need to use ReSharper, it will auto-suggest any missing namespaces.

Create an enum with description attributes:

public enum EnumSelectedView
{
    [Description("Drop Down 1")]
    DropDown1 = 0,

    [Description("Drop Down 2")]
    DropDown2 = 1,
}

And a ComboBox:

<ComboBox HorizontalAlignment="Right"
   VerticalAlignment="Top"
   Width="130"
   ItemsSource="{Binding AvailableSelectedViews, Mode=OneWay}"
   SelectedItem="{Binding SelectedView, Mode=TwoWay, Converter={StaticResource enumToDescriptionConverter}}"
</ComboBox>

The converter in XAML needs to be pointed at the C# class. If you are using a UserControl or a Window, it would be UserControl.Resources or Window.Resources.

<DataTemplate.Resources>
    <converters:EnumToDescriptionConverter x:Key="enumToDescriptionConverter" />            
</DataTemplate.Resources>

Add some extension methods and a converter anywhere in your project:

using System;

namespace CMCMarkets.Phantom.CoreUI.Converters
{
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Globalization;
    using System.Linq;
    using System.Reflection;
    using System.Windows.Data;

    public class EnumToDescriptionConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if ((value is Enum) == false) throw new ArgumentException("Error: value is not an enum.");
            return ((Enum)value)?.GetDescriptionAttribute();
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if ((value is string) == false)
            {
                throw new ArgumentException("Error: Value is not a string");
            }
            foreach (var item in Enum.GetValues(targetType))
            {
                var asString = (item as Enum).GetDescriptionAttribute();
                if (asString == (string)value)
                {
                    return item;
                }
            }
            throw new ArgumentException("Error: Unable to match string to enum description.");
        }
    }

    public static class EnumExtensions
    {
        /// <summary>
        /// For a single enum entry, return the [Description("")] attribute.
        /// </summary>
        public static string GetDescriptionAttribute(this Enum enumObj)
        {
            FieldInfo fieldInfo = enumObj.GetType().GetField(enumObj.ToString());
            object[] attribArray = fieldInfo.GetCustomAttributes(false);
            if (attribArray.Length == 0)
            {
                return enumObj.ToString();
            }
            else
            {
                DescriptionAttribute attrib = attribArray[0] as DescriptionAttribute;
                return attrib?.Description;
            }
        }

        /// <summary>
        /// For an enum type, return a list of all possible [Description("")] attributes.
        /// </summary>
        /*
         * Example: List<string> descriptions = EnumExtensions.GetDescriptionAttributeList<MyEnumType>();
         */
        public static List<string> GetDescriptionAttributeList<T>()
        {
            return typeof(T).GetEnumValues().Cast<Enum>().Select(x => x.GetDescriptionAttribute()).ToList();
        }

        /// <summary>
        /// For an enum instance, return a list of all possible [Description("")] attributes.
        /// </summary>
        /*
         * Example:
         *
         * List<string> descriptions = typeof(CryptoExchangePricingOrGraphView).GetDescriptionAttributeList();
         */
        public static List<string> GetDescriptionAttributeList(this Type type)
        {
            return type.GetEnumValues().Cast<Enum>().Select(x => x.GetDescriptionAttribute()).ToList();
        }

        /// <summary>
        /// For an enum instance, return a list of all possible [Description("")] attributes.
        /// </summary>
        /*
         * Example:
         *
         * MyEnumType x;
         * List<string> descriptions = x.GetDescriptionAttributeList();
         */
        public static List<string> GetDescriptionAttributeList(this Enum thisEnum)
        {
            return thisEnum.GetType().GetEnumValues().Cast<Enum>().Select(x => x.GetDescriptionAttribute()).ToList();
        }
    }
}

In your ViewModel:

public IReadOnlyList<string> AvailableSelectedViews { get; }

And in the constructor:

 this.AvailableSelectedViews = typeof(EnumSelectedView).GetDescriptionAttributeList();

The selected item will be bound to this. It uses the converter to go from the string in the combobox straight to the enum. You could also do the conversion inside the property updater by using the extension methods above.

public EnumSelectedView SelectedView { get; set; }
like image 24
Contango Avatar answered Sep 17 '22 14:09

Contango