Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to get a combobox to appropriately set focus directly after the popup closes

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.

like image 385
Alain Avatar asked Mar 14 '12 20:03

Alain


2 Answers

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.

like image 151
Alain Avatar answered Nov 14 '22 06:11

Alain


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;
}
like image 2
Jeff Avatar answered Nov 14 '22 06:11

Jeff