-->

UI Automation not working for DataGridView

2019-05-11 04:35发布

问题:

After trying out several solutions, I am in desperate need for help.

I tried several approaches, before finally copying and still being stuck with the solution from Getting full contents of a Datagrid using UIAutomation.

Let's talk code, please consider the comments:

// Get Process ID for desired window handle
uint processID = 0;
GetWindowThreadProcessId(hwnd, out processID);

var desktop = AutomationElement.RootElement;

// Find AutomationElement for the App's window
var bw = AutomationElement.RootElement.FindFirst(TreeScope.Children, new PropertyCondition(AutomationElement.ProcessIdProperty, (int)processID));

// Find the DataGridView in question
var datagrid = bw.FindFirst(TreeScope.Children, new PropertyCondition(AutomationElement.AutomationIdProperty, "dgvControlProperties"));

// Find all rows from the DataGridView
var loginLines = datagrid.FindAll(TreeScope.Children, new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.DataItem));

// Badumm Tzzz: loginLines has 0 items, foreach is therefore not executed once 

foreach (AutomationElement loginLine in loginLines)
{
    var loginLinesDetails = loginLine.FindAll(TreeScope.Children, new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.Custom));

    for (var i = 0; i < loginLinesDetails.Count; i++)
    {
        var cacheRequest = new CacheRequest 
        { 
            AutomationElementMode = AutomationElementMode.None,
            TreeFilter = Automation.RawViewCondition
        };

        cacheRequest.Add(AutomationElement.NameProperty);
        cacheRequest.Add(AutomationElement.AutomationIdProperty);

        cacheRequest.Push();

        var targetText = loginLinesDetails[i].FindFirst(TreeScope.Children, new PropertyCondition(AutomationElement.ClassNameProperty, "TextBlock"));

        cacheRequest.Pop();

        var myString = targetText.Cached.Name;
    }

}

I can neither fetch a GridPattern, nor a TablePattern instance from datagrid, both results in an exception:

GridPattern gridPattern = null;

try
{
    gridPattern = datagrid.GetCurrentPattern(GridPattern.Pattern) as GridPattern;
}
catch (InvalidOperationException ex)
{
    // It fails!
}

TablePattern tablePattern = null;

try
{
    tablePattern = datagrid.GetCurrentPattern(TablePattern.Pattern) as TablePattern;
}
catch (InvalidOperationException ex)
{
    // It fails!
}

The rows were added to the DataGridView beforehand, like this:

dgvControlProperties.Rows.Add(new object[] { false, "Some Text", "Some other text" });

I am compiling to .Net Framework 4.5. Tried both regular user rights and elevated admin rights for the UI Automation client, both yielded the same results described here.

Why does the DataGridView return 0 rows?

Why can't I get one of the patterns?

Kudos for helping me out!


Update:

James help didn't to the trick for me. The following code tough returns all rows (including the headers):

var rows = dataGrid.FindAll(TreeScope.Children, PropertyCondition.TrueCondition);

Header cells can then be identified by their ControlType of ControlType.Header.

回答1:

The code that you are copying is flawed. I just tested this scenario with an adaptation of this sample program factoring in your code above, and it works.

The key difference is that the code above is using TreeScope.Children to grab the datagrid element. This option only grabs immediate children of the parent, so if your datagrid is nested it's not going to work. Change it to use TreeScope.Descendants, and it should work as expected.

var datagrid = bw.FindFirst(TreeScope.Descendants, new PropertyCondition(AutomationElement.AutomationIdProperty, "dgvControlProperties"));

Here is a link to how the various Treescope options behave. Also, I don't know how your binding the rows to the grid, but I did it like this in my test scenario and it worked flawlessly.

Hopefully this helps.

public class DataObject
{
    public string FieldA { get; set; }
    public string FieldB { get; set; }
    public string FieldC { get; set; }
}

List<DataObject> items = new List<DataObject>();
items.Add(new DataObject() {FieldA="foobar",FieldB="foobar",FieldC="foobar"});
items.Add(new DataObject() { FieldA = "foobar", FieldB = "foobar", FieldC = "foobar" });
items.Add(new DataObject() { FieldA = "foobar", FieldB = "foobar", FieldC = "foobar" });

dg.ItemsSource = items;


回答2:

Your code looks fine though this could be a focus issue.

Even though you are getting a reference to these automation element objects you should set focus on them (using the aptly named SetFocus method) before using them.

Try:

var desktop = AutomationElement.RootElement;

desktop.SetFocus();

// Find AutomationElement for the App's window
var bw = desktop.FindFirst(TreeScope.Children, new PropertyCondition(AutomationElement.ProcessIdProperty, (int)processID));

and if that does not work try explicitly focusing on the dataGrid prior to calling "FindAll" on it i.e.

datagrid.SetFocus()


回答3:

Why does the DataGridView return 0 rows?

The DataGridViewRows have a ControlType of ControlType.Custom. So I modified the line

// Find all rows from the DataGridView
var loginLines = datagrid.FindAll(TreeScope.Children, new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.DataItem));

To

var loginLines = datagrid.FindAll(TreeScope.Children, new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.Custom));

That gets you all the rows within the DataGridView. But your code has that within the loop.

What pattern is supported by DataGridView (not DataGrid)?

LegacyIAccessiblePattern. Try this-

 LegacyIAccessiblePattern legacyPattern = null;

       try
       {
           legacyPattern = datagrid.GetCurrentPattern(LegacyIAccessiblePattern.Pattern) as LegacyIAccessiblePattern;
       }
       catch (InvalidOperationException ex)
       {
           // It passes!
       }

