There is a table on the web page. Items are loaded while scrolling the list. It also works so, that it is loading and unloading items. So there is no way to scroll it all the way down first and then load items. Also, log in to a web page is required in order to see the table so Selenium seems to be the only option. After proceeding to the correct web page with a table, there are already some items, but in order to get them, all the tables should be scrolled down step by step.
I was working on a method that will:
Load what is visible to List
Compare what is visible to what we already have in the table
If not found in List, scroll down
Add new items to list
My current code is able to load visible items after page load:
public class UserTableRow
{
private readonly IWebElement row;
public string Username => row.FindElement(By.XPath(".//div[contains(#class, 'slick-cell l0 r0')]")).Text;
public string Firstname => row.FindElement(By.XPath(".//div[contains(#class, 'slick-cell l1 r1')]")).Text;
public string Lastname => row.FindElement(By.XPath(".//div[contains(#class, 'slick-cell l2 r2')]")).Text;
public string Type => row.FindElement(By.XPath(".//div[contains(#class, 'slick-cell l3 r3')]")).Text;
public string Crew => row.FindElement(By.XPath(".//div[contains(#class, 'slick-cell l4 r4')]")).Text;
public string JobTitle => row.FindElement(By.XPath(".//div[contains(#class, 'slick-cell l5 r5')]")).Text;
public string DefaultPrice => row.FindElement(By.XPath(".//div[contains(#class, 'slick-cell l6 r6')]")).Text;
public string Future => row.FindElement(By.XPath(".//div[contains(#class, 'slick-cell l7 r7')]")).Text;
public string Language => row.FindElement(By.XPath(".//div[contains(#class, 'slick-cell l8 r8')]")).Text;
//public override string ToString()
//{
// return "TableRow: " + Username.ToString() + ": " + Firstname.ToString();
//}
public UserTableRow(IWebElement row)
{
this.row = row;
}
Here is the method itself. For some reason, it is failing at checking bool AlreadyExist = DataHere.Any(cus => cus.Username == row.Text); - if item already exists in IEnumerable as nothing is scrolled down. Any ideas on how it can be fixed?
public static IEnumerable<UserTableRow> AddItemsToList(IWebDriver driver)
{
var rows = driver.FindElements(By.CssSelector("#users_table .slick-viewport .slick-row"));
// This part will get our first visible items in table to list, after page load
var DataHere = from row in driver.FindElements(By.CssSelector("#users_table .slick-viewport .slick-row"))
select new UserTableRow(row);
// We need to select table first to be able to scroll down. We do it directly here
driver.FindElement(By.XPath("//*[#id=\"users_table\"]/div[5]/div/div[14]/div[1]")).Click();
// Now we will iterate through cells in the table and compare to what we already have in the list
foreach (var row in rows)
{
// Define variable for comparison
bool AlreadyExist = DataHere.Any(cus => cus.Username == row.Text);
// If item already exist, scroll down and add to list
if (AlreadyExist == true)
{
SendKeys.SendWait("{PGDN}");
DataHere.Add(new UserTableRow(row));
}
}
return DataHere;
}
It's really hard to tell what could be the exact reason why the comparison fails.
However, I can try to suggest several approaches to find the problem in your code.
First, lets apply ToList() at the end of your query for DataHere to skip possible multiple call for the driver.FindElements(...):
var DataHere = (from row in
driver.FindElements(By.CssSelector("#users_table .slick-viewport .slick-row"))
select new UserTableRow(row)).ToList();
Second, even before getting into foreach loop, it would be wise to check if your DataHere has items, otherwise, you will be guaranteed to have false results for every AlreadyExist check.
// Now we will iterate through cells in the table and compare to what we already have in the list
if (DataHere.Any())
{
foreach (var row in rows)
{
// ...
}
}
And the last point, ask yourself:
What kind of check am I performing (exact or contain)?
If the answer is "contain" then use Contains or IndexOf method for comparing cus.Username and row.Text.
Is the check case sensitive or not?
If the answer is not case sensitive then apply one of the ToLower(), ToLowerInvariant(), ToUpper(), ToUperInvariant() methods to both cus.Username and row.Text during comparison.
Finally, if both of the above points are true then combine the suggestions.
Related
My DataGridView looks like this:
How to clear the text of duplicate cells in the DataGridView Rows?
I tried below but it's clearing all values of Cells[0].
string duplicateValue = dataGridView1.Rows[0].Cells[0].Value.ToString();
for (int i = 1; i < dataGridView1.Rows.Count; i++)
{
if (dataGridView1.Rows[i].Cells[0].Value.ToString() == duplicateValue)
{
dataGridView1.Rows[i].Cells[0].Value = string.Empty;
}
else
{
dataGridView1.Rows[i].Cells[0].Value = duplicateValue;
}
}
One way to achieve this would be to use a HashSet as follows:
var valuesFound = new HashSet<string>();
for (int i = 0; i < dataGridView1.Rows.Count; i++)
{
string cellText = dataGridView1.Rows[i].Cells[0].Value.ToString();
// Attempt to add the value to the HashSet. If it fails, then it's a duplicate.
if (!valuesFound.Add(cellText))
{
dataGridView1.Rows[i].Cells[0].Value = string.Empty;
}
}
Or if you prefer LINQ, you could do something like this:
var duplicateCells = dataGridView1.Rows.OfType<DataGridViewRow>()
.Select(r => r.Cells[0])
.GroupBy(c => c.Value.ToString())
.SelectMany(g => g.Skip(1))
.ToList();
duplicateCells.ForEach(c => c.Value = string.Empty);
Short answer:
How to clear the text of duplicate cells in the DataGridView Rows?
Apparent you consider some products to be the same. Alas you forgot to say when two products are equal. Is Product [Apple, UK, 1] equal to [Apple, UK, 2]? And if so, which one do you want to show?
Or do you want to show the sum: [Apple, UK, 3]?
And what about: [Apple, Ireland, 1]? Is that the same as [Apple, UK, 1]?
Clearly you need a method that says: this product equals that product, but that one is a different product.
For this we'll have to create an equality comparer.
class Product
{
public Name {get; set;}
public string Country {get; set;}
public int Quantity {get; set;}
...
}
IEqualityComparer<Product> productComparer = ... // TODO: implement
Once you've got this, you can get rid of duplicates:
IEnumerable<Product> productsWithDuplicates = ...
IEnumerable<Product> noDuplicates = productsWithDuplicates.Distinct(productComparer);
Or if you want to combine [Apple, UK, 1] and [Apple, UK, 2] to show the sum [Apple, UK, 3], use groupBy to make groups:
IEnumerable<Product> productsToDisplay = productsWithDuplicates
.GroupBy(product => new {product.Name, product.Country}
(key, productsWithThisKey) => new Product
{
Name = key.Name,
Country = key.Country,
Quantity = productWithThisKey.Select(product => product.Quantity).Sum(),
},
productComparer);
So the solution depends on when two products are equal, and what you want to show if you've found equal produts.
Equality Comparer for Products
class ProductComparer : EqualityComparer<Product>()
{
public static IEqualityComparer<Product> NameCountry {get;} = new ProductComparer();
public override bool Equals(Product x, Product y)
{
if (x == null) return y == null; // true if both null, false if x null, but y not
if (y == null) return false; // because x not null
if (object.ReferenceEquals(x, y) return true;
// define equality, for instance:
return x.Name == y.Name && x.Country == y.Country;
}
If you want case insensitive, add a property:
private static IEqualityComparer<string> NameComparer {get; } = StringComparer.InvariantIgnoreCase;
private static IEqualityComparer<string> CountryComparer {get;} = ...
And in Equals:
return NameComparer.Equals(x.Name, y.Name)
&& CountryComparer.Equals(x.Country, y.Country);
Now if you later decide that you want to be case sensitive when comparing Countries, or maybe want to use the current culture, you'll only have to change this on one location.
The use of the comparers, makes changing easier, but also your code: you don't have to check for null names and countries, that is handled by the comparers.
GetHashCode: only requirement: if x equals y, return same GetHashCode. if not equal, you are free to return whatever you want, but it is more efficient if you return different hashcode.
public override int GetHashCode(Product product)
{
if (product == null) return 47855249; // just a number
return NameComparer.GetHashCode(product.Name)
^ CountryComparer.GetHashCode(product.Country);
}
There's room for improvement
It is usually not a good idea to intertwine your model with the view of your model. If in future you want to change how your data is displayed, for instance you want to show it in a ListBox, or in a Graph, you'll have to change a lot.
Besides, if you have separated your model from the way that it is displayed, it will be a lot easier to unit test your model. To test your view, you won't need your original data, you can test with edge conditions, like an empty datagridview,
First of all, you need a method to fetch the products from your model:
private IEnumerable<Productm> FetchProducts(...) {...}
So now you have a unit testable method that fetches the products. The nice thing is that you even hid where you get this information from: it can be from a database, or an XML file, or even from the internet: your Form doesn't know, and doesn't have to know. Totally designed for change.
Using visual studio designer you have defined columns. Every column shows exactly the value of one property. Which property the column shows is defined in property DataGridViewColumn.DataPropertyName
columnName.DataPropertyName = nameof(Product.Name);
columnCountry.DatapropertyName = nameof(Product.Country);
...
To show the fetched Products, use property DataGridView.DataSource. If you assign a List<Product>, then changes that the operator makes (add / remove rows, change cells) are not reflected in the List. If you want to automatically update the changes that the operator made, use a BindingList
public BindingList<Product> DisplayedProducts
{
get => (BindingList<Product>)this.datagridView1.DataSource;
set => this.datagridView1.DataSource = value;
}
private IEqualityComparer<Product> ProductComparer {get;} = ProductComparer.NameCountry;
public void InitProductDisplay()
{
IEnumerable<Product> productsToDisplay = this.FetchProducts()
.Distinct(productComparer);
// or if you want to show the totals: use the GroupBy described above
this.DisplayedProducts = new BindingList<Product>(productsToDisplay.ToList());
}
Nice! If you don't want to compare on NameCountry, but differently, or if you want to Compare using current culture, if you want to show the totals of the quantity, or even if you want to show it in a graph instead of a table: there is only one place you need to change.
Now every change that the operator makes: add / remove / change is reflected in your BindingList, even if the rows are sorted.
For instance, if the operator indicates that he finished editing by clicking a button:
private void OnButtonOk_Clicked(object sender, ...)
{
var displayedProducts = this.DisplayedProducts;
// find out which products are added / removed / changed
this.ProcessEditedProducts(displayedProducts);
}
If you need to do something with selected rows, consider to add the follwing:
private Product CurrentProduct => (Product)(this.datagridView1.CurrentRow?.DataBoundItem);
private IEnumerable<Product> SelectedProducts = this.datagridView1.SelectedRows
.Cast<DataGridViewrow>()
.Select(row => row.DataBoundItem)
.Cast<Product>();
i know it is not complicated but i struggle with it.
I have IList<Material> collection
public class Material
{
public string Number { get; set; }
public decimal? Value { get; set; }
}
materials = new List<Material>();
materials.Add(new Material { Number = 111 });
materials.Add(new Material { Number = 222 });
And i have DbSet<Material> collection
with columns Number and ValueColumn
I need to update IList<Material> Value property based on DbSet<Material> collection but with following conditions
Only one query request into database
The returned data from database has to be limited by Number identifier (do not load whole database table into memory)
I tried following (based on my previous question)
Working solution 1, but download whole table into memory (monitored in sql server profiler).
var result = (
from db_m in db.Material
join m in model.Materials
on db_m.Number.ToString() equals m.Number
select new
{
db_m.Number,
db_m.Value
}
).ToList();
model.Materials.ToList().ForEach(m => m.Value= result.SingleOrDefault(db_m => db_m.Number.ToString() == m.Number).Value);
Working solution 2, but it execute query for each item in the collection.
model.Materials.ToList().ForEach(m => m.Value= db.Material.FirstOrDefault(db_m => db_m.Number.ToString() == m.Number).Value);
Incompletely solution, where i tried to use contains method
// I am trying to get new filtered collection from database, which i will iterate after.
var result = db.Material
.Where(x=>
// here is the reasonable error: cannot convert int into Material class, but i do not know how to solve this.
model.Materials.Contains(x.Number)
)
.Select(material => new Material { Number = material.Number.ToString(), Value = material.Value});
Any idea ? For me it is much easier to execute stored procedure with comma separated id values as a parameter and get the data directly, but i want to master linq too.
I'd do something like this without trying to get too cute :
var numbersToFilterby = model.Materials.Select(m => m.Number).ToArray();
...
var result = from db_m in db.Material where numbersToFilterBy.Contains(db_m.Number) select new { ... }
I have a system where people can sent me a List<ProductCategory>.
I need to do some mapping to my system and then save these to the database.
The incoming data is in this format:
public string ExternalCategoryID { get; set; }
public string ExternalCategoryName { get; set; }
public string ExternalCategoryParentID { get; set; }
The incoming list is in no particular order. If ExternalCategoryParentID is null then this is a top level category. The parent child relationship can be any depth - i.e. Technology > TVs > Samsung > 3D > 40" > etc > etc
When I'm saving I need to ensure I've already saved the parent - I can't save TVs until I have saved Technology. The ExternalCategoryID is likely to be an int but this has no relevance on the parent child relationship (a parent can have a higher or lower id than a child).
How can I order this list so I can loop through it and be certain that for any child, I have already processed it's parent.
The only way I can think of is to get all where ExternalCategoryParentID == null then get all where the ExternalCategoryParentID is in this "Top Level" list, then get the next set of children... etc. but this can't be the best solution. I'd prefer to sort first, then have a single loop to process. I have found this post, but it relies on createdDate which isn't relevant to me.
Turns out it wasn't so difficult after all. I wrote this function to do it - you pass in the original list and it will return a sorted list.
It works by looping through the list checking if there are any items in the list that have id == current items parentid. If there is one, we ignore that item and continue. if there aren't any, we add the current item to the sortedList and remove it from the original list and continue. This ensures that items are inserted in the sorted list after their parent.
private List<HeisenbergProdMarketplaceCategory> SortByParentChildRelationship(List<HeisenbergProdMarketplaceCategory> heisenbergMarketplaceCategories)
{
List<HeisenbergProdMarketplaceCategory> sortedList = new List<HeisenbergProdMarketplaceCategory>();
//we can check that a category doesn't have a parent in the same list - if it does, leave it and continue
//eventually the list will be empty
while(heisenbergMarketplaceCategories.Count > 0)
{
for (int i = heisenbergMarketplaceCategories.Count-1; i >= 0; i--)
{
if (heisenbergMarketplaceCategories.SingleOrDefault(p => p.ExternalCategoryID == heisenbergMarketplaceCategories[i].ExternalCategoryParentID) == null)
{
sortedList.Add(heisenbergMarketplaceCategories[i]);
heisenbergMarketplaceCategories.RemoveAt(i);
}
}
}
return sortedList;
}
We can check the field value to be null and then set int.max to it and then order by desc to get it at the top.
We can check the ExternalCategoryID = field.Value ?? int.max and then Order by descending.
Sample :
var query = context.Categories.Select(o => new { Obj = o, OrderID = o.OrderID ?? int.MaxValue }).OrderByDescending(o => o.OrderID).Select(o => o.Obj);
I'm working on creating a filter for a collection of employees. In order to do this I initially fetch a raw collection of all employees. I clone this list so I can iterate over the original list but remove items from the second list.
For each filter I have, I build a collection of employee ids that pass the filter. Having gone through all filters I then attempt to remove everything that isn't contained in any of these lists from the cloned list.
However for some reason, whenever I attempt to do this using .RemoveAll(), all records seemed to be removed and I can't figure out why.
Here is a stripped down version of the method I'm using, with only 1 filter applied:
public List<int> GetFilteredEmployeeIds(int? brandId)
{
List<int> employeeIds = GetFilteredEmployeeIdsBySearchTerm();
List<int> filteredEmployeeIds = employeeIds.Clone();
// Now filter the results based on which checkboxes are ticked
foreach (var employeeId in employeeIds)
{
// 3rd party API used to get values - please ignore for this example
Member m = new Member(employeeId);
if (m.IsInGroup("Employees"))
{
int memberBrandId = Convert.ToInt32(m.getProperty("brandID").Value);
// Filter by brand
List<int> filteredEmployeeIdsByBrand = new List<int>();
if (brandId != null)
{
if (brandId == memberBrandId)
filteredEmployeeIdsByBrand.Add(m.Id);
var setToRemove = new HashSet<int>(filteredEmployeeIdsByBrand);
filteredEmployeeIds.RemoveAll(x => !setToRemove.Contains(x));
}
}
}
return filteredEmployeeIds;
}
As you can see, I'm basically attempting to remove all records from the cloned record set, wherever the id doesn't match in the second collection. However for some reason every record seems to be getting removed.
Anybody know why?
P.S: Just to clarify, I have put in logging to check the values throughout the process and there are records appearing in the second list, however for whatever reason they're not getting matched in the RemoveAll()
Thanks
Ok only minutes after posting this I realised what I did wrong: The scoping is incorrect. What it should've been was like so:
public List<int> GetFilteredEmployeeIds(int? brandId)
{
List<int> employeeIds = GetFilteredEmployeeIdsBySearchTerm();
List<int> filteredEmployeeIds = employeeIds.Clone();
List<int> filteredEmployeeIdsByBrand = new List<int>();
// Now filter the results based on which checkboxes are ticked
foreach (var employeeId in employeeIds)
{
Member m = new Member(employeeId);
if (m.IsInGroup("Employees"))
{
int memberBrandId = Convert.ToInt32(m.getProperty("brandID").Value);
// Filter by brand
if (brandId != null)
{
if (brandId == memberBrandId)
filteredEmployeeIdsByBrand.Add(m.Id);
}
}
}
var setToRemove = new HashSet<int>(filteredEmployeeIdsByBrand);
filteredEmployeeIds.RemoveAll(x => !setToRemove.Contains(x));
return filteredEmployeeIds;
}
Essentially the removal of entries needed to be done outside the loop of the employee ids :-)
I know that you said your example was stripped down, so maybe this wouldn't suit, but could you do something like the following:
public List<int> GetFilteredEmployeeIds(int? brandId)
{
List<int> employeeIds = GetFilteredEmployeeIdsBySearchTerm();
return employeeIds.Where(e => MemberIsEmployeeWithBrand(e, brandId)).ToList();
}
private bool MemberIsEmployeeWithBrand(int employeeId, int? brandId)
{
Member m = new Member(employeeId);
if (!m.IsInGroup("Employees"))
{
return false;
}
int memberBrandId = Convert.ToInt32(m.getProperty("brandID").Value);
return brandId == memberBrandId;
}
I've just done that off the top of my head, not tested, but if all you need to do is filter the employee ids, then maybe you don't need to clone the original list, just use the Where function to do the filtering on it directly??
Please someone let me know if i've done something blindingly stupid!!
Two lists. One is a object representing a group of people I sent a email to. For brevity lets say the structure is
public class EmailSent
{
public int Id {get;set;}
public string Email {get;set;}
public bool HasResponse {get;set;}
}
That has a backing table of every email I've sent (minus the HasResponse column). I have another table that stores all the responses.
public class EmailResponse
{
public int Id {get;set;}
public string Response {get;set;}
}
I have a test that is currently failing and I can't figure out how to get it to pass. In production I basically query the EmailSent table with something like SELECT Id, Email from EmailSent where Id between #MinId and #MaxId
Nothing fancy there. My test basically does a yield return that does a single EmailSent between each of those numbers... The next part is that I do a select on that list to give me the Id's and make a second query to the EmailResponse. SELECT Id from EmailResponse WHERE Id in (...generate list of id's) In my test I write
public IEnumerable<EmailResponse> GetEmailResponses(IEnumerable<long> Ids, int waveId)
{
foreach (var id in Ids.Take(10))
{
yield return new EmailResponse {Id = id};
}
}
The test that is failling is this
[Test]
public void WhenAnEmailGroupIsSelectedSpecificInformationIsShown()
{
_viewModel.SelectedEmailGroup = _viewModel.EmailGroups[0];
_viewModel.Emails.Count.Should().Be(286);
_viewModel.Emails.Count(x => x.HasMatchingResult).Should().Be(10);
}
it's failling with error message, expected 10 for the count, but found 0. What I have in place right now is (changed var to IEnumerable for clarity)
IEnumerable<EmailGroup> emails = _dao.GetEmailsSent(SelectedEmailGroup);
IEnumerable<EmailResponse> results = _dao.GetEmailResponses(emails.Select(x => x.Id), SelectedEmailGroup.WaveId);
IEnumerable<EmailGroup> matches = emails.Join(results, x => x.Id, y => y.Id, (x, y) => x).ToList();
//matches.ForEach(x => x.HasMatchingResult = true); this is the line that probably needs to change
foreach (var email in emails)
{
Emails.Add(email);
}
it's obvious to me what is wrong, but i can't figure out how to easily update emails based on responses. Please help :)
The most likely problem is that you do not have a ToList() on making emails IEnumerable, meaning that it would be re-generated again when your unit tests ask for it. At this point the HasMatchingResult flag would be lost, so your tests would fail. Fixing this one is easy - simply add ToList to the call that makes emails, and uncomment your ForEach:
IEnumerable<EmailGroup> emails = _dao.GetEmailsSent(SelectedEmailGroup).ToList();
You do not need to perform a join there: all you have to do is picking EmailGroups that have matching Ids among the responces:
ISet<int> emailIdsWithResponses = new HashSet<int>(results.Select(r => r.Id));
IEnumerable<EmailGroup> matches = emails.Where(e => emailIdsWithResponses.Contains(e.Id)).ToList();
At this point you can call your ForEach, or better yet walk through the items in a "plain" foreach loop, setting their HasMatchingResult flags:
foreach (var e in matches) {
e.HasMatchingResult = true;
}