Search code examples
c#.netautomationui-automationmicrosoft-ui-automation

Subscribe to ComboBox selection changed event with UI Automation


I am new in UI-automatation.
There is an AutomationElement of type ComboBox.
I am looking for a way to subscribe to an event that is raised when the ComboBox changes its Name property.

This is what I'm trying to do but it doesn't work:

Automation.AddAutomationPropertyChangedEventHandler(
    elementComboBox,
    TreeScope.Element,
    new AutomationPropertyChangedEventHandler(OnUIAutomationPropChanged),
    NameProperty
);

Solution

  • It appears you already know how to get to the ComboBox element, so let's just setup an AddAutomationPropertyChangedEventHandler to detect ExpandCollapsePattern.ExpandCollapseStateProperty change events.

    The ComboBox controls are composited objects composed of:

    • The external container,
    • The internal Edit Control, used to enter a selection/search a value, available when the ComboBox is of type DropDown
    • The internal ListControl, used to present the list of items contained in the ComboBox, shown as a the ComboBox drop down element.

    The item selection belongs to the ListControl, it's this control we need to get the value from.
    The List Automation Element supports the SelectionPattern.
    The SelectionPatter.GetSelection() method return a collection of ListItem elements: the Name property of the Item returns the current selection value.

    With the AutomationElement identifying the ComboBox of interest, when can setup an event handler for its ExpandCollapseState property changes like this:

    bool success = SetExpandCollapseEventHandler(elementComboBox);
    // [...]
    // Remove the handler when you're done with it
    RemoveExpandCollapseEventHandler(elementComboBox);
    

    Here, string selectedValue contains the value selected when the ComboBox is closed:
    Of course, you could read the value both when the ExpandCollapseState is Expanded and Collpsed, to compare the current value and the selected one

    private AutomationPropertyChangedEventHandler ExpandCollapsedHandler = null;
    
    public bool SetExpandCollapseEventHandler(AutomationElement element)
    {
        if ((bool)element.GetCurrentPropertyValue(AutomationElement.IsExpandCollapsePatternAvailableProperty)) {
            Automation.AddAutomationPropertyChangedEventHandler(element, TreeScope.Element,
                ExpandCollapsedHandler = new AutomationPropertyChangedEventHandler(OnExpandCollapeChanged),
                ExpandCollapsePattern.ExpandCollapseStateProperty);
            return true;
        }
        return false;
    }
    
    private void OnExpandCollapeChanged(object elm, AutomationPropertyChangedEventArgs e)
    {
        var element = elm as AutomationElement;
        if (element.TryGetCurrentPattern(ExpandCollapsePattern.Pattern, out object value)) {
            var state = (value as ExpandCollapsePattern).Current.ExpandCollapseState;
            if (state == ExpandCollapseState.Collapsed) {
                var listElement = element.FindFirst(TreeScope.Children, 
                    new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.List));
                if (listElement != null) {
                    var selectedItem = (listElement.GetCurrentPattern(
                         SelectionPattern.Pattern) as SelectionPattern).Current.GetSelection().FirstOrDefault();
                    string selectedValue = selectedItem?.Current.Name;
                }
            }
        }
    }
    
    void RemoveExpandCollapseEventHandler(AutomationElement element)
    {
        Automation.RemoveAutomationPropertyChangedEventHandler(element, ExpandCollapsedHandler);
        ExpandCollapsedHandler = null;
    }
    

    ► Subscribe to SelectionItemPattern's ElementSelectedEvent:

    If you need to know when any elements of the ComboBox List is changed/selected when no actual User selection is made using the GUI, you can subscribe to the events raised by the child elements of the List element.
    You need just one handler, attached to a wider scope: TreeScope.Children.

    WARNING: Since these events need to be handled asynchnonously, the events are raised in Threadpool threads.

    This has consequences:

    • you cannot access controls in the UI thread directly (you need to BeginInvoke())
    • You MUST remove the AutomationEventHandler if/when the ComboBox Control is disposed (e.g., when the parent Window is closed), otherwise your app will become unstable and probably stuck eternally waiting for an event to return a value from a Element that, at a certain point, doesn't answer anymore.

    You may want to use a WindowPattern.WindowClosedEvent handler to receive notifications when the parent Window is closed, then call the Automation.RemoveAutomationEventHandler (as shown here), but probably (to evaluate based on the context of the operations) also the Automation.RemoveAllEventHandlers() method.

    Called as:

    bool success = SetListItemChangedEventHandler(elementComboBox);
    // [...]
    // Remove the handler when you're done with it
    RemoveListItemChangedHandler();
    

    private AutomationElement listElement = null;
    private AutomationEventHandler ListItemChangedHandler = null;
    
    public bool SetListItemChangedEventHandler(AutomationElement combobox)
    {
        listElement = combobox.FindFirst(TreeScope.Children,
            new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.List));
    
        if ((bool)listElement.GetCurrentPropertyValue(AutomationElement.IsSelectionPatternAvailableProperty)) {
            Automation.AddAutomationEventHandler(SelectionItemPattern.ElementSelectedEvent, 
                listElement, TreeScope.Children, 
                ListItemChangedHandler = new AutomationEventHandler(OnListItemChanged));
    
            return true;
        }
        return false;
    }
            
    private void OnListItemChanged(object elm, AutomationEventArgs e)
    {
        string selectedValue = (elm as AutomationElement)?.Current.Name;
    }
    
    private void RemoveListItemChangedHandler()
    {
        Automation.RemoveAutomationEventHandler(
            SelectionItemPattern.ElementSelectedEvent, listElement, ListItemChangedHandler);
        ListItemChangedHandler = null;
    }