6

When the user is selecting values from a combobox, if they choose a value, the "SelectionChanged" event fires and the new value is set and everything's fine. If, however, they decide not to change the value and click elsewhere on the UI (like a text box they want to edit), they have to click twice - the first click simply closes the combobox popup, and the next click will focus the element they wanted to activate on the first click.

How can I prevent the combobox popup from hijacking the focus target on the first click like that?

I've tried monitoring the ComboBox_LostFocus event, but this fires at the wrong time. When the user clicks the dropdown and the popup list is displayed, the ComboBox_LostFocus event fires - it's losing focus to it's own dropdown list. I don't want to do anything to change that. When the user then clicks away and the popup closes, the ComboBox never regains focus (focus is just 'lost' to everything) and so this event is useless.

Alain
  • 26,663
  • 20
  • 114
  • 184
  • Hi @Alain, the problem here is that you are trying to deviate from the usual behaviour of a standard control.. Even if you think it's better to do it the way you describe, it will be inconsistent with how people are used to comboboxes working, which in most cases is a bad idea. – joshuahealy Mar 14 '12 at 20:52
  • 1
    The real problem here is that the user is complaining that they "need to click twice before the UI responds after viewing a combobox list". If I just go back to the user and tell them that's by design, they will find another designer. – Alain Mar 14 '12 at 21:03
  • That's why I said in MOST cases... as we've all had to deal with customers before... Hopefully some WPF guru can help! – joshuahealy Mar 14 '12 at 21:26
  • I'd try 2 things: 1st, try hacking into `ComboBox` to see if you can get at its internal `Popup`. If not, extend `ComboBox` and implement the appropriate functionality. It's probably gonna take some `Mouse` hacking. Plz post a solution if you find one.. – Jake Berger Mar 14 '12 at 21:33

2 Answers2

7

I think I might have found a solution. Comboboxes do have a DropDownClosed event - the problem is it isn't a RoutedEvent, so you can't create a style for ComboBoxes and have them all inherit the event via an EventSetter. (You get the error 'DropDownClosed' must be a RoutedEvent registered with a name that ends with the keyword "Event")

However, the Loaded event is a RoutedEvent, so we can hook into that in the style:

<Style x:Key="ComboBoxCellStyle" TargetType="ComboBox">
    <EventSetter Event="Loaded" Handler="ComboBox_Loaded" />
</Style>

Now that we have an event that will always fire before anything else is done with the ComboBox, we can hook into the event we actually care about:

private void ComboBox_Loaded(object sender, RoutedEventArgs e)
{
    ((ComboBox)sender).DropDownClosed -= ComboBox_OnDropDownClosed;
    ((ComboBox)sender).DropDownClosed += new System.EventHandler(ComboBox_OnDropDownClosed);
}

Now that I finally have access to the event that fires when the DropDown is closing, I can perform whatever actions I need to make sure the focus is terminated on the bothersome ComboBox. In my case, the following:

void ComboBox_OnDropDownClosed(object sender, System.EventArgs e)
{
    FrameworkElement visualElement = (FrameworkElement)sender;

    while( visualElement != null && !(visualElement is DataCell) )
        visualElement = (FrameworkElement)visualElement.TemplatedParent;
    if( visualElement is DataCell )
    {
        DataCell dataCell = (DataCell)visualElement;
        dataCell.EndEdit();
        if( !(dataCell.ParentRow is InsertionRow) ) dataCell.ParentRow.EndEdit();
    }
}

I had a ComboBox as the template of a DataCell in a GridView, and this particular problem was preventing the DataRow from ending edit when the user popped open a ComboBox then clicked outside of the grid.

That was my biggest problem with this bug. A secondary problem setting the focus in this event iff the user clicked. The combobox might also have just been closed because the user hit tab or escape though, so we can't just setfocus to the mouse position. We'd need more information on what caused the DropDownClosed event to fire. Probably means hooking into more unrouted events in the _Loaded event handler.

Alain
  • 26,663
  • 20
  • 114
  • 184
2

There's a DropDownClosed event:

private void comboBox_DropDownClosed(object sender, EventArgs e)
{
    Point m = Control.MousePosition;
    Point p = this.PointToClient(m);
    Control c = this.GetChildAtPoint(p);
    c.Focus();
}

This will only set focus to whatever control they clicked on. If they click a TextBox, for instance, the caret will be at the left rather than where they clicked. If they click another ComboBox, it'll focus there, but it won't show its popup. However, I'm sure you could deal with those cases in this event handler if you need to.

EDIT: Whoops, you're using WPF! Nevermind, then; this is how you'd do it in WinForms. However, you've still got the DropDownClosed event in WPF.

EDIT 2: This seems to do it. I'm not familiar with WPF so I don't know how robust it is, but it'll focus on a TextBox, for example. This is a default WPF app with a Window called MainWindow. When you close the DropDown of the comboBox, it'll focus the top-most focusable Control at the mouse position that isn't MainWindow:

private void comboBox_DropDownClosed(object sender, EventArgs e)
{
    Point m = Mouse.GetPosition(this);
    VisualTreeHelper.HitTest(this, new HitTestFilterCallback(FilterCallback),
        new HitTestResultCallback(ResultCallback), new PointHitTestParameters(m));
}

private HitTestFilterBehavior FilterCallback(DependencyObject o)
{
    var c = o as Control;
    if ((c != null) && !(o is MainWindow))
    {
        if (c.Focusable)
        {
            c.Focus();
            return HitTestFilterBehavior.Stop;
        }
    }
    return HitTestFilterBehavior.Continue;
}

private HitTestResultBehavior ResultCallback(HitTestResult r)
{
    return HitTestResultBehavior.Continue;
}
Jeff
  • 7,504
  • 3
  • 25
  • 34