Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I conditionally style only the text in the selected item of a ComboBox?

Tags:

wpf

I have a situation where I need to style the selected item in a ComboBox differently (make the text bold) when it is one of all except one value. For example, in the drop-down box labelled "What is your favourite primary colour?" I would have four options: No Preference, Red, Green, and Blue. The ComboBox items are just text with default styling, no images or anything else fancy, and are C# classes, not wrapped in ComboBoxItems.

When the user specifies a preference from the list, I want to highlight that choice by setting the text of the selected item in the collapsed list to be bold. If the user chooses No Preference, the font weight should remain normal.

I have achieved a 90% solution by setting the FontWeight property on the ComboBox to Bold in a Style with a DataTrigger defined as SelectedItem != No Preference. However, this styles all items in the ComboBox's list of items, including all those in the drop-down list. I would like those items to always be displayed with a normal font weight.

Is this possible?

Edit

I have been trying @crazyarabian's method of styling the ComboBoxItem with a MultiTrigger. The style definition is:

<Style x:Key="SelectedItemStyle">
    <Setter Property="ComboBoxItem.FontWeight" Value="Normal" />
    <Style.Triggers>
        <MultiTrigger>
            <MultiTrigger.Conditions>
                <Condition Property="ComboBoxItem.IsSelected" Value="True" />
                <Condition Binding="{Binding IsNoPreferenceSelected,Mode=OneWay}" Value="False" />
            </MultiTrigger.Conditions>
            <Setter Property="ComboBoxItem.FontWeight" Value="Bold" />
        </MultiTrigger>
    </Style.Triggers>
</Style>

and it is applied to a ComboBox in the following DataTemplate:

<DataTemplate x:Key="PrimaryColoursTemplate" DataType="{x:Type ViewModels:PrimaryColoursViewModel}">
    <ComboBox ItemsSource="{Binding PrimaryColours}" SelectedItem="{Binding SelectedPrimaryColour}"
              ItemContainerStyle="{StaticResource SelectedItemStyle}" />
</DataTemplate>

Unfortunately, this kills WPF:

System.Windows.Data Error: 8 : Cannot save value from target back to source. BindingExpression:Path=IsDropDownOpen; DataItem='ComboBox' (Name=''); target element is 'ToggleButton' (Name=''); target property is 'IsChecked' (type 'Nullable`1') InvalidOperationException:'System.InvalidOperationException: Must have non-null value for 'Property'.

