WPF: How to automatically open/close the DropDown-portion of a ComboBox when the mouse enters/leaves the control

by Olaf Rabbachin 2. January 2011 17:20

Today, I came about a thread where the poster was asking for a way to automatically open a ComboBox'es DropDown/popup when the mouse hovers over the ComboBox. Pretty easy I thought. Oh well ...

Code or XAML

The easiest approach to opening the DropDown portion would be to attach to the ComboBox'es MouseEnter-event and simply doing a ...

 

private void ComboBox_MouseEnter(object sender, MouseEventArgs e)
{
	((ComboBox)sender).IsDropDownOpen = true;
}

 

But the above would mean you'd have to attach the handler to each and every ComboBox you want to use this with. Not neat. Not neat at all. So, given the fact that we can use Triggers for setting a property, a style with a simple Trigger should do the trick (or so I thought):

 

<ComboBox>
  <ComboBox.Resources>
   <Style TargetType="{x:Type ComboBox}">
     <Setter Property="IsDropDownOpen" Value="False"/>
     <Style.Triggers>
      <Trigger Property="IsMouseOver" Value="True">
        <Setter Property="IsDropDownOpen" Value="True"/>
      </Trigger>
     </Style.Triggers>
   </Style>
  </ComboBox.Resources>
  <ComboBoxItem>Item #1</ComboBoxItem>
  <ComboBoxItem>Item #2</ComboBoxItem>
  <ComboBoxItem>Item #3</ComboBoxItem>
  <ComboBoxItem>Item #4</ComboBoxItem>
  <ComboBoxItem>Item #5</ComboBoxItem>
</ComboBox>

 

Actually, this does work and, while the above XAML targets just a single ComboBox, it could as well be applied to all ComboBoxes throughout a Window or a complete application.
However, it'll only work only once. That is, the first time you hover over the ComboBox, the DropDown will open just as expected. But once you closed the DropDown again, it'll never automatically open again for all subsequent hover-actions.

