Detecting highlighted dropdown item in WPF

October 29th 2021 WPF

I am not doing much WPF development lately. When I was recently tasked with helping with a feature, I had to refresh my memory a bit about bindings and attached properties. These notes are intended as a future reference for me, but they might be helpful for others as well.

I needed to recognise the currently highlighted item in a combo box so I could display expanded information about it elsewhere in the window:

Details about the currently highlighted item

The combo box does not have a property with the value of the currently highlighted item, so I had to attach an event handler to the combo box item's MouseMove event:

<ComboBox ItemsSource="{Binding Items}"
          SelectedItem="{Binding SelectedItem}"
          DisplayMemberPath="Label"
          SelectedValuePath="Id"
          ToolTip="{Binding SelectedItem.Description}">
    <ComboBox.ItemContainerStyle>
        <Style TargetType="{x:Type ComboBoxItem}">
            <EventSetter Event="MouseMove" Handler="ComboBoxItem_MouseMove" />
        </Style>
    </ComboBox.ItemContainerStyle>
</ComboBox>

The event handler in the code-behind file extracted the item and passed its value to the view model so it could be used in bindings:

private void ComboBoxItem_MouseMove(object sender, MouseEventArgs e)
{
    ViewModel.HighlightedItem = (sender as ComboBoxItem)?.DataContext as Item;
}

This was already a working solution. To increase reusability (and brush up on my WPF skills), I decided to create an attached property for it so I could get rid of the event handler in the code-behind file. I started with the following:

public static class ComboBoxItemHighlightBehavior
{
    public static readonly DependencyProperty HighlightedItemProperty =
        DependencyProperty.RegisterAttached(
            "HighlightedItem",
            typeof(object),
            typeof(ComboBoxItemHighlightBehavior));

    public static void SetHighlightedItem(UIElement element, object? value)
    {
        element.SetValue(HighlightedItemProperty, value);
    }

    public static object? GetHighlightedItem(UIElement element)
    {
        return element.GetValue(HighlightedItemProperty);
    }

    private static void OnMouseMove(object sender, MouseEventArgs e)
    {
        if (sender is not ComboBoxItem comboBoxItem)
        {
            return;
        }

        SetHighlightedItem(comboBoxItem, comboBoxItem.DataContext);
    }
}

This allowed me to bind the view model property to the newly created attached property:

<ComboBox.ItemContainerStyle>
    <Style TargetType="{x:Type ComboBoxItem}">
        <Setter Property="local:ComboBoxItemHighlightBehavior.HighlightedItem"
                Value="{Binding DataContext.HighlightedItem,
                        Mode=OneWayToSource,
                        RelativeSource={RelativeSource
                                        AncestorType={x:Type Window}}}" />
    </Style>
</ComboBox.ItemContainerStyle>

However, the OnMouseMove event handler was never called. I needed a way to attach it to the combo box items. Attached properties support calling a callback function when their value changes, but that does not solve the problem. The value of the attached property is only changed by the OnMouseMove event handler, so it can not be used to attach the same handler.

As far as I can tell, the only way to attach the OnMouseMove event handler is to create another attached property that acts as a switch for that functionality. When the value of this property changes to true, said event handler can be attached:

public static readonly DependencyProperty EnabledProperty =
    DependencyProperty.RegisterAttached(
        "Enabled",
        typeof(bool),
        typeof(ComboBoxItemHighlightBehavior),
        new PropertyMetadata(false, OnEnabledChange));

public static void SetEnabled(UIElement element, bool value)
{
    element.SetValue(EnabledProperty, value);
}

public static bool EnabledItem(UIElement element)
{
    return (bool)element.GetValue(EnabledProperty);
}

private static void OnEnabledChange(
    DependencyObject d,
    DependencyPropertyChangedEventArgs e)
{
    if (d is not ComboBoxItem comboBoxItem)
    {
        return;
    }

    comboBoxItem.MouseMove -= OnMouseMove;
    if (e.NewValue is true)
    {
        comboBoxItem.MouseMove += OnMouseMove;
    }
}

It is important that the new attached property defaults to false to ensure that the callback is called when it is set to true:

<ComboBox.ItemContainerStyle>
    <Style TargetType="{x:Type ComboBoxItem}">
        <Setter Property="local:ComboBoxItemHighlightBehavior.Enabled"
                Value="True" />
        <Setter Property="local:ComboBoxItemHighlightBehavior.HighlightedItem"
                Value="{Binding DataContext.HighlightedItem,
                        Mode=OneWayToSource,
                        RelativeSource={RelativeSource
                                        AncestorType={x:Type Window}}}" />
    </Style>
</ComboBox.ItemContainerStyle>

If you set these two attached properties for any combo box, the value of the highlighted item can be bound to a property of the view model, making it easy to reuse the functionality.

You can find the code for a working example application in my GitHub repository. The last commit uses the attached properties approach, the one before that uses an event handler in the code-behind file.

Although the MVVM pattern is recommended for WPF applications, sometimes it's easier to implement functionality by creating an event handler in the code-behind file. There is nothing wrong with that. For one-off solutions, it may not be worth writing all the extra code for an attached property. However, if you want to reuse the same functionality in multiple places, creating an attached property will make your code easier to maintain in the future.

Get notified when a new blog post is published (usually every Friday):

If you're looking for online one-on-one mentorship on a related topic, you can find me on Codementor.
If you need a team of experienced software engineers to help you with a project, contact us at Razum.
Copyright
Creative Commons License