One-time sorting with ObjectListView - c#

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!

Related

Remove DataGridView cells containing duplicate values

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

What collection should i use if i need to control the position of item and need to be able to serialize the collection

i need a collection and i am not sure which one to use. I have used List before but i need to also be sure about the specific position. If user views an item A i will ads it to the collection and if he sees another item B i will add that item on top of the first one and so on, but the limit number fot he items is 3 so i would remove the first item, also i need to be able to seriliaze the collection. I have tried Dictionary, but i could use XmlSerializer, so i have tried to use Lst<KeyValue<>> and now i am trying an array like this. Also had a look on Queue but i have found out that using XmlSerializer could also be an issue. Any suggestion for what collection i can use?
class Program
{
static void Main(string[] args)
{
string[] myObjArray = new string[3] ;
if(myObjArray[0] == null)
{
myObjArray[0] = "article1";
Console.WriteLine(myObjArray[0]);
}
if (myObjArray[1] == null )
{
myObjArray[1] = "article2";
Console.WriteLine(myObjArray[1]);
}
if (myObjArray[2] == null)
{
myObjArray[2] = "article3";
Console.WriteLine(myObjArray[2]);
}
var input = Console.ReadLine();
myObjArray[0] = input;
Console.WriteLine(myObjArray[0]);
}
}
You can use a List<Item> and use the Index as position and methods Insert and Delete to achieve your goal. If the position if encapsulated in the entity, you can create methods to manage it.
So when you add an item you will check if the count is over than the allowed and delete the first if nedeed.
[Serializable]
public class MyList
{
private readonly List<Item> Items = new List<Item>();
public int Count { get { return Items.Count; } }
public int MaxCount { get; set; } = 0;
public void Add(Item item)
{
if ( MaxCount > 0 && Items.Count >= MaxCount )
Items.RemoveAt(0);
Items.Add(item);
}
public void Insert(int index, Item item)
{
Items.Insert(index, item);
}
public int FindById(int id)
{
for ( int index = 0; index < Items.Count; index++ )
if ( Items[index].Id == id )
return index;
return - 1;
}
// Add all over new methods and wrapping methods needed
}
This code use 0 to indicate that the max count is not considered, but if the list may not accept items, it can manage -1 for that, so 0 indicates that the list is closed.
Perhaps you can use a LinkedList that is searializable but you need to implement it for XML:
https://learn.microsoft.com/dotnet/api/system.collections.generic.linkedlist-1
How to Xml serialize a LinkedList?
So with that you can easily manage items as you wrote:
Add a cell between two cells.
Add a cell before a cell.
Add a cell after a cell.
Add a cell at the start.
Add a cell at the end.
Remove the first.
Remove the last.
And so on...
Hence you can add automatic delete the first cell if the count is over the allowed.
When should I use a List vs a LinkedList
LinkedList in C# - tutorialspoint.com
Linked Lists - dotnetcademy.net
C# | LinkedList Class - geeksforgeeks.org
Linked List Implementation in C# - geeksforgeeks.org

Remove duplicates and put the list into a listbox

