Remove DataGridView cells containing duplicate values - c#

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>();

Related

Check if value from website table already exists in IEnumerable<T>

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.

Sorting strings by specified order

I need to find out how to sort some data by pre-defined pattern.
Lets say I have some strings, which represents product informafion, e. g.
Product1, red, 70/n
Product6, blue, 90/n
Product3, red, 50/n
Product9, white, 33/n
I separated these strings by coma string split and stored them at different Arrays (name, color, price) and then DataTable with same columns.
I can order created rows by color using :
DataView.sort = "color"
or by LINQ with
DataRow[] dr = table.Select().OrderBy(u=>u[color]).ToArray();
DataTable sortedtable = dr.CopyToDataTable();
However this is just simple sorting, asc/desc, based on alphabet.
I would like to achieve sorting with pre-defined pattern. In example the item order would be defined by colors in order red, black, blue, white.
Is there anything simple I could do? I think this is possible with checking each row color and comparing it with predefined color list, then building new Array / DataTable based on this order. However I feel that this is weak approach.
You could store the order in another collection and then use IndexOf:
var colorOrderList = new List<string>{"red", "black", "blue", "white"};
table = table.AsEnumerable()
.OrderBy(row => colorOrderList.IndexOf(row.Field<string>("color")))
.CopyToDataTable();
You can define n ordering array like
var order = new [] { "red", "blue", "white"};
and then use IndexOf
DataRow.Select().OrderBy(u=>Array.IndexOf(order, u[color]))
Use IComparable to make a custom sort order.
https://support.microsoft.com/en-ca/help/320727/how-to-use-the-icomparable-and-icomparer-interfaces-in-visual-c
Products.OrderBy(u => u == "Red" ? 0 : u == "Black" ? 1 : u == "Blue" ? 2 : 3)
has the advantage that it should be translatable to a SQL statement so that the database server can do the sorting.
You can use IComaparable. First create a custom class which can accommodate your product details. Make this class implement the IComparable interface.
public class ProductDetails : IComparable
{
public int ProductId { get; set; }
public int CompareTo(object obj)
{
ProductDetails prodDetails = obj as ProductDetails;
if (obj == null) return 1;
if (prodDetails != null)
{
if (this.ProductId < prodDetails.ProductId) return 1;
else
return 0;
}
else {
return 0;
}
}
}

One-time sorting with ObjectListView

I have an ObjectListView populated with some data. When the user clicks a button, I do some data analysis and I need to sort rows of my ObjectListView based on results of my analysis - some rows should be on the top of the view, some rows should follow next...
This sorting is one-time sorting - It is not based on any column of the view and next time the analysis results might be completely different.
Firstly, I tried the most basic situation: I just need to put some rows on the top of the view. I checked this similar question:
Custom Sorting with ObjectListView
and I wrote this piece of code:
SortDelegate prevDelegate = myListView.CustomSorter;
IComparer prevComparer = myListView.ListViewItemSorter;
myListView.CustomSorter = delegate(OLVColumn column, SortOrder order)
{
myListView.ListViewItemSorter = new MyComparer(specialItemsList);
};
myListView.Sort();
myListView.ListViewItemSorter = prevComparer;
myListView.CustomSorter = prevDelegate;
where
class MyComparer : IComparer
{
private readonly List<string> specialItemsList;
public SetPrivilegeComparer(List<string> specialItemsList)
{
this.specialItemsList= specialItemsList;
}
public int Compare(object xobj, object yobj)
{
if (!(xobj is MyListItem && yobj is MyListItem))
return 0;
MyListItem x = (MyListItem )xobj;
MyListItem y = (MyListItem )yobj;
if (specialItemsList.Contains(x.strVal) && specialItemsList.Contains(y.strVal))
{
return 0;
}
if (!specialItemsList.Contains(x.strVal) && !specialItemsList.Contains(y.strVal))
{
return 0;
}
if (specialItemsList.Contains(x.strVal) && !specialItemsList.Contains(y.strVal))
{
return 1;
}
if (!specialItemsList.Contains(x.strVal) && specialItemsList.Contains(y.strVal))
{
return -1;
}
return 0;
}
}
The problem is nothing happens when I run the above code, the Compare method is never called. I studied http://objectlistview.sourceforge.net/cs/recipes.html#how-can-i-do-some-fancy-sorting, but I probably did not understand the trick with CustomSorter and ListViewItemSorter - what are responsibilities of these two classes? Thanks for any idea!

