Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I use UI Automation on a WPF ItemsControl that groups items?

I am using Microsoft UI Automation (i.e. AutomationElement) to run automated acceptance tests against my application. This has gone well, but I've hit a situation that doesn't appear to be exposed to the automation framework.

I have an ItemsControl (although I could be using one of its derived controls, e.g. ListBox) and I am using CollectionViewSource to group items. Here is a complete window to demonstrate:

<Window x:Class="GroupAutomation.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Orchestra">
    <Window.Resources>

        <!-- Take some simple data -->
        <XmlDataProvider x:Key="SampleData" XPath="Orchestra/Instrument">
            <x:XData>
                <Orchestra xmlns="">
                    <Instrument Name="Flute" Category="Woodwind" />
                    <Instrument Name="Trombone" Category="Brass" />
                    <Instrument Name="French horn" Category="Brass" />
                </Orchestra>
            </x:XData>
        </XmlDataProvider>

        <!-- Add grouping -->
        <CollectionViewSource Source="{Binding Source={StaticResource SampleData}}" x:Key="GroupedView">
            <CollectionViewSource.GroupDescriptions>
                <PropertyGroupDescription PropertyName="@Category" />
            </CollectionViewSource.GroupDescriptions>
        </CollectionViewSource>
    </Window.Resources>

    <!-- Show it in an ItemsControl -->
    <ItemsControl ItemsSource="{Binding Source={StaticResource GroupedView}}" HorizontalAlignment="Left" Margin="4">
        <ItemsControl.GroupStyle>
            <GroupStyle>
                <GroupStyle.HeaderTemplate>
                    <DataTemplate>
                        <TextBlock Text="{Binding Path=Name}" FontWeight="Bold" />
                    </DataTemplate>
                </GroupStyle.HeaderTemplate>
            </GroupStyle>
        </ItemsControl.GroupStyle>
        <ItemsControl.ItemTemplate>
            <DataTemplate>
                <Border Padding="4" Margin="4" Background="#FFDEDEDE">
                    <StackPanel>
                        <Label Content="{Binding XPath=@Name}" />
                        <Button Content="Play" />
                    </StackPanel>
                </Border>
            </DataTemplate>
        </ItemsControl.ItemTemplate>
    </ItemsControl>
</Window>

This produces a window containing the items grouped into their categories, and each item has a button that I'd like to click with UI Automation:

Screenshot of window with a list
(source: brizzly.com)

However, if I look in UISpy.exe (or navigate with AutomationElement) I only see the groups (even in the Raw view):

UISpy
(source: brizzly.com)

As you can see, the groups are there but they contain no items, so there is nowhere to look for the buttons. I have tried this in both WPF 3.5 SP1 and WPF 4.0 and get the same result.

Is it possible to use UI Automation on items that are grouped, and if so, how?

like image 332
GraemeF Avatar asked May 05 '10 09:05

GraemeF


2 Answers

I came across this problem and managed to solve it by implementing a 'GenericAutomationPeer' from http://www.colinsalmcorner.com/post/genericautomationpeer--helping-the-coded-ui-framework-find-your-custom-controls and adding a special case for GroupItems.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Automation;
using System.Windows.Automation.Peers;
using System.Windows.Media;
using System.Xml;

namespace ClassLibrary1
{
    public class MyItemsControl : ItemsControl
    {
        protected override AutomationPeer OnCreateAutomationPeer()
        {
            return new GenericAutomationPeer(this);
        }
    }

    public class GenericAutomationPeer : UIElementAutomationPeer
    {
        public GenericAutomationPeer(UIElement owner) : base(owner)
        {
        }

        protected override List<AutomationPeer> GetChildrenCore()
        {
            var list = base.GetChildrenCore();
            list.AddRange(GetChildPeers(Owner));
            return list;
        }

        private List<AutomationPeer> GetChildPeers(UIElement element)
        {
            var list = new List<AutomationPeer>();
            for (int i = 0; i < VisualTreeHelper.GetChildrenCount(element); i++)
            {
                var child = VisualTreeHelper.GetChild(element, i) as UIElement;
                if (child != null)
                {
                    AutomationPeer childPeer;
                    if (child is GroupItem)
                    {
                        childPeer = new GenericAutomationPeer(child);
                    }
                    else
                    {
                        childPeer = UIElementAutomationPeer.CreatePeerForElement(child);
                    }
                    if (childPeer != null)
                    {
                        list.Add(childPeer);
                    }
                    else
                    {
                        list.AddRange(GetChildPeers(child));
                    }
                }
            }
            return list;
        }
    }

}

I hope this helps anyone still searching for an answer!

like image 143
Trasvi Avatar answered Sep 20 '22 22:09

Trasvi


I'm not 100% sure about buttons, but TextBlock controls that are inside DataTemplates do not get put into the UI Automation tree. Apparently this is an optimization to avoid 1000's of unneccessary textblocks.

You can work around it by SubClassing TextBlock. Here's mine:

public class AutomatableTextBlock : TextBlock
{
    protected override AutomationPeer OnCreateAutomationPeer()
    {
        return new AutomatableTextBlockAutomationPeer(this);
    }

    class AutomatableTextBlockAutomationPeer : TextBlockAutomationPeer
    {
        public AutomatableTextBlockAutomationPeer(TextBlock owner)
            : base(owner)
        { }

        protected override bool IsControlElementCore()
        { return true; }
    }
}

Note: UI Automation also doesn't expose various other controls like Canvas, Panel, you can get them to show up with a similar subclass.

In saying that, I'm not sure why the Button isn't appearing.... Hrmmm

like image 43
Orion Edwards Avatar answered Sep 21 '22 22:09

Orion Edwards