6

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);
Simon Mourier
  • 132,049
  • 21
  • 248
  • 298
Mohamad Mahmoud Darwish
  • 3,865
  • 9
  • 51
  • 76
  • 1
    Probably not what you want to hear but... It's usual to automate tests on wpf viewmodels and not views. Iterating through a datagrid or listview to find content of a cell is always going to be painful. A gridviewcolumn is an abstract thing. You won't find it in the visual tree. I recommend you download snoop ( there's a different version for .net core ) and use that to explore what you're actually getting in your UI when you run. – Andy Jun 27 '20 at 11:04
  • 1
    WinAppDrive uses UI Automation. Use inspect.exe from the Windows SDK to check what UI Automation "sees" from your running application. https://github.com/microsoft/WinAppDriver/blob/master/Docs/FAQ.md#what-is-inspectexe Only UIElement-derived classes can support UI Automation. GridViewColumn is not a UIElement – Simon Mourier Jun 30 '20 at 06:12

3 Answers3

0

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.

EldHasp
  • 6,079
  • 2
  • 9
  • 24
0

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];
0

Using inspect.exe (downloaded with WinAppDriver), I found that in a DataGridView one can access the text in each Cell of the DataGridView as follows

string text = driver.FindElementByName( "<ColumnName> Row <x>" ).Text

where ColumnName is the name of the Column and x is the Row number (starting from 0)

However I found the above method to be very slow a much quicker method is to locate the DataGridView and them use XPath to locate all its elements (cells), as illustrate below

var Results_DGV = Driver.FindElementByAccessibilityId( "DGV_Results" );

var DGV_Cells = Results_DGV.FindElementsByXPath("//*");

for ( for loop controls ) {
     loop over the cells
}                                                           
Tony H
  • 61
  • 3