How to sort stock months using linq C#

I have some stock data that I'm trying to sort using linq, however I'm very unfamiliar with linq and cannot understand the documentation.
Right now I have a list of bars (a class I've created that holds the stock data) and it's all of the stock data for each day since 1990. Now I'm trying to group this stock data by year and month so I can turn daily stock data into monthly stock data (the resolution of the stock).
public class Stock
{
private string stockSymboll;
private string period;
private List<bar> aBar = new List<bar>();
private DateTime startingDate;
private DateTime endingDate;
enum period { DAILY, WEEKLY, MONTHLY };
private period PeriodType;
}
public class bar
{
private double open;
private double high;
private double low;
private double close;
private double volume;
private DateTime stockDate;
}
within the stock class I have a function that I'm trying to use to convert from a list of daily data to a list of monthly data, in order from most recent to least recent.
Here is what I've attempted:
stock convertPeriod(Period pt)
{
stock newStock = new Stock(stockName, startingDate, endingDate, period);
if (pt == Periode.MONTHLY)
{
List<bar> monthlyGroup = new List<bar>();
var group1 = (from b in bar group c by b.getDate().Month);
var group2 = from g in group1 group g by g.getDate().Year)
return...;
}
}
However I've discovered that you cannot sort a var. So I was thinking the best way would be to attempt a nested query in linq, however I can hardly seem to even get basic queries to work. Any help would be greatly appreciated.
I'm not 100% clear on what contract you're supposed to be setting up, but as I understand it, you want to:
Receive an IEnumerable<Bar>, and
Return an IEnumerable<Bar> such that StockDate is the first day of the year and month for the summarized stocks, all sorted descending by date.
As I understand it, Stock is more or less irrelevant to your true question here. If I'm incorrect in that, let me know and I can help you take this that one step further.
You have a good start on this in your LINQ. I'm a little confused about your use of private fields in the types, but I'm assuming those to be typos, and that your actual code uses public, probably properties.
I'll do this as a couple separate methods just to make what I'm doing more clear, but you may want to bring them together in the interest of performance. Particularly since I order the collection twice, effectively.
This method groups and sorts the data, based on the first day of their year-month set. Note that you can actually perform a grouping on an anonymous type. Sorting doesn't work on the anonymous object itself, as you noted, but it does work on its properties.
public IEnumerable<IEnumerable<Bar>> GroupIntoMonths(IEnumerable<Bar> bars)
{
return bars.GroupBy(c => new { c.StockDate.Year, c.StockDate.Month })
.OrderByDescending(c => c.Key.Year)
.ThenByDescending(c => c.Key.Month);
}
The choice is yours whether you prefer to group on an instantiated DateTime object with a date set of one, or what I've done here. I don't touch the Key property again, so I was fine effectively losing track of it after I left the method. Other implementations might drive you to make a different decision on that.
Once you've got that, it's a matter of converting an IEnumerable<Bar> into a single Bar that summarizes the whole period.
public IEnumerable<Bar> GroupIntoBars(IEnumerable<IGrouping<DateTime, Bar>> groups)
{
return groups.Select(GetBar);
}
public Bar GetBar(IEnumerable<Bar> bars)
{
Bar ret = new Bar();
Bar last = null;
int index = -1;
foreach(var v in bars.OrderBy(c => c.StartingDate))
{
index++;
if(index == 0)
{
ret.Open = v.Open;
ret.StockDate = v.StockDate;
ret.High = v.High;
ret.Low = v.Low;
}
else
{
ret.High = Math.Max(ret.High, v.High);
ret.Low= Math.Max(ret.Low, v.Low);
}
last = v;
}
if(last == null) throw new ArgumentException("Collection cannot be empty!");
ret.Close = last.Close;
return ret;
}
I think that method is pretty straight-forward, but let me know if I can clear anything up.
You can group by multiple properties at once by specifying them in an anonymous object that will be the group key:
var monthlyGroup = aBar.OrderBy(bar => bar.stockDate)
.GroupBy(bar => new { Year = bar.stockDate.Year, Month = bar.stockDate.Month })
//create bars from groups
.Select(g => new bar()
{
open = g.First().open,
high = g.Max(b => b.high),
low = g.Min(b => b.low),
close = g.Last().close,
volume = g.Average(b => b.volume),
stockDate = new DateTime(g.Key.Year, g.Key.Month, 1)
})
.ToList();
Sorry but I prefer the functions syntax of linq.
I noticed that fields in bar class are private so they will be inaccessible. However, I assume that you have properties for each field.
In that case you will have to replace field names with property names in the code above.