As I commented on @James answer, there is no support for UIA for DataGridView (again, not DataGrid).

If you search in google with terms: "UI Automation DataGridView" the first result has an incomplete answer. Mike replies with what provider class (Obviously one that extends DataGridView)to create within the source, unfortunately I have no clue as to how to make UIA load that class a provider. If anyone can shed some clues, Phil and I will be extremely pleased!

EDIT

Your DataGridView should implement the interfaces in that link above. Once you do that the ControlType of Row will be ControlType.DataItem instead of ControlType.Custom. You can then use it how you'd use a DataGrid.

EDIT

This is what I ended up doing -

Creating a Custom DataGridView like below. Your datagridview could subclass this too. That will make it support ValuePattern and SelectionItemPattern.

public class CommonDataGridView : System.Windows.Forms.DataGridView,
    IRawElementProviderFragmentRoot,
    IGridProvider,
    ISelectionProvider
{.. }

The complete code can be found @ this msdn link.

Once I did that, I played a bit with visualUIVerify source. Here is how I can access the gridview's cell and change the value. Also, make note of the commented code. I didn't need that. It allows you to iterate through rows and cells.

private void _automationElementTree_SelectedNodeChanged(object sender, EventArgs e)
    {
        //selected currentTestTypeRootNode has been changed so notify change to AutomationTests Control
        AutomationElementTreeNode selectedNode = _automationElementTree.SelectedNode;
        AutomationElement selectedElement = null;

        if (selectedNode != null)
        {
            selectedElement = selectedNode.AutomationElement;

            if (selectedElement.Current.ClassName.Equals("AutomatedDataGrid.CommonDataGridViewCell"))
            {
                if(selectedElement.Current.Name.Equals("Tej")) //Current Value
                {

                    var valuePattern = selectedElement.GetCurrentPattern(ValuePattern.Pattern) as ValuePattern;

                    valuePattern.SetValue("Jet");
                }



                //Useful ways to get patterns and values

                //System.Windows.Automation.SelectionItemPattern pattern = selectedElement.GetCurrentPattern(System.Windows.Automation.SelectionItemPattern.Pattern) as System.Windows.Automation.SelectionItemPattern;


                //var row = pattern.Current.SelectionContainer.FindFirst(TreeScope.Children, new PropertyCondition(AutomationElement.ClassNameProperty, "AutomatedDataGrid.CommonDataGridViewRow", PropertyConditionFlags.IgnoreCase));

                //var cells = row.FindAll(TreeScope.Children, new PropertyCondition(AutomationElement.ClassNameProperty, "AutomatedDataGrid.CommonDataGridViewCell", PropertyConditionFlags.IgnoreCase));
                //foreach (AutomationElement cell in cells)
                //{
                //    Console.WriteLine("**** Printing Cell Value **** " + cell.Current.Name);

                //    if(cell.Current.Name.Equals("Tej")) //current name
                //    {

                //        var valuePattern = cell.GetCurrentPattern(ValuePattern.Pattern) as ValuePattern;

                //        valuePattern.SetValue("Suraj");
                //    }
                //}

                //Select Row
                //pattern.Select();

                //Get All Rows
                //var arrayOfRows = pattern.Current.SelectionContainer.FindAll(TreeScope.Children, new PropertyCondition(AutomationElement.ClassNameProperty, "AutomatedDataGrid.CommonDataGridViewRow", PropertyConditionFlags.IgnoreCase));


                //get cells
                //foreach (AutomationElement row in arrayOfRows)
                //{
                //   var cell = row.FindFirst(TreeScope.Children, new PropertyCondition(AutomationElement.ClassNameProperty, "AutomatedDataGrid.CommonDataGridViewCell", PropertyConditionFlags.IgnoreCase));


                //    var gridItemPattern = cell.GetCurrentPattern(GridItemPattern.Pattern) as GridItemPattern;

                //    // Row number.
                //    Console.WriteLine("**** Printing Row Number **** " + gridItemPattern.Current.Row);

                //    //Cell Automation ID
                //    Console.WriteLine("**** Printing Cell AutomationID **** " + cell.Current.AutomationId);

                //    //Cell Class Name
                //    Console.WriteLine("**** Printing Cell ClassName **** " + cell.Current.ClassName);

                //    //Cell Name
                //    Console.WriteLine("**** Printing Cell AutomationID **** " + cell.Current.Name);                        
                //}

            }
        }

        _automationTests.SelectedElement = selectedElement;
        _automationElementPropertyGrid.AutomationElement = selectedElement;
    }

Hopefully this helps.



回答4:

I run into this problem too, after researching, i figured out that you can only access data grid view with LegacyIAccessible. However, .NET does not support this. So, here are the steps:

  1. there are 2 versions of UIA: managed version and unmanaged version (native code). In some cases, the unmanaged version can do what the managed version can't.

=> as said here, using tblimp.exe (go along with Windows SDK) to generate a COM wrapper around the unmanaged UIA API, so we can call from C#.
I have done it here

  1. we can use this to access DataGridView now but with very limited data, you can use Inspect.exe to see.

The code is:

using InteropUIA = interop.UIAutomationCore;

if (senderElement.Current.ControlType.Equals(ControlType.Custom))
{
    var automation = new InteropUIA.CUIAutomation();
    var element = automation.GetFocusedElement();
    var pattern = (InteropUIA.IUIAutomationLegacyIAccessiblePattern)element.GetCurrentPattern(10018);
    Logger.Info(string.Format("{0}: {1} - Selected", pattern.CurrentName, pattern.CurrentValue));
}