Custom Sorting Based on strings - c#

I want to sort the rows in my DataGrid view based on the "Product Name" column value. So, for example, the possible values for the above mentioned column are : "Module","RX5000", "RM5000". If my gridview has 10 rows, and the "Product Name" column has the values in the following order:
RX5000
RM5000
RM5000
Module
RX5000
RX5000
RM5000
Module
RX5000
RM5000
After I click a button, I want them in the following order (displayed in the same gridview):
Module
Module
RX5000
RX5000
RX5000
RX5000
RM5000
RM5000
RM5000
RM5000
How do I obtain this in C#?
I thought of creating a list of each type of product and then going through each row and filling up the lists based on the product name and then somehow merging the 3 lists into a 4th one and then assigning the 4th list to my mergeview. But I have 12 products (I'm only showing 3 here) so I don't want to have 12 lists. I want it more dynamic.
private void dataGridView1_SortCompare(object sender, DataGridViewSortCompareEventArgs e)
{
string cell1, cell2;
if (e.Column == "Your_Column")
{
if (e.CellValue1 == null) cell1 = "";
else cell1 = e.CellValue1.ToString();
if (e.CellValue2 == null) cell2 = "";
else cell2 = e.CellValue2.ToString();
if (cell1 == "Account Manager") { e.SortResult = -1; e.Handled = true; }
else
{
if (cell2 == "Account Manager") { e.SortResult = 1; e.Handled = true; }
else
{
if (cell1 == "Assistant Account Manager") { e.SortResult = -1; e.Handled = true; }
else
{
if (cell2 == "Assistant Account Manager") { e.SortResult = 1; e.Handled = true; }
}
}
}
}
}
I found the above code in a different Post but I don't know how to apply this to my problem or if it's even possible to apply it to mine.

The answer you found has the biggest part of the puzzle: you need to subscribe to and handle the DataGridView.SortCompare event. Unfortunately, the code you found there is very specific to that one particular scenario. It assumes that there is at most one of the "special" names, and it doesn't impose any particular ordering on the remaining ones.
In your example, it's not clear why you want "RX5000" be ordered such that it comes before "RM5000". A normal alphabetic sort would have those reversed. But, taking as an assumption that that deviation from a "normal alphabetic sort" is exactly your question, then all you need to do is define your desired order, and then refer to that in your SortCompare event handler.
For example, you might do something like this:
private readonly Dictionary<string, int> _sortOrder = new[] { "Module", "RX5000", "RM5000" }
.Select((s, i) => new KeyValuePair<string, int>(s, i))
.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
private void dataGridView1_SortCompare(object sender, DataGridViewSortCompareEventArgs e)
{
e.SortResult = _sortOrder[e.CellValue1.ToString()].CompareTo(_sortOrder[e.CellValue2.ToString()]);
e.Handled = true;
}
The _sortOrder_ dictionary is constructed by enumerating the elements of an array, which you define as the strings in the order you want them to be sorted, and using the string values along with their index in the array to create the key/value pairs used to initialize the dictionary. This has the effect of assigning specific numeric values to the strings, in the same order you want the strings to be sorted.
Then the dataGridView1_SortCompare() method is declared, with the implementation to simply delegate the comparison to the comparison of the numeric values assigned to the strings. I.e. the numeric value of the string representation of the first cell is retrieved from _sortOrder, and this value is compared to the numeric value, also retrieved from _sortOrder, of the string representation of the second cell.

Related

How to Group and Sort Objects in ObjectListView?

I am trying to group my list of objects in an ObjectListView.
The ObjectListView should group the objects based on the first column but then have that same column sorted based on a custom sort.
How do I do that? I have read through the documentation for ObjectListView:
http://objectlistview.sourceforge.net/cs/gettingStarted.html#gettingstarted
So far, I have implemented my custom sort but I am not sure how to trigger the grouping? Remember that I am trying to group on the first column but then apply a custom sort.
My custom sort relis on the BeforeSorting event:
// after initializing components
olv.BeforeSorting += olv_BeforeSorting;
Then...
private void olv_BeforeSorting(object sender,BrightIdeasSoftware.BeforeSortingEventArgs e)
{
olvDataSource.Sort((x, y) => x.Group.ID.CompareTo(y.Group.ID));
e.Handled = true;
}
The ObjectListView displays my ordered object list but it is not grouped together. Each object displays on its own row without a group heading.
How do I group my objects after sorting them?
You can force the grouping column as follows:
olv.ShowGroups = true;
olv.AlwaysGroupByColumn = olvColumn1;
If you want to show one value in the column and group by a different one you can use GroupByKeyGetter
olvColumn1.GroupKeyGetter = GroupKeyGetter;
Delegate would be something like:
private object GroupKeyGetter(object rowObject)
{
var o = rowObject as MyClass;
if(o == null)
return "unknown";
return o.ID;
}
Some stuff doesn't take affect till you call
olv.RebuildColumns();
Always Sort By (Arbitrary Function)
If you want to force sorting on some custom logic you can use ListViewItemSorter in the BeforeSorting event. This is similar to registering a CustomSorter (but that doesn't seem to work when ShowGroups is true).
olv.BeforeSorting += olv_BeforeSorting;
Then
private void olv_BeforeSorting(object sender, BrightIdeasSoftware.BeforeSortingEventArgs e)
{
//example sort based on the last letter of the object name
var s = new OLVColumn();
s.AspectGetter = (o) => ((MyClass)o).Name.Reverse().First();
this.olv.ListViewItemSorter = new ColumnComparer(
s, SortOrder.Descending, e.ColumnToSort, e.SortOrder);
e.Handled = true;
}
I am sharing this for anyone that might come across here looking for a way to apply a custom sort on groups within an ObjectListView.
There might be better ways to do this, but this way worked for me.
colFirst.GroupFormatter = (BrightIdeasSoftware.OLVGroup group, BrightIdeasSoftware.GroupingParameters parms) =>
{
ObjectA a = (OjectA)group.Key;
/* Add any processing code that you need */
group.Task = " . . . ";
group.Header = "Special Name: " + a.Name;
group.Subtitle = $("Object A: {a.Index}, Total Water Consumption: {a.WaterConsumption}");
// This is what is going to be used as a comparable in the GroupComparer below
group.Id = a.ID;
// This will create the iComparer that is needed to create the custom sorting of the groups
parms.GroupComparer = Comparer<BrightIdeasSoftware.OLVGroup>.Create((x, y) => (x.GroupId.CompareTo(y.GroupId)));
};
The OLVColumn.GroupFormatter is lightly explained here:
http://objectlistview.sourceforge.net/cs/recipes.html#how-do-i-put-an-image-next-to-a-group-heading
This works and is basically what is described in the cookbook here http://objectlistview.sourceforge.net/cs/recipes.html?highlight=sort#how-can-i-change-the-ordering-of-groups-or-rows-within-a-group
First subscribe to the olv BeforeCreatingGroups event.
Then create a custom sort comparator in the event handler. In this case for a group matching "Turtles" it will push to the end of the sort, but you can obviously have however much convoluted logic you want in there.
private void Olv_BeforeCreatingGroups(object sender, CreateGroupsEventArgs e)
{
e.Parameters.GroupComparer = Comparer<BrightIdeasSoftware.OLVGroup>.Create(
(x, y) => (
x.Header == "Turtles" ? 1
: x.GroupId.CompareTo(y.GroupId)
)
);
}
This is what I used initially since it's what was in the cookbook. But I ended up switching to something more like Marwan's answer because that one creates a space to reconfigure the group headers themselves.

Sorting a ListView by tag in C#

I have a list view with some columns listViewItems. When I use the Sort method listViewItems.Sort(); it sorts by column text by default. This is the code I use:
private void OnColumnClick(object sender, ColumnClickEventArgs e)
{
int sortColumn = 0; --I only want to sort if you click this column header, not others
if (e.Column == sortColumn)
{
if (listViewItems.Sorting == SortOrder.Ascending)
{
listViewItems.Sorting = SortOrder.Descending;
}
else
{
listViewItems.Sorting = SortOrder.Ascending;
}
}
listViewItems.Sort();
However, my items have a Tag, accessible by listViewItems[0].Tag;
I would like to use that tag to sort the list (in my case tag is an int), but I don't know how to do it nor find information about something similar. Sort method doesn't accept any parameters. I tried to create a column sorter, but it also expects a column.
ListViewColumnSorter lvwColumnSorter;
lvwColumnSorter = new ListViewColumnSorter();
listViewItems.ListViewItemSorter = lvwColumnSorter;
lvwColumnSorter.SortColumn = ?;
Any ideas?
Thanks in advance.
var items = ListView.Items.Cast<ListViewItem>().OrderBy(x => x.Tag).ToList();
ListView.Items.Clear();
ListView.Items.AddRange(items.ToArray());

Display item value to specific datagridcolumn selected from datagridcomboboxcolumn and dynamically compute for the total for each selection

I am developing windows form using c# and using datagridview object. I am almost done but I have a problem with displaying item value to a specific column(PTS GAIN COLUMN) that I selected in a comboboxcell all inside datagridview. Data is selected from database(coded). The column(PTS GAIN COLUMN) where I want to display the selected item value has no entry in the database. It is empty. I want that every time I select a item from a comboboxcell per row is that it will display the value to a specific column(PTS GAIN COLUMN) and compute the total dynamically/real-time(I want to show the result in label.text)
Also the combobox cell has items YES,NO,NA(this has no datatable, I just added the items by coding combobox.items.add("yes/no/na"). Yes item will get value depending on the column PTS AVAIL and display on column PTS GAIN. If I select no, 0 will display in PTS GAIN column, and if NA, both PTS AVAIL and PTS GAIN will have 0. Again I want to if possible to compute the total real-time.
Any help with this matter is much appreciated. I am meeting a dead line so please, anyone! Have a great day! Btw, I will post screenshot of the program, and if you want to see a particular block of code for reference just comment.
You will need to hook up with 2 events to accomplish this but for the most part it is pretty easy.
private void MyInitializeComponent()
{
dg.CurrentCellDirtyStateChanged += Dg_CurrentCellDirtyStateChanged;
dg.CellValueChanged += Dg_CellValueChanged;
this.CalculateTotals();
}
private void Dg_CellValueChanged(object sender, DataGridViewCellEventArgs e)
{
if (dg.Columns[e.ColumnIndex].Name == "Choices")
{
switch (dg.Rows[e.RowIndex].Cells["Choices"].Value.ToString())
{
case ("Yes"):
{
dg.Rows[e.RowIndex].Cells["PointsGained"].Value =
dg.Rows[dg.CurrentCell.RowIndex].Cells["PointsAvailable"].Value;
break;
}
case ("No"):
{
dg.Rows[e.RowIndex].Cells["PointsGained"].Value = 0;
break;
}
case ("NA"):
{
dg.Rows[e.RowIndex].Cells["PointsGained"].Value = "NA";
break;
}
}
this.CalculateTotals();
}
}
private void Dg_CurrentCellDirtyStateChanged(object sender, EventArgs e)
{
if ((dg.Columns[dg.CurrentCell.ColumnIndex].Name == "Choices") &&
(dg.IsCurrentCellDirty))
{
dg.CommitEdit(DataGridViewDataErrorContexts.Commit);
}
}
private void CalculateTotals()
{
var totalPointsGained = dg.Rows.Cast<DataGridViewRow>()
.Where(a => a.Cells["PointsGained"].Value?.ToString() != "NA")
.Sum(a => Convert.ToInt32(a.Cells["PointsGained"].Value));
var totalPointsAvailable = dg.Rows.Cast<DataGridViewRow>()
.Where(a => a.Cells["PointsAvailable"].Value?.ToString() != "NA")
.Sum(a => Convert.ToInt32(a.Cells["PointsAvailable"].Value));
lblTotalPointsGained.Text = "Total Points Gained: " + totalPointsGained;
lblTotalAvailable.Text = "Total Points Available: " + totalPointsAvailable;
}
You can put the code I have in MyInitializeComponent() wherever you initialize your other objects on the form.

How can I Validate Textboxes Values To Be Unique?

I have 12 text boxes, and I am trying to find a strategy to not permit duplicate entries in the TextBoxes by the user at run-time.
List<string> lstTextBoxes = new List<string>();
private void Textbox1(object sender, EventArgs e)
{
lstTextBoxes.Add(Textbox1.Text);
}
public bool lstCheck(List<string> lstTextBoxes,string input)
{
if(lstTextBoxes.Contains(input))
{
return true;
}
else
{
return false;
}
}
private void Textbox2(object sender, EventArgs e)
{
lstTextBoxes.Add(Textbox2.Text);
if (lstCheck(lstTextBoxes, Textbox2.Text))
{
MessageBox.Show("test");
}
}
public bool CheckForDuplicates()
{
//Collect all your TextBox objects in a new list...
List<TextBox> textBoxes = new List<TextBox>
{
textBox1, textBox2, textBox3
};
//Use LINQ to count duplicates in the list...
int dupes = textBoxes.GroupBy(x => x.Text)
.Where(g => g.Count() > 1)
.Count();
//true if duplicates found, otherwise false
return dupes > 0;
}
There are many ways to accomplish this. I've chosen to present a solution as an extension method but you can achieve the same result by simply placing the method within the class containing the list of textboxes or passing the list as a parameter. Which ever you prefer.
Cut and paste following into your project. Make sure you put it within the same namespace, otherwise, add the containing namespace as a reference.
public static class TextBoxCollectionExtensions
{
/// <summary>
/// Extension method that determines whether the list of textboxes contains a text value.
/// (Optionally, you can pass in a list of textboxes or keep the method in the class that contains the local list of textboxes.
/// There is no right or wrong way to do this. Just preference and utility.)
/// </summary>
/// <param name="str">String to look for within the list.</param>
/// <returns>Returns true if found.</returns>
public static bool IsDuplicateText(this List<TextBox> textBoxes, string str)
{
//Just a note, the query has been spread out for readability and better understanding.
//It can be written Inline as well. ( ex. var result = someCollection.Where(someItem => someItem.Name == "some string").ToList(); )
//Using Lambda, query against the list of textboxes to see if any of them already contain the same string. If so, return true.
return textBoxes.AsQueryable() //Convert the IEnumerable collection to an IQueryable
.Any( //Returns true if the collection contains an element that meets the following condition.
textBoxRef => textBoxRef //The => operator separates the parameters to the method from it's statements in the method's body.
// (Word for word - See http://www.dotnetperls.com/lambda for a better explanation)
.Text.ToLower() == str.ToLower() //Check to see if the textBox.Text property matches the string parameter
// (We convert both to lowercase because the ASCII character 'A' is not the same as the ASCII character 'a')
); //Closes the ANY() statement
}
}
To use it, you do something like this:
//Initialize list of textboxes with test data for demonstration
List<TextBox> textBoxes = new List<TextBox>();
for (int i = 0; i < 12; i++)
{
//Initialize a textbox with a unique name and text data.
textBoxes.Add(new TextBox() { Name = "tbField" + i, Text = "Some Text " + i });
}
string newValue = "some value";
if (textBoxes.IsDuplicateText(newValue) == true) //String already exists
{
//Do something
}
else
{
//Do something else
}

Add all elements of array to datagridview rows except one

I'm reading a text file line by line, and inserting it into an array.
I then have this list called custIndex, which contains certain indices, indices of the items array that I'm testing to see if they are valid codes. (for example, custIndex[0]=7, so I check the value in items[7-1] to see if its valid, in the two dictionaries I have here). Then, if there's an invalid code, I add the line (the items array) to dataGridView1.
The thing is, some of the columns in dataGridView1 are Combo Box Columns, so the user can select a correct value. When I try adding the items array, I get an exception: "The following exception occurred in the DataGridView: System.ArgumentException: DataGridViewComboBoxCell value is not valid."
I know the combo box was added correctly with the correct data source, since if I just add a few items in the items array to the dataGridView1, like just items[0], the combo box shows up fine and there's no exception thrown. I guess the problem is when I try adding the incorrect value in the items array to the dataGridView1 row.
I'm not sure how to deal with this. Is there a way I can add all of the items in items except for that value? Or can I add the value from items and have it show up in the combo box cell, along with the populated drop down items?
if(choosenFile.Contains("Cust"))
{
var lines = File.ReadAllLines(path+"\\"+ choosenFile);
foreach (string line in lines)
{
errorCounter = 0;
string[] items = line.Split('\t').ToArray();
for (int i = 0; i <custIndex.Count; i++)
{
int index = custIndex[i];
/*Get the state and country codes from the files using the correct indices*/
Globals.Code = items[index - 1].ToUpper();
if (!CountryList.ContainsKey(Globals.Code) && !StateList.ContainsKey(Globals.Code))
{
errorCounter++;
dataGridView1.Rows.Add(items);
}
}//inner for
if (errorCounter == 0)
dataGridView2.Rows.Add(items);
}//inner for each
}//if file is a customer file
Say your text file contains:
Australia PNG, India Africa
Austria Bali Indonisia
France England,Scotland,Ireland Greenland
Germany Bahama Hawaii
Greece Columbia,Mexico,Peru Argentina
New Zealand Russia USA
And lets say your DataGridView is setup with 3 columns, the 2nd being a combobox.
When you populate the grid and incorrectly populate the combobox column you will get the error.
The way to solve it is by "handling/declaring explicitly" the DataError event and more importantly populating the combobox column correctly.
private void dataGridView1_DataError(object sender, DataGridViewDataErrorEventArgs e)
{
//Cancelling doesn't make a difference, specifying the event avoids the prompt
e.Cancel = true;
}
private void dataGridView2_DataError(object sender, DataGridViewDataErrorEventArgs e)
{
e.Cancel = true;
}
So imagine the 2nd column contained a dropdownlist of countries and the 1st & 3rd column contained text fields.
For the 1st and 3rd columns they are just strings so I create a class to represent each row:
public class CountryData
{
public string FirstCountry { get; set; }
public string ThirdCountry { get; set; }
}
For the 2nd column "Countries" combobox cell's I have created a separate class because I will bind it to the 2nd columns datasource.
public class MultiCountryData
{
public string[] SeceondCountryOption { get; set; }
}
Populating the grid with combobox columns and the like as shown here: https://stackoverflow.com/a/1292847/495455 is not good practice. You want to separate your business logic from your presentation for a more encapsulated, polymorphic and abstract approach that will ease unit testing and maintenance. Hence the DataBinding.
Here is the code:
namespace BusLogic
{
public class ProcessFiles
{
internal List<CountryData> CountryDataList = new List<CountryData>();
internal List<MultiCountryData> MultiCountryDataList = new List<MultiCountryData>();
internal void foo(string path,string choosenFile)
{
var custIndex = new List<int>();
//if (choosenFile.Contains("Cust"))
//{
var lines = File.ReadAllLines(path + "\\" + choosenFile);
foreach (string line in lines)
{
int errorCounter = 0;
string[] items = line.Split('\t');
//Put all your logic back here...
if (errorCounter == 0)
{
var countryData = new CountryData()
{
FirstCountry = items[0],
ThirdCountry = items[2]
};
countryDataList.Add(countryData);
multiCountryDataList.Add( new MultiCountryData() { SeceondCountryOption = items[1].Split(',')});
}
//}
}
}
}
In your presentation project here is the button click code:
imports BusLogic;
private void button1_Click(object sender, EventArgs e)
{
var pf = new ProcessFiles();
pf.foo(#"C:\temp","countries.txt");
dataGridView2.AutoGenerateColumns = false;
dataGridView2.DataSource = pf.CountryDataList;
multiCountryDataBindingSource.DataSource = pf.MultiCountryDataList;
}
I set dataGridView2.AutoGenerateColumns = false; because I have added the 3 columns during design time; 1st text column, 2nd combobox column and 3rd text column.
The trick with binding the 2nd combobox column is a BindingSource. In design time > right click on the DataGridView > choose Edit Columns > select the second column > choose DataSource > click Add Project DataSource > choose Object > then tick the multiCountry class and click Finish.
Also set the 1st column's DataPropertyName to FirstCountry and the 3rd column's DataPropertyName to ThirdCountry, so when you bind the data the mapping is done automatically.
Finally, dont forget to set the BindingSource's DataMember property to the multiCountry class's SeceondCountryOption member.
Here is a code demo http://temp-share.com/show/HKdPSzU1A

Categories