How To Access GridView Cells using Winappdriver?

Viewed 2296

I am trying to get Cell Value From GridView In WPF Project with winappdriver.

I get an issue with this line:

 string name = row.FindElementByName("Name1").Text;

An element could not be located on the page using the given search parameters.

Could you please check my follwing code:

 <Grid>
        <ListView Margin="10" Name="lvUsers" AutomationProperties.AutomationId="lvUsers">
                <ListView.View>
                <GridView x:Name="ListViewItem"  AutomationProperties.AutomationId="ListViewItem">
                        <GridViewColumn x:Name="Name1" AutomationProperties.Name="Name1" AutomationProperties.AutomationId="Name1" Header="Name" Width="120" DisplayMemberBinding="{Binding Name}" />
                        <GridViewColumn Header="Age" Width="50" DisplayMemberBinding="{Binding Age}" />
                        <GridViewColumn Header="Mail" Width="150" DisplayMemberBinding="{Binding Mail}" />
                    </GridView>
                </ListView.View>
            </ListView>
  </Grid>

 var listBox = session.FindElementByAccessibilityId("lvUsers");
            var comboBoxItems = listBox.FindElementsByClassName("ListViewItem");
             foreach (var row  in  comboBoxItems)
             {
                string name = row.FindElementByName("Name1").Text;
                if (name == "John Doe")
                {                     
                   findName = true;
                   break;
                }
         }
        Assert.AreEqual(findName, true);
2 Answers

You have obviously chosen the wrong tool to accomplish your task. Automation is designed to work with UI elements, but you need data for the task. See what the Visual Tree of your DataGrid looks like:

enter image description here

DataGrid is inherited from ItemsControl. And in his visualization there are only rows. No columns. It is possible to extract data from a specific cell, but it is very difficult and does not make sense.

You need to create a normal Data source. To get started, take some kind of implementation of INotifyPropertyChanged. For example, this:

/// <summary>Base class implementing INotifyPropertyChanged.</summary>
public abstract class BaseINPC : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    /// <summary>Called AFTER the property value changes.</summary>
    /// <param name="propertyName">The name of the property.
    /// In the property setter, the parameter is not specified. </param>
    public void RaisePropertyChanged([CallerMemberName] string propertyName = "")
        => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));

    /// <summary> A virtual method that defines changes in the value field of a property value. </summary>
    /// <typeparam name = "T"> Type of property value. </typeparam>
    /// <param name = "oldValue"> Reference to the field with the old value. </param>
    /// <param name = "newValue"> New value. </param>
    /// <param name = "propertyName"> The name of the property. If <see cref = "string.IsNullOrWhiteSpace (string)" />,
    /// then ArgumentNullException. </param> 
    /// <remarks> If the base method is not called in the derived class,
    /// then the value will not change.</remarks>
    protected virtual void Set<T>(ref T oldValue, T newValue, [CallerMemberName] string propertyName = "")
    {
        if (string.IsNullOrWhiteSpace(propertyName))
            throw new ArgumentNullException(nameof(propertyName));

        if ((oldValue == null && newValue != null) || (oldValue != null && !oldValue.Equals(newValue)))
            OnValueChange(ref oldValue, newValue, propertyName);
    }

    /// <summary> A virtual method that changes the value of a property. </summary>
    /// <typeparam name = "T"> Type of property value. </typeparam>
    /// <param name = "oldValue"> Reference to the property value field. </param>
    /// <param name = "newValue"> New value. </param>
    /// <param name = "propertyName"> The name of the property. </param>
    /// <remarks> If the base method is not called in the derived class,
    /// then the value will not change.</remarks>
    protected virtual void OnValueChange<T>(ref T oldValue, T newValue, string propertyName)
    {
        oldValue = newValue;
        RaisePropertyChanged(propertyName);
    }

}

On its basis you can create a type for collection edements:

public class PersonVM : BaseINPC
{
    private string _name;
    private uint _age;
    private string _mail;

    public string Name { get => _name; set => Set(ref _name, value); }
    public uint Age { get => _age; set => Set(ref _age, value); }
    public string Mail { get => _mail; set => Set(ref _mail, value); }
}

And ViewModel with collection:

public class ViewModel
{
    public ObservableCollection<PersonVM> People { get; } 
        = new ObservableCollection<PersonVM>()
        {
            new PersonVM(){Name="Peter", Age=20, Mail="Peter@mail.com"},
            new PersonVM(){Name="Alex", Age=30, Mail="Alex@mail.com"},
            new PersonVM(){Name="Nina", Age=25, Mail="Nina@mail.com"},
        };
}

Connect it to the DataContext Windows:

<Window.DataContext>
    <local:ViewModel/>
</Window.DataContext>
<Grid>
    <ListView Margin="10" ItemsSource="{Binding People}">
        <ListView.View>
            <GridView x:Name="ListViewItem" >
                <GridViewColumn x:Name="Name1" Header="Name" Width="120" DisplayMemberBinding="{Binding Name}" />
                <GridViewColumn Header="Age" Width="50" DisplayMemberBinding="{Binding Age}" />
                <GridViewColumn Header="Mail" Width="150" DisplayMemberBinding="{Binding Mail}" />
            </GridView>
        </ListView.View>
    </ListView>
</Grid>

Now your task is reduced to finding the desired item in the People collection.

If you know the exact location of the cell (e.g. x row, y column) in the grid, use following custom code.
It worked for me, I had to get the number in 3rd row, 2nd column. The grid has 6 columns.

var gridItemsCollection = grid.FindElementsByXPath("//ListItem/Text");
List<int> allIds = HelperClass.GetColumnValuesFromGrid(gridItemsCollection, 6,2).ConvertAll(int.Parse);
var myId = allIds[2];//3rd row. 3-1

Following is the function definition. (Not a perfect code though)

public static List<string> GetColumnValuesFromGrid(IReadOnlyCollection<AppiumWebElement> gridItemsCollection, int gridColumns, int selectColumn)
    {
        List<string> list = new List<string>();
    List<string> selectList = new List<string>();

int index = selectColumn - 1;
if (index < 0 || gridItemsCollection.Count == 0)
{
return null;
}

foreach (var element in gridItemsCollection)
{
    list.Add(element.Text);
}            

while (index < list.Count)
{
    selectList.Add(list[index]);
    index += gridColumns;
}

return selectList;
}

I also had to get the maximum number. So, I did the following.

allIds.Sort();
allIds.Reverse();
var maxId = allIds[0];
Related