The application dies with a NullReferenceException, which is thrown after the InvalidOperationException above (or perhaps leads to it, I can't decipher the output). The only thing I can think of that might be causing this is resolving the property in the binding in my second MultiTrigger condition, but I don't get any binding errors at all. Here's the top of the stack trace in case that helps too:

InvalidOperationException:'System.InvalidOperationException: Must have non-null value for 'Property'.
   at System.Windows.Condition.Seal(ValueLookupType type)
   at System.Windows.ConditionCollection.Seal(ValueLookupType type)
   at System.Windows.MultiTrigger.Seal()
   at System.Windows.TriggerCollection.Seal()
   at System.Windows.Style.Seal()
   at System.Windows.StyleHelper.UpdateStyleCache(FrameworkElement fe, FrameworkContentElement fce, Style oldStyle, Style newStyle, Style& styleCache)
   at System.Windows.FrameworkElement.OnStyleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
   at System.Windows.DependencyObject.OnPropertyChanged(DependencyPropertyChangedEventArgs e)
   at System.Windows.FrameworkElement.OnPropertyChanged(DependencyPropertyChangedEventArgs e)
   at System.Windows.DependencyObject.NotifyPropertyChange(DependencyPropertyChangedEventArgs args)
   at System.Windows.DependencyObject.UpdateEffectiveValue(EntryIndex entryIndex, DependencyProperty dp, PropertyMetadata metadata, EffectiveValueEntry oldEntry, EffectiveValueEntry& newEntry, Boolean coerceWithDeferredReference, Boolean coerceWithCurrentValue, OperationType operationType)
   at System.Windows.DependencyObject.SetValueCommon(DependencyProperty dp, Object value, PropertyMetadata metadata, Boolean coerceWithDeferredReference, Boolean coerceWithCurrentValue, OperationType operationType, Boolean isInternal)
   at System.Windows.DependencyObject.SetValue(DependencyProperty dp, Object value)
   at System.Windows.Controls.ItemsControl.ApplyItemContainerStyle(DependencyObject container, Object item)
like image 293
alastairs Avatar asked Jul 21 '11 21:07

alastairs


2 Answers

There is no need to get into anything as despicable as owner-draw-- we are talking about WPF here, not WinForms. In WinForms, your only solution was to write more code. In WPF, we can solve this problem with a few very simple custom templates. For this example, I used Kaxaml, a free light-weight XAML editor. No code-behind was required. Kaxaml comes packed with a bunch of "starter" styles called Simple Styles. I used the ComboBox Simple Style and made modifications from that. So although this looks like a lot of XAML, I really just started with the boilerplate one and added a couple lines.

You can probably think of more elegant ways of triggering the font weight change; I used SelectedIndex.

<Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
   <Page.Resources>
      <DataTemplate x:Key="SelectionBoxTextTemplate">
         <TextBlock FontWeight="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ComboBox}}, Path=FontWeight}" Text="{Binding}"/>
      </DataTemplate>
      <ControlTemplate x:Key="ComboBoxToggleButton" TargetType="{x:Type ToggleButton}">
         <Grid>
            <Grid.ColumnDefinitions>
               <ColumnDefinition/>
               <ColumnDefinition Width="20"/>
            </Grid.ColumnDefinitions>
            <Border
               x:Name="Border"
               Grid.ColumnSpan="2"
               Background="#C0C0C0"
               BorderBrush="#404040"
               BorderThickness="1"
               CornerRadius="2"/>
            <Border
               Grid.Column="0"
               Margin="1"
               Background="#FFFFFF"
               BorderBrush="#404040"
               BorderThickness="0,0,1,0"
               CornerRadius="2,0,0,2"/>
            <Path
               x:Name="Arrow"
               Grid.Column="1"
               HorizontalAlignment="Center"
               VerticalAlignment="Center"
               Data="M 0 0 L 4 4 L 8 0 Z"
               Fill="#404040"/>
         </Grid>
         <ControlTemplate.Triggers>
            <Trigger Property="ToggleButton.IsMouseOver" Value="true">
               <Setter TargetName="Border" Property="Background" Value="#808080"/>
            </Trigger>
            <Trigger Property="ToggleButton.IsChecked" Value="true">
               <Setter TargetName="Border" Property="Background" Value="#E0E0E0"/>
            </Trigger>
            <Trigger Property="IsEnabled" Value="False">
               <Setter TargetName="Border" Property="Background" Value="#EEEEEE"/>
               <Setter TargetName="Border" Property="BorderBrush" Value="#AAAAAA"/>
               <Setter Property="Foreground" Value="#888888"/>
               <Setter TargetName="Arrow" Property="Fill" Value="#888888"/>
            </Trigger>
         </ControlTemplate.Triggers>
      </ControlTemplate>
      <Style x:Key="{x:Type ComboBox}" TargetType="{x:Type ComboBox}">
         <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="MinWidth" Value="120"/>
         <Setter Property="MinHeight" Value="20"/>
         <Setter Property="Template">
            <Setter.Value>
               <ControlTemplate TargetType="{x:Type ComboBox}">
                  <Grid>
                     <ToggleButton
                        Name="ToggleButton"
                        Grid.Column="2"
                        ClickMode="Press"
                        Focusable="false"
                        IsChecked="{Binding Path=IsDropDownOpen,Mode=TwoWay,RelativeSource={RelativeSource TemplatedParent}}"
                        Template="{StaticResource ComboBoxToggleButton}">
                     </ToggleButton>
                     <ContentPresenter
                        Name="ContentSite"
                        HorizontalAlignment="Left"
                        Margin="3,3,23,3"
                        VerticalAlignment="Center"
                        Content="{TemplateBinding SelectionBoxItem}"
                        ContentTemplate="{StaticResource SelectionBoxTextTemplate}"
                        ContentTemplateSelector="{TemplateBinding ItemTemplateSelector}"
                        IsHitTestVisible="False"/>
                     <TextBox
                        x:Name="PART_EditableTextBox"
                        HorizontalAlignment="Left"
                        Margin="3,3,23,3"
                        VerticalAlignment="Center"
                        Background="Transparent"
                        Focusable="False"
                        IsReadOnly="{TemplateBinding IsReadOnly}"
                        Style="{x:Null}"
                        Visibility="Hidden"/>
                     <Popup
                        Name="Popup"
                        AllowsTransparency="True"
                        Focusable="False"
                        IsOpen="{TemplateBinding IsDropDownOpen}"
                        Placement="Bottom"
                        PopupAnimation="Slide">
                        <Grid
                           Name="DropDown"
                           MaxHeight="{TemplateBinding MaxDropDownHeight}"
                           MinWidth="{TemplateBinding ActualWidth}"
                           SnapsToDevicePixels="True">
                           <Border
                              x:Name="DropDownBorder"
                              Background="#FFFFFF"
                              BorderBrush="#888888"
                              BorderThickness="1"/>
                           <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="IsEnabled" Value="false">
                        <Setter Property="Foreground" Value="#888888"/>
                     </Trigger>
                     <Trigger Property="IsGrouping" Value="true">
                        <Setter Property="ScrollViewer.CanContentScroll" Value="false"/>
                     </Trigger>
                     <Trigger Property="Popup.AllowsTransparency" SourceName="Popup" Value="true">
                        <Setter TargetName="DropDownBorder" Property="CornerRadius" Value="4"/>
                        <Setter TargetName="DropDownBorder" Property="Margin" Value="0,2,0,0"/>
                     </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>
                     <Trigger Property="SelectedIndex" Value="1">
                        <Setter Property="FontWeight" Value="Bold"/>
                     </Trigger>
                     <Trigger Property="SelectedIndex" Value="2">
                        <Setter Property="FontWeight" Value="Bold"/>
                     </Trigger>
                     <Trigger Property="SelectedIndex" Value="3">
                        <Setter Property="FontWeight" Value="Bold"/>
                     </Trigger>
                  </ControlTemplate.Triggers>
               </ControlTemplate>
            </Setter.Value>
         </Setter>
      </Style>
      <Style x:Key="{x:Type ComboBoxItem}" TargetType="{x:Type ComboBoxItem}">
         <Setter Property="SnapsToDevicePixels" Value="true"/>
         <Setter Property="OverridesDefaultStyle" Value="true"/>
         <Setter Property="FontWeight" Value="Normal"/>
         <Setter Property="Template">
            <Setter.Value>
               <ControlTemplate TargetType="{x:Type ComboBoxItem}">
                  <Border Name="Border" Padding="2" SnapsToDevicePixels="true">
                     <ContentPresenter/>
                  </Border>
                  <ControlTemplate.Triggers>
                     <Trigger Property="IsHighlighted" Value="true">
                        <Setter TargetName="Border" Property="Background" Value="#DDDDDD"/>
                     </Trigger>
                     <Trigger Property="IsEnabled" Value="false">
                        <Setter Property="Foreground" Value="#888888"/>
                     </Trigger>
                  </ControlTemplate.Triggers>
               </ControlTemplate>
            </Setter.Value>
         </Setter>
      </Style>
   </Page.Resources>
   <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
      <TextBlock Margin="5" Text="What is your favorite primary colour?"/>
      <ComboBox Width="150" SelectedIndex="0">
         <ComboBoxItem>No Preference</ComboBoxItem>
         <ComboBoxItem>Red</ComboBoxItem>
         <ComboBoxItem>Green</ComboBoxItem>
         <ComboBoxItem>Blue</ComboBoxItem>
      </ComboBox>
   </StackPanel>
</Page>

I used the ContentTemplate property of the ContentPresenter in ComboBox to add a custom data template (SelectionBoxTextTemplate). That TextBlock grabs its FontWeight from an ancestor combo-box. I then added a template for the individual items, that forces them to normal font weight. This got the result you were looking for:

enter image description here

like image 193
Charlie Avatar answered Nov 11 '22 22:11

Charlie


You need to apply your trigger to the ComboBoxItem itself. Unless you've changed the behavior of the ComboBox, all items are displayed within containers (which are used to apply styles and templates) and the default container used by a ComboBox is a ComboBoxItem.

<Style TargetType="ComboBoxItem">
  <Style.Triggers>
    <Trigger Property="IsSelected" Value="True">
      <Setter Property="FontWeight" Value="Bold" />
    </Trigger>
  </Style.Triggers>
</Style>

You'll need to add your existing triggers to compensate for not bolding the selection when it is No Preference.

like image 36
sellmeadog Avatar answered Nov 11 '22 21:11

sellmeadog