I have tried all sorts of things to get this to work with XAML only, to no avail (I'll spare you from sharing those attempts). Since the XAML-approach really should be the equivalent of the code I posted in the beginning of this post, I wonder whether this actually is a bug ... Well, at least I don't understand as to why this shouldn't work. If you do know, make sure you post a comment!

So, since - from my point of view - it seems unfeasible to have a XAML-only approach, how about using the code, but in a way so that you still have the option of applying this to all ComboBox controls of a Window or even an application without having to attach to the handlers each and every time?

 

Enter the Attached Behavior

An attached behavior will allow us to write code that will execute depending on a condition that can be set from XAML; which means that we could still use a bit of XAML in order to apply the behavior to a single control, all controls within a container or even all controls within the scope of the application.

Now, if you don't know about attached behaviors - Josh Smith posted a nice and concise article on the CodeProject: Introduction to Attached Behaviors. If you find the article doesn't suffice to get you a firm understanding, I'd suggest you google or bing the term - there's probably thousands of articles and tutorials out there, so I won't dupe those here.

 

Get me something that works and stop babbling

Another goal for the attached behavior was that I thought that if you want a ComboBox to automatically hover-open its DropDown portion, you'd probably want to also close it when the mouse leaves the control. Well, again, this proved to be quite tedious. As you might already be tired of reading about things that don't work, I'll skip the babbling here and show you the final result before going into more detail.

Hence and without further ado, let's see what seems to be working reliably. Here's the complete code of an attached behavior that you can simply copy'n'paste into your own project. Note that, if you're developing with VB, you'll find a VB-version of the code below in the accompanying sample solution (see the bottom of this post for the download):

 

using System;
using System.Windows.Controls;
using System.Windows;
using System.Windows.Controls.Primitives;

namespace WpfTests
{
	public static class ComboBox_DropdownBehavior
	{
		/// <summary>
		/// Gets/sets whether or not the ComboBox this behavior is applied to opens its items-popup
		/// when the mouse hovers over it and closes again when the mouse leaves.
		/// </summary>
		public static readonly DependencyProperty OpenDropDownAutomaticallyProperty =
				 DependencyProperty.RegisterAttached(
				 "OpenDropDownAutomatically",
				 typeof(bool),
				 typeof(ComboBox_DropdownBehavior),
				 new UIPropertyMetadata(false, OnOpenDropDownAutomatically_Changed)
			 );

		//DP-getter and -setter
		public static bool GetOpenDropDownAutomatically(ComboBox cbo)
		{
			return (bool)cbo.GetValue(OpenDropDownAutomaticallyProperty);
		}
		public static void SetOpenDropDownAutomatically(ComboBox cbo, bool value)
		{
			cbo.SetValue(OpenDropDownAutomaticallyProperty, value);
		}

		/// <summary>
		/// Fired when the assignment of the behavior changes (IOW, is being turned on or off).
		/// </summary>
		static void OnOpenDropDownAutomatically_Changed(
				DependencyObject doSource, 
				DependencyPropertyChangedEventArgs e
			)
		{
			//The ComboBox that is the target of the assignment
			ComboBox cbo = doSource as ComboBox;
			if (cbo == null)
				return;

			//Just to be safe ...
			if (e.NewValue is bool == false)
				return;

			if ((bool)e.NewValue)
			{
				//Attach
				cbo.MouseMove += cbo_MouseMove;
				cbo.MouseEnter += cbo_MouseEnter;
			}
			else
			{
				//Detach
				cbo.MouseMove -= cbo_MouseMove;
				cbo.MouseEnter -= cbo_MouseEnter;
			}
		}

		static void cbo_MouseMove(object sender, System.Windows.Input.MouseEventArgs e)
		{
			//Get a ref to the ComboBox
			ComboBox cbo = (ComboBox)sender;
			//Get a ref to the ComboBox'es popup (which is what displays the available items)
			Popup p = (Popup)cbo.Template.FindName("PART_Popup", cbo);
			
			//The DropDown/popup is to close when 
			// - it is still open
			// - the mouse is no longer over the popup
			// - the cbo's IsMouseDirectlyOver returns true (which, albeit strange, is true
			//   when the mouse is neither over the popup NOR the cbo itself
			if (cbo.IsDropDownOpen && !p.IsMouseOver && cbo.IsMouseDirectlyOver)
				cbo.IsDropDownOpen = false;
		}

		static void cbo_MouseEnter(object sender, System.Windows.Input.MouseEventArgs e)
		{
			//Open the DropDown/popup as soon as the mouse hovers over the control
			((ComboBox)sender).IsDropDownOpen = true;
		}
	}
}

 

 

A sample Window that utilizes the above behavior

To test out the above behavior, here's a simple-enough Window that contains four ComboBox controls, three of them having the behavior applied:

 

<Window x:Class="CS.ComboBox_AutoOpenDropDown_AttachedBehavior"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:sys="clr-namespace:System;assembly=mscorlib"
        xmlns:local="clr-namespace:CS"
        Title="ComboBox_AutoOpenDropDown_AttachedBehavior (CS)" 
        Width="450" SizeToContent="Height">
   <Window.Resources>
      <!-- Just to provide a list that the ComboBoxes can bind to -->
      <x:Array Type="sys:String" x:Key="stringList">
         <sys:String>Item #1</sys:String>
         <sys:String>Item #2</sys:String>
         <sys:String>Item #3</sys:String>
         <sys:String>Item #4</sys:String>
         <sys:String>Item #5</sys:String>
      </x:Array>

      <!-- Base-style for all ComboBoxes in this Window (so we don't need to repeat this over and over again) -->
      <Style TargetType="{x:Type ComboBox}">
         <Setter Property="Margin" Value="10"/>
         <Setter Property="StaysOpenOnEdit" Value="True"/>
         <Setter Property="ItemsSource" Value="{Binding Source={StaticResource stringList}}"/>
         <!-- Here's where the attached behavior is applied -->
         <Setter Property="local:ComboBox_DropdownBehavior.OpenDropDownAutomatically" Value="True"/>
      </Style>
   </Window.Resources>

   <Grid>
      <Grid.RowDefinitions>
         <RowDefinition Height="Auto"/>
         <RowDefinition Height="Auto"/>
      </Grid.RowDefinitions>
      <Grid.ColumnDefinitions>
         <ColumnDefinition Width="*"/>
         <ColumnDefinition Width="*"/>
      </Grid.ColumnDefinitions>

      <ComboBox />
      <ComboBox Grid.Row="1"/>

      <!-- 
         Let's have one ComboBox behave as per default (i.e., without the attached behavior). 
         Since the behavior is being applied to *all* ComboBoxes (See the Window.Resources section),
         we'll explicitly turn it off here.
      -->
      <Grid Grid.Column="1">
         <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="*"/>
         </Grid.ColumnDefinitions>
         <TextBlock Text="No auto-open/-close:" Margin="0,0,5,0" VerticalAlignment="Center"/>
         <ComboBox Grid.Column="1" Background="LightPink" 
                   local:ComboBox_DropdownBehavior.OpenDropDownAutomatically="False"/>
      </Grid>
      <ComboBox Grid.Column="1" Grid.Row="1"/>
   </Grid>
</Window>

 

(You can also find the above Window in the sample solution - see the bottom for the download-link.)

 

What's all the fuzz (or: still not tired of reading?)

As I wrote further up, creating the behavior was quite tedious (for me, anyway). That is, closing the DropDown portion by simply attaching to the MouseLeave-event of the ComboBox wouldn't work. The reason is actually quite simple - the DropDown portion of the ComboBox control (IOW, the PART_popup as found in the ControlTemplate) is a popup control which sort of usurps events as well as the focus once it has been opened/shown, hiding them for the ComboBox control itself. So, once the DropDown is visible, the ComboBox'es MouseLeave-event will not be fired until the DropDown portion has again been closed. As a result, I needed to find a different way of triggering the code that would do the required cbo.IsDropDownOpen = false. So the core of the problem really is to sort of have the MouseLeave-event be triggered for both the ComboBox itself and its DropDown portion. The only reliable and not-all-too-verbose way I could find was to actually use the ComboBox'es MouseMove-event instead. Here's the code of the corresponding handler (again):

 

static void cbo_MouseMove(object sender, System.Windows.Input.MouseEventArgs e)
{
	//Get a ref to the ComboBox
	ComboBox cbo = (ComboBox)sender;
	//Get a ref to the ComboBox'es popup (which is what displays the available items)
	Popup p = (Popup)cbo.Template.FindName("PART_Popup", cbo);

	//The DropDown/popup is to close when 
	// - it is still open
	// - the mouse is no longer over the popup
	// - the cbo's IsMouseDirectlyOver returns true (which, albeit strange, is true when the mouse
	//   neither over the popup NOR the cbo itself
	if (cbo.IsDropDownOpen && !p.IsMouseOver && cbo.IsMouseDirectlyOver)
		cbo.IsDropDownOpen = false;
}

 

So what happens is that, taking the reference to the ComboBox in question, the code creates a reference to the DropDown portion resp. the underlying Popup-control and checks to see whether the mouse is over the popup or the ComboBox control itself, closing the DropDown if the mouse is off of the control. Now you might be wondering whether there isn't a typo. Namely, shouldn't the cbo.IsMouseDirectlyOver (or cbo.IsMouseDirectlyOver == true) really be a !cbo.IsMouseDirectlyOver (or cbo.IsMouseDirectlyOver == false)? The answer is no. Thing is, frankly, that I have no idea as to why the IsMouseDirectlyOver property returns true when the mouse pointer is neither over the popup nor over the ComboBox control itself, but that's how it is and what I had to find out while debugging the code, trying to find out whether I'm still sane. That having been said, the code above does the trick, albeit having some sort of a strange flavor attached. Again, if you can shed some light regarding the reason for this, I'd be happy to see your comment (I spent enough time on this for a Sunday afternoon anyway Foot in mouth). FWIW, I can't find any problems with the behavior as it is now, which means that it passed the few (manuak) tests I did - I haven't yet used this is a productive app.

 

The sample solution

I’ve created a sample solution that contains everything discussed here, containing one project for each the C# and the VB versions.

Download: ComboBox_AutoOpenDropDown_AttachedBehavior.zip (29.50 kb)

 

UPDATE:

As per a private request on 08/15 2011, I've added another sample solution, for those of you still sticking with VS 2008.

Download: ComboBox_AutoOpenDropDown_AttachedBehavior_VS2008.zip (127.61 kb)


Location: SinglePost

Tags: , , , , ,

WPF (.net)

Comments


United Kingdom Richard 
February 24. 2011 17:55
Richard
A good explanation of what I think the problem is can be found here: blogs.msdn.com/.../...ontrol-local-values-bug.aspx

As far as I understand the original problem comes from the way dependency properties have their values set.  There's an order of precedence with 'Local Value' being at the top.  The problem is that when ever you open the drop down it sets the property value locally and immediately overrides any bindings or triggers you have set up.  After that point the only way you can affect the value is to set it manually as you discovered.  It's highly inconvenient!


February 24. 2011 18:16
Olaf Rabbachin
Hi Richard,

thanks for the heads up! I only skimmed over the the post you linked, but it sure looks promising with respect to a possible/reasonable explanation. I'll have to deeper dig into things there ...

Cheers,
Olaf

Add comment


(Will show your Gravatar icon)

  Country flag

Click to change captcha   

biuquote
  • Comment
  • Preview
Loading



About

Hi and welcome to my blog!

I'm a developer from Germany, currently focusing on .Net and WPF.

More about me ...