CodedUI: Why is searching for a cell so slow? - c#

I've got a grid (FlexGrid, from ComponentOne) in a Winform application and I'm trying to find a cell in that grid, given the cell's column index and its value.
I've written the extension method below to loop through the grid and find that cell.
I'm testing that method on a grid that has 6 columns and 64 rows. It took 10 minutes for my code to find the correct cell (which was on the last row)
Is there any way I can speed up my algorithm ?
Note: I've also tried setting PlayBack.PlayBackSetting.SmartMatchOption to TopLevelWindow, but it does not seem to change anything...
Thanks!
public static WinCell FindCellByColumnAndValue(this WinTable table, int colIndex, string strCellValue)
{
int count = table.GetChildren().Count;
for (int rowIndex = 0; rowIndex < count; rowIndex++)
{
WinRow row = new WinRow(table);
WinCell cell = new WinCell(row);
row.SearchProperties.Add(WinRow.PropertyNames.RowIndex, rowIndex.ToString());
cell.SearchProperties.Add(WinCell.PropertyNames.ColumnIndex, colIndex.ToString());
cell.SearchProperties.Add(WinCell.PropertyNames.Value, strCellValue);
if (cell.Exists)
return cell;
}
return new WinCell();
}
Edit
I modified my method to be like below(e.g. I don't use winrow anymore), this seems to be around 3x faster. It still needs 7 sec to find a cell in a table with 3 rows and 6 columns though, so it's still quite slow...
I'll mark this answer as accepted later, to leave time to other people to suggest something better
public static WinCell FindCellByColumnAndValue(this WinTable table, int colIndex, string strCellValue, bool searchHeader = false)
{
Playback.PlaybackSettings.SmartMatchOptions = Microsoft.VisualStudio.TestTools.UITest.Extension.SmartMatchOptions.None;
int count = table.GetChildren().Count;
for (int rowIndex = 0; rowIndex < count; rowIndex++)
{
WinCell cell = new WinCell(table);
cell.SearchProperties.Add(WinRow.PropertyNames.RowIndex, rowIndex.ToString());
cell.SearchProperties.Add(WinCell.PropertyNames.ColumnIndex, colIndex.ToString());
cell.SearchProperties.Add(WinCell.PropertyNames.Value, strCellValue);
if (cell.Exists)
return cell;
}
return new WinCell();
}
Edit #2:
I've tried using FindMatchingControls as per #Andrii's suggestion, and I'm nearly there, except that in the code below the cell's column index (c.ColumnIndex) has the wrong value..
public static WinCell FindCellByColumnAndValue2(this WinTable table, int colIndex, string strCellValue, bool searchHeader = false)
{
WinRow row = new WinRow(table);
//Filter rows containing the wanted value
row.SearchProperties.Add(new PropertyExpression(WinRow.PropertyNames.Value, strCellValue, PropertyExpressionOperator.Contains));
var rows = row.FindMatchingControls();
foreach (var r in rows)
{
WinCell cell = new WinCell(r);
cell.SearchProperties.Add(WinCell.PropertyNames.Value, strCellValue);
//Filter cells with the wanted value in the current row
var controls = cell.FindMatchingControls();
foreach (var ctl in controls)
{
var c = ctl as WinCell;
if (c.ColumnIndex == colIndex)//ERROR: The only cell in my table with the correct value returns a column index of 2, instead of 0 (being in the first cell)
return c;
}
}
return new WinCell();
}

I would suggest to perform direct looping through child controls - according to my experience searching controls with complicated search crieteria in Coded UI often works slow.
Edit:
To improve performance it is better to remove counting table's children and looping through rows. Also to avoid exception when finding control without row number you can use FindMatchingControls method, like in following:
public static WinCell FindCellByColumnAndValue(this WinTable table, int colIndex, string strCellValue, bool searchHeader = false)
{
Playback.PlaybackSettings.SmartMatchOptions = Microsoft.VisualStudio.TestTools.UITest.Extension.SmartMatchOptions.None;
WinCell cell = new WinCell(table);
cell.SearchProperties.Add(WinCell.PropertyNames.ColumnIndex, colIndex.ToString());
cell.SearchProperties.Add(WinCell.PropertyNames.Value, strCellValue);
UITestControlCollection foundControls = cell.FindMatchingControls();
if (foundControls.Count > 0)
{
cell = foundControls.List[0];
}
else
{
cell = null;
}
return cell;
}
When table field will be searched directly in the table it will save time for counting child controls in table. Also searching without for loop will save time for search of field for each iteration of row number that does not match.
As row number is iterated through all available values in your extension - it is not essential search criterion in the long run. In same time each iteration through a row number value invokes additional control search request - eventually multiplying method's execution time by the number of rows in grid.

Related

Comparing two DataGridViews and deleting the values of the second if exists in the first one

I am doing a code where I compare two columns of DGV roles, the first DGV (DGV1) has the raw data with duplicate roles, and the second DGV (DGV4) is a dictionary with all existing roles (no duplicates), it has to go to each row of the dictionary and if the role exists in the DGV1, it should be removed from the dictionary, leaving only the roles in the dictionary that are not currently being used in the raw data. My code is removing the roles, but when the dictionary has a value that doesn't exist in DGV1, it stops working (DGV1 continues to loop until it has an index error). Any suggestion?
NOTE: The rows in the dictionary automatically go to the first index, so there is no need to increment int i.
int eliminado = 0;
int filasDGV1 = dataGridView1.Rows.Count;
int filasDGV4 = dataGridView4.Rows.Count;
int i = 0;
int j = 0;
do
{
string perfilVacio = dataGridView4["GRANTED_ROLE", i].Value.ToString();
string perfiles = dataGridView1["GRANTED_ROLE", j].Value.ToString();
if(perfiles != perfilVacio)
{
j++;
}
else if(perfiles == perfilVacio)
{
dataGridView4.Rows.RemoveAt(i);
}
}
while (eliminado <= filasDGV4);
The first excel is DGV1 and the other is DGV2, I highlighted where is the code looping currently
The orange highlight is where the loop change in DGV1 but in the dictionary doesnt exist so its stuck there
Change your loop condition to include a test for the changing index j and also to check whether there are rows left to be eliminated.
int filasDGV1 = dataGridView1.Rows.Count;
int j = 0;
while (j < filasDGV1 && dataGridView4.Rows.Count > 0)
{
string perfilVacio = dataGridView4["GRANTED_ROLE", 0].Value.ToString();
string perfiles = dataGridView1["GRANTED_ROLE", j].Value.ToString();
if(perfiles == perfilVacio)
{
dataGridView4.Rows.RemoveAt(0);
}
else
{
j++;
}
}
If you test perfiles != perfilVacio in if you don't have to test perfiles == perfilVacio in else if, because this automatically the case. Either they are equal or they are not. There no other possibility.
Also, it is generally more readable if you ask a positive question in if like == instead of a negative one like !=.
Since i is always 0 I replaced it by the constant 0. The variable eliminado is not required (unless it is incremented when rows are removed to display the number of deleted rows).
The number of rows in dataGridView4 should not be stored in filasDGV4 as this number changes.
Update
According to your comments and the new screenshots, you need two loops. (The code above only works if both lists are sorted). We could use two nested loops; however, this is slow. Therefore, I suggest collecting the unwanted roles in a HashSet<string> first. Testing whether an item is in a HashSet is extremely fast. Then we can loop through the rows of the dictionary and delete the unwanted ones.
var unwanted = new HashSet<string>();
for (int i = 0; i < dataGridView1.Rows.Count: i++)
{
unwanted.Add(dataGridView1["GRANTED_ROLE", i].Value.ToString());
}
int row = 0;
while (row < dataGridView4.Rows.Count)
{
string perfilVacio = dataGridView4["GRANTED_ROLE", row].Value.ToString();
if(unwanted.Contains(perfilVacio))
{
dataGridView4.Rows.RemoveAt(row);
}
else
{
row++;
}
}
Suggestion: Using data binding to bind your DataGridViews to generic lists would enable you to work on these lists instead of working on the DGVs. This would simplify the data handling considerably.

Show select gridview columns

I have a list of strings that I'm trying to use to control the columns shown in a gridview, but can't seem to figure out how to get it to work. Here's an example List<string> selectedHeaders = new List<string>(new string[] { "header1", "header2", "header3", "header4" });
How can I loop through the gridview columns and compare them to the values in selectedHeaders and set the visibility to false for all columns that don't match. Also note that the number of selectedHeaders can be different than the total number of columns within the gridview.
Here is what I have so far:
foreach (GridViewRow row in gvEmployees)
{
for (int i = 0; i < gvEmployees.Columns.Count; i++)
{
if (gvEmployees.Column[i].HeaderText != selectedHeaders[i])
{
gvEmployees.Column[i].Visible = false;
}
}
}
I'm not sure how to refractor this it gives me an index out of range error, because the gridview has 6 columns, but selectedHeaders could contain 1-6 values.
Your loops don't make sense for what you're trying to accomplish.
What you're doing: looping through every row in the GridView, and looping through every column within that, and looking for a string in your selectedHeaders with a matching index
What you need to be doing: looping through every column, and checking to see if there's a corresponding record in selectedHeaders by value, not by index position.
Change your code to this:
for (int i = 0; i < gvEmployees.Columns.Count; i++)
{
if (!selectedHeaders.Any(h => h == gvEmployees.Column[i].HeaderText))
{
gvEmployees.Column[i].Visible = false;
}
}

Is there a way to check if a grid cell, NOT datagrid cell, contains an object

I'm assigning textboxes to cells in a grid but will like to confirm if an object already exists in the cell before assigning. Is it possible to query a row at a specific column that returns null if empty?
I could create a list of lists representing the grid which I modify as i add and remove objects but this sounds to be inefficient.
A sample code I've written:
TextBlock _text = new TextBlock()
{
Text = _cont,
Background = new SolidColorBrush(_colo.disciplinecolor)
}; TextBlockStyle(_text);
int index = SearchDate((DateTime)_dt);
Grid.SetRow(_text, 1); Grid.SetColumn(_text, index);
Maindispgrid.Children.Add(_text);
Essentially this code block is called every time the user clicks a button with the TextBlock added to a dated column(pre-selected by the user), and hopefully, the next available row in the column . I've tried GetRow() but this searched by UIElement which didn't seem to work as all TextBlock are created with the same name.
I might have approached this all wrong so any leads as to what I need to read up on will be much appreciated.
Basically the end result should hopefully work as this:
TextBlock _text = new TextBlock()
{
Text = _cont,
Background = new SolidColorBrush(_colo.disciplinecolor)
}; TextBlockStyle(_text);
int index = SearchDate((DateTime)_dt);
//check for next available row at specific column index
Grid.SetRow(_text, nextAvailableRow); Grid.SetColumn(_text, index);
Maindispgrid.Children.Add(_text);
You can iterate over the children list via Maindispgrid.Children. For each child, retrieve the assigned Row and Column value (if assigned at all). Use this information to calculate the available row.
IList<int[]> indices = new List<int[]>();
foreach (var child in Maindispgrid.Children)
{
int rowIndex = Grid.GetRow(child);
int columnIndex = Grid.GetColumn(child);
indices.Add(new int[] {rowIndex, columnIndex});
}
// work with the "indices" list

How to read column names of a multicolumn ListView control?

What is the best way to find the names of the columns of a ListView?
I converted a DataTable to a List using a procedure I found on this forum, but I cannot make it to put the Id column first, especially because not all of my DataTables have a column "Id".
I can search in collection listView.Columns.ToString() but the format I am seeing is:
"ColumnHeader: Text: Id"
which I have to parse to find the proper name "Id".
This does not look like the spirit of C#.
I also tried: listView.SelectedItems[0].SubItems["Id"]
but that does not compile.
Ok Here is the complete code.
The exact problem is that the user selects a row in the listView with Courier Names and Ids, but it could also be Ids and Names, in that order. The fastest way to find the Id of the selected courier would be:
ListViewItem si = listCouriers.SelectedItems[0];
CourierId = si.SubItems["Id"].Text;
but that does not work. The hardcoded way would be this, but I cannot guarantee that some day the wrong column will be used:
ListViewItem si = listCouriers.SelectedItems[0];
CourierId = si.SubItems[1].Text;
Using #HuorSwords method leads to this not-so-simple solution, which works for me, but depends on the reasonable assumption that the order of columns in the ColumnHeaderCollection corresponds to the display on the form:
ListViewItem si = listCouriers.SelectedItems[0];
string CourierId = null;
int icol = 0;
foreach (ColumnHeader header in listCouriers.Columns)
{
if (header.Text == "Id")
{
CourierId = si.SubItems[icol].Text;
break;
}
icol++;
}
As listView.Columns is of type ListView.ColumnHeaderCollection, then it contains ColumnHeader objects.
The ColumnHeader.Text contains the column title, so you can check for concrete column with:
foreach (ColumnHeader header in listView.Columns)
{
if (header.Text == "Id")
{
// Do something...
}
}
I don't know if is the best approach, but you don't need to parse the results to find "Id" value...
UPDATE
Also, have you tried to reference it with the String indexer? > listView.Columns["Id"]
use this code:
private ColumnHeader GetColumn(string Text)
{
for (int i = 0; i < listView1.Columns.Count; i++)
for (int j = 0; j < listView1.Items.Count; j++)
if (listView1.Items[j].SubItems.Count - 1 >= i)
if (listView1.Items[j].SubItems[i].Text == Text)
return listView1.Columns[i];
return null;
}
just give item text to this code and get everything you want of a column.
Enjoy ;)