Lambda SQL Query / Manipulate String At Query Or After Result

I'm using C#, EF5, and Lambda style queries against SQL.
I have the usual scenario of binding data to gridviews. Some of the results for my columns may be too long (character count) and so I only want to display the first 'n' characters. Let's say 10 characters for this example. When I truncate a result, I'd like to indicate this by appending "...". So, let's say the following last names are returned:
Mercer, Smith, Garcia-Jones
I'd like them to be returned like this:
Mercer, Smith, Garcia-Jon...
I was doing something like this:
using (var context = new iaiEntityConnection())
{
var query = context.applications.Where(c => c.id == applicationPrimaryKey);
var results = query.ToList();
foreach (var row in results)
{
if (row.employerName.Length > 10)
{
row.employerName = row.employerName.Substring(0, Math.Min(10, row.employerName.ToString().Length)) + "...";
}
if (row.jobTitle.Length > 10)
{
row.jobTitle = row.jobTitle.Substring(0, Math.Min(10, row.jobTitle.ToString().Length)) + "...";
}
}
gdvWorkHistory.DataSource = results;
gdvWorkHistory.DataBind();
However, if I change my query to select specific columns like this:
var query2 = context.applications.Select(c => new
{
c.id,
c.applicationCode,
c.applicationCategoryLong,
c.applicationType,
c.renew_certification.PGI_nameLast,
c.renew_certification.PGI_nameFirst,
c.renew_certification.PAI_homeCity,
c.renew_certification.PAI_homeState,
c.reviewStatusUser,
c.dateTimeSubmittedByUser
})
The result appears to become read-only if specific columns are selected, and I really should be selecting just the columns I need. I'm losing my ability to edit the result set.
So, I'm rethinking the entire approach. There must be away to select the first 'n' characters on select, right? Is there anyway to append the "..." if the length is > 10 on select? That seems trickier. Also, I guess I could parse through the gridview after bind and make this adjustment. Or, perhaps there is a way to maintain my ability to edit the result set when selecting specific columns?
I welcome your thoughts. Thanks!
To quote MSDN
Anonymous types provide a convenient way to encapsulate a set of read-only properties into a single object without having to explicitly define a type first.
So you would have to define a class and select into that if you want read write capability.
e.g.
public class MyClass {
public int id { get; set; }
public string applicationCode {get; set; }
// rest of property defintions.
}
var query2 = context.applications.Select(c => new MyClass {
id = c.id,
applicationCode = c.applicationCode,
// Rest of assignments
};
As to just providing 10 character limit with ... appended. I'm going to assume you mean on the applicationcategoryLog field but you can use the same logic on other fields.
var query2 = context.applications.Select(c => new
{
c.id,
c.applicationCode,
applicationCategoryLong = (c.applicationCategoryLong ?? string.Empty).Length <= 10 ?
c.applicationCategoryLong :
c.applicationCategoryLong.Substring(0,10) + "...",
c.applicationType,
c.renew_certification.PGI_nameLast,
c.renew_certification.PGI_nameFirst,
c.renew_certification.PAI_homeCity,
c.renew_certification.PAI_homeState,
c.reviewStatusUser,
c.dateTimeSubmittedByUser
})

Categories

Resources