This is an uni assignment and I am having problem with part of it. This is the code;
namespace Assignment_1
{
public partial class Classifier : System.Web.UI.Page // We are using a web form as stated
{
protected void Page_Load(object sender, EventArgs e) // No variables are initiated for the beginning
{
}
protected void ButtonClassify_Click(object sender, EventArgs e)
{
if (this.TextBox1.Text != "")
{
List<string> numbersText = this.TextBox1.Text.Split(',').ToList<string>();
foreach (var item in numbersText)
{
int num = int.Parse(item);
if (RadioButtonList1.SelectedValue == "Both")
{
if (num % 2 == 0)
{
if (CheckBoxDuplicate.Checked == true)
{
List<int> evenNumbers = new List<int>();
evenNumbers.Add(num);
List<int> distinctEvenNumbers = evenNumbers.Distinct().ToList();
ListBoxEvenNumbers.DataSource = distinctEvenNumbers;
}
else
{
//Put the results into the respective boxes
ListBoxEvenNumbers.Items.Add(num.ToString());
}
}
else
{
//Put the results into the respective boxes
ListBoxOddNumbers.Items.Add(num.ToString());
}
}
if (RadioButtonList1.SelectedValue == "Even")
{
if (num % 2 == 0)
{
//Put the results into the respective boxes
ListBoxEvenNumbers.Items.Add(num.ToString());
}
}
if (RadioButtonList1.SelectedValue == "Odd")
{
if (num % 2 == 1)
{
//Put the results into the respective boxes
ListBoxOddNumbers.Items.Add(num.ToString());
}
}
Let me explain the question and what I have done. User inserts list of numbers into a text box and then has 3 options (radiolistbutton). He can list even, odd or both type of numbers. They display in even and odd listboxes(2 listboxes). I have done up to this part.
There is a checkbox to remove duplicates and the user can check it if he wishes to. If the button is checked, the code should remove the duplicates. I tried to do this part in the 4th "if-else" "if (CheckBoxDuplicate.Checked == true)". The way I understand it, I check if the number is even and then check the CheckboxDuplicate button. if it is checked I put the values in a new list and then delete repeated values. Then put into EvenNumbers listbox. For some reason, this part doesn't work.
If you would like to help me, please don't post just your answer. This is my first project in C# and it is difficult for me to understand an elegant solution yet. If you have time, please check my code and let me know where I made a mistake. Thanks for your time in advance.
Sprinkle a bit of linq magic on it, and you're done.
var my_list = new List<int>{1,2,3,4,5,5,6,7};
var pair = my_list.Where (n => n%2 ==0); // 2,4,6
var odd = my_list.Where (n => n%2 ==1); // 1,3,5,5,7
var unique_pair = pair.Distinct(); // stays the same
var unique_odd = odd.Distinct(); // 1,3,5,7
From here is just adding it to your appropriate calls and GUI containers
From your comment, here are a couple of things:
Change the if to if - else if, since only one will apply.
you can do it the way you do, but it's not the most efficient. If you go that way, you'll have to figure out which numbers you've added in order to not have duplicates.
alternatively, you can simply create the lists like I've done in the code above, and then assign them at the end. It'll save you time and code.
Here's some more help, with no code, since I believe i covered it.
Step 1: get the user input, and create a list of ints. (call it: input_list).
Step 2: According to what he chose (even, odd, both), you want to assign to each listbox, a list of numbers. Look at my above code, it'll do that bit for you.
Step 3: If user choses unique, you pass to those listboxes the Distinct list, again, look at my above code for that.
You can apply the unique on the event of the checkbox being selected if you prefer.
Notes:
Keep the list of ints (the input_list) as a variable, so you don't need to parse it whenever he changes his selection.
public enum PairOddEnum
{
Evens,
Odds,
Both
}
public void BindControl(PairOddEnum type)
{
if (this.textBox1.Text != "")
{
List<string> numbersText = this.textBox1.Text.Split(',').ToList<string>();
var evens = numbersText.Where(t => int.Parse(t) % 2 == 0).Distinct();
var odds = numbersText.Where(t => int.Parse(t) % 2 == 1).Distinct();
if (type == PairOddEnum.Evens)
{
ListBoxEvenNumbers.DataSource = evens.ToList();
}
else if (type == PairOddEnum.Odds)
{
ListBoxOddNumbers.DataSource = odds.ToList();
}
else
{
ListBoxEvenNumbers.DataSource = evens.ToList();
ListBoxOddNumbers.DataSource = odds.ToList();
}
}
}
protected void ButtonClassify_Click(object sender, EventArgs e)
{
if (RadioButtonList1.SelectedValue == "Both")
{
BindControl(PairOddEnum.Both);
}
if (RadioButtonList1.SelectedValue == "Even")
{
BindControl(PairOddEnum.Evens);
}
if (RadioButtonList1.SelectedValue == "Odd")
{
BindControl(PairOddEnum.Odds);
}
}

C# Removing a List from a List

Is there a convenient way to remove a nested list from another list if it meets certain requirements? For example, say we have a collection of stops, and we decide to call each collection of stops a route. Each route is in list from. Then we decide to put each route into a list as well.
So now that we have a list of routes, someone decides that certain types of routes really shouldn't be included in the route list. How can I remove those routes? Here's some sample code:
Example Class
public class Stops
{
public Stops(int _param1, string _param2)
{
param1 = _param1;
param2 = _param2;
}
public int param1 { get; set; }
public string param2 { get; set; }
}
Create the Lists
List<List<Stops>> lstRoutes = new List<List<Stops>>();
List<Stops> lstStops = new List<Stops>();
List<Stops> lstMoreStops = new List<Stops>();
// Create some stops
for (int i = 0; i < 5; i++)
{
lstStops.Add(new Stops(i, "some text"));
}
lstRoutes.Add(lstStops);
// Create some more stops
for (int i = 5; i < 10; i++)
{
lstMoreStops.Add(new Stops(i, "some more text"));
}
lstRoutes.Add(lstMoreStops);
How can I remove any route from lstRoutes that has, say, any param1 value greater than 6?
The simplest way (which can be applicable to all enumerables, not just lists) would be:
lstRoutes = lstRoutes.Where(r => !r.Any(s => s.param1 > 6)).ToList();
The snippet above creates a new list, so copying will occur which means both the performance and memory usage will slightly suffer. The most efficient way would be not adding those items to the list in the first place.
The second most efficient way would be to remove items from the list instead of constructing a new one, so the memory usage wouldn't be affected as much:
lstRoutes.RemoveAll(r => r.Any(s => s.param1 > 6));
List<Stops> stop = lstRoutes.Find(delegate(List<Stops> stp) { return stp.param1 > 6; });

how to subtract two datatables with linq

Is there any way to subtract two datatables in order to have rows of the first datatable that are not in the second one?
I have tried .Except() method like below but it does not work for me.
dt1.AsEnumerable().Except(dt2.AsEnumerable()).CopyToDataTable();
I think i have mistake in using this method but i could not find that?
You can create your own Comparer using IEquailtyComparer
public class CustomDataRowComparer : IEqualityComparer<DataRow>
{
public bool Equals(DataRow x, DataRow y)
{
for (int i = 0; i < x.Table.Columns.Count; i++)
{
if (x[i].ToString() != y[i].ToString())
{
return false;
}
}
return true;
}
public int GetHashCode(DataRow obj)
{
return obj.ToString().GetHashCode();
}
}
Later you can call it like:
CustomDataRowComparer myDRComparer = new CustomDataRowComparer();
var result2 = dt1.AsEnumerable().Except(dt2.AsEnumerable(),myDRComparer).CopyToDataTable();
Except will not work because the references are different. Each "copy" of a data row in memory is a different object, thus the equality comparison being made in Except will always return false.
You need to compare with something comparable, like row IDs. A couple ideas how to do this:
var rowsInFirstNotInSecond = dt1.Rows.Where(r1=>!dt2.Rows.Any(r2=>r1.ID == r2.ID));
Or:
var rowsInFirstNotInSecond = dt1.Rows.Where(r1=>dt2.FindById(r1.ID) == null);
I have used the following code to subtract two datatables from each other :
var query =
from row1 in dt1
where !(from row2 in dt2
select row2.ID)
.Contains(row1.ID)
select row1;
this code returns exactly what i want...
This Code works for sure:
var rows =dtFirst.AsEnumerable().Except(dtSecond.AsEnumerable(), DataRowComparer.Default);
DataTable result = null;
if (rows.Count() != 0)
result = rows.CopyToDataTable();

Categories

Resources