Total cell count of a DataGridView

I need to get the total number of cells that are present in a datagridview. This is then used to determine if I want to include the column header text when copying/pasting the data, I only want this displayed if all records are selected.
I am using the following code to get the total number of cells but is there a better way to get this value?
var totalCellCount = DataGridView2.ColumnCount * DataGridView2.RowCount;
I couldn't find a property that contained a count of all cells, maybe I am missing it. Is there a better way to get the number of cells?
My datagridview has the ClipboardCopyMode set to EnableWithAutoHeaderText, but I want to set it to EnableAlwaysIncludeHeaderText when they select all rows/columns in the grid. So I am using the total number of cells in the code below:
private void DataGridView_KeyPress(object sender, KeyPressEventArgs e)
{
if (m_RecordCount == 0)
return;
var totalCellCount = DataGridView2.ColumnCount * DataGridView2.RowCount;
if (DataGridView2.SelectedCells.Count == totalCellCount)
{
if (e.KeyChar == (char)3)
{
DataGridView2.ClipboardCopyMode = DataGridViewClipboardCopyMode.EnableAlwaysIncludeHeaderText;
var clipboardContent = this.DataGridView2.GetClipboardContent();
if (clipboardContent != null)
{
Clipboard.SetText(clipboardContent.GetText(TextDataFormat.Text));
}
e.Handled = true;
}
}
}
The DataGrid.Items property returns a DataGridItemCollection representing the DataGridItems in the DataGrid.
Each DataGridItem is representative of a single row in the rendered table. Also, the DataGridItem exposes a Cells property which represents the no. of tablecells (in other words, the columns) in the rendered table. From here if you need any other custom scenarios you will have to either add it to the original Question or code a solution
var rowCount = DataGridView2.Items.Count; //Number of Items...i.e. Rows;
// Get the no. of columns in the first row.
var colCount = DataGridView2.Items[0].Cells.Count;
if you want the total number of Rows also try
If you want to get at total items an you want a real total for example if you have multiple pages.. If so you shouldn't be trying to find that information from the GridView but instead look at the underlying DataSource that you bound your GridView.
Example ----
List<SomeObject> lis = GetYourData();
DataGrid.DataSource = list;
DataGrid.DataBind();
// if you want to get the count for a specific page
int currentPage = 2;
int countForPage2 = (list.Count > currentPage * totalItemsPerPage)) ?
totalItemsPerPage : list.Count - ((currentPage - 1) * totalItemsPerPage);

Categories