Customized sorting on DataTable in C#? - c#

This is a C# Winform question. I have a DataGridView which is bounded to a DataTable. I construct the DataTable myself, which several DataColumn instances. When the DataTable is bound to the DataGridView, by default, every column is sortable by clicking the headers of the DataGridView.
But the sorting behavior is something "by default". It seems that it is sorted by string. This is true even if I put this as my code:
DataColumn dc = new DataColumn("MyObjectColumn", typeof(MyObject));
And MyObject has overriden ToString() and has implemented the IComparable interface. That means even if I have told the DataTable how to sort the special column with the implementation of IComparable interface, DataGridView still doesn't do it the way I expect.
So how can I let DataTable sort data in the way I want?
Thanks for the answers.

I would recommend using the DefaultView of the DataTable. It has some built in sorting features that are little more extendable. The easiest is RowFilter, but I'm not sure if this will be what you're looking for if your data types are overridden as .ToString() at the table level.
EDIT: added code snippet
A custom method like this that maybe even overrides or is called during the sort event of your DataGridView might be able to sort a DataView before the binding actually occurs. But as I understand it, the IComparable never gets called unless you specify it to be called.
protected void SortGrid()
{
System.Data.DataView dv = myDataTable.DefaultView;
myOjbect comparer = new MyObject();
// Comparer specifics go here. Sort order, column/fieldname etc
// or any custom properties used in sorting
dv.Sort(comparer)
dgMyGrid.DataSource = dv
dgMyGrid.DataBind()
}

I had to deal with this today. I've implemented a natural sort (hat tip: Natural Sort Order in C#) and had to sort a DataTable. This is more of a "low fi" method, but it did the trick for the small dataset I'm working with.
I'm creating a key/value value relationship between my sorting column, and the DataRow itself, and popping it into a SortedList constructed with an IComparer.
DataTable myDataTable = {all my data...}
SortedList myDataNaturallySorted = new SortedList(new NaturalComparer());
foreach (DataRow dataRow in myDataTable.AsEnumerable())
myDataNaturallySorted.Add(dataRow["columWithKeyName"].ToString(), dataRow);
Then I moved forward using the sorted list as a data source for my repeater.

Related

Completely exclude columns in DataGridView

I have a DataTable I want to use as a DataSource of a DataGridView, but with a twist: for reasons (below) I need to exclude a column from DataGridView (not just hide it, truly exclude), ideally preventing it from being generated alltogether. Theoretically, I can call Columns.RemoveAt at an appropriate moment (which is the DataBindingComplete event handler - docs), but that's too late for me (for reasons).
An obvious solution is to set AutoGenerateColumns = false and do it manually. Without having looked into the details of this, I fear I'd need to reinvent the wheel in this scenario (to keep the data bindings working etc).
My reasons for this whole esoteric are, there is huge legacy app, originally written in VB6, and there any byte-array column is just ignored by the MS Hierarchical Grid. I'm trying to emulate this behavior in a custom control derived from DataGridView, and most stuff works.
UPDATE/ANSWER
Just set dt.Columns[0].ColumnMapping = MappingType.Hidden; (courtesy of https://stackoverflow.com/a/31282356/5263865)
In modern programming, there is a tendency to separate your data (=Model) from how the data is shown to the operator (=View). An adapter class (=ViewModel) is needed to connect the Model to the View. Abbreviated this gives MVVM. If you are not familiar with this concept of separation, consider to do some background reading.
Your Data is in a DataTable. You didn't mention what kind of items are in the DataTable. To ease the discussion I'll assume that the DataTable contains a sequence of Products.
class Product
{
...
}
You have methods to put Products in the DataTable and to Access them. Something like:
interface IProductRepository
{
IEnumerable<Product> AllProducts {get;}
Product GetProductById(int productId);
void AddProductAtEnd(Product product);
void ReplaceProduct(Product product, int index);
...
}
etc. The exact methods are not important for the answer. What I try to explain is that when using this interface you hide that the Products are stored in a DataTable. This give you the freedom to change where your Products are stored: in a DataBase? A List? or maybe a file, or even the internet.
I use a generic term repository (warehouse) for something where you can store items, and later retrieve them, replace them with other items or remove them from the repository. This can be a DataTable, or a database, or maybe a file, a Dictionary, whatever. The nice thing is that I've hidden that the Products are in a DataTable.
The DataGridView
When accessing the data in a DataGridView, people tend to fiddle directly with the DataGridViewCells and DataGridViewRows.
Well, don't!
Use DataBinding instead.
In almost all forms that have DataGridViews I have the following properties:
BindingList<Product> DisplayedProducts
{
get => (BindingList<Product>)this.DataGridView1.DataSource;
set => this.DataGridView1.DataSource = value;
}
Product CurrentProduct => this.DataGridView1.CurrentRow as Product;
IEnumerable<Product> SelectedProducts => this.DataGridView1.SelectedRows
.Select(row => row.DataboundItem)
.Cast<Product>();
Back to your question
for reasons (below) I need to exclude a column from DataGridView (not just hide it, truly exclude), ideally preventing it from being generated
If I read your question literally: you don't want to generate the DataGridViewCells that are in columns that are excluded.
This does not influence the Product that each row represents, it only influences the display of these Products. For example: even though each Product has an Id, you might want not to Display this Id.
The most easy thing for this is to use visual studios designer for this. Instead of defining the columns with the DataBinder, just add the columns one by one, and use the properties of each column for the name of the column, the name of the property that it has to show, the format that is used to show the value.
Code will look like this:
DataGridView dataGridView1 = new DataGridView();
// Column to show Product.Id
DataGridViewColumn columnProductId = new DataGridViewColumn();
columnProductId.HeaderText = "ID";
columnProductId.DataPropertyName = nameof(Product.Id);
// Column to show Product.Name
DataGridViewColumn columnProductName = new DataGridViewColumn();
columnProductName.HeaderText = "Name";
columnProductName.DataPropertyName = nameof(Product.Name);
// etc. for all columns that you want to show
Note: in DataPropertyName you store the name of the Property that must be shown in this column. I use the keyword nameof, so if later the name of the property changes, this won't be a problem.
Of course, if you want some special formatting, for example for numbers or dates, you need to set the proper properties as well. This can also be done in visual studio designer.
Once that you have defined your columns, add them to the DataGridView.
To Display the Products is a two-liner:
IDataTableProducts ProductRepository {get;} // initialize in constructor
void ShowProducts()
{
IEnumerable<Product> productsToDisplay = this.ProductRepository.AllProducts;
this.DisplayedProducts = new BindingList<Product>(productsToDisplay.ToList());
}
I stumbled upon an answer: https://stackoverflow.com/a/31282356/5263865
Setting the column.ColumnMapping = MappingType.Hidden does exactly what I needed: the column isn't autogenerated anymore.
DataTable data;
data.Columns[0].ColumnMapping = MappingType.Hidden;

Binding an ArrayList containing DataRow objects to DataGridView.DataSource

I'm trying to bind Items from an ArrayList called duplicates to DataGridView. However It is not showing data from ArrayList ItemArray which is what I want but instead showing metadata.
duplicates is type ArrayList:
duplicates = compare.FindDuplicates(existingLeadFilePath, newLeadFilePath);
gridViewMain.DataSource = duplicates;
The Results
You are trying to show an ArrayList containing some DataRow objects and the DataGridView is showing properties of DataRow. It's expected.
But propbably you have noticed that when you show a DataTable in DataGridView, the control shows the columns rather than properties.
Why? What's the difference between showing a DataTable and showing a list of DataRow objects?
The difference is here, DataGridView shows DefaultView property of the DataTable which is of type of DataView which implements ITypedList. So it shows just what is returned by GetItemProperties which in fact is asked from DataTable and it's a list of table columns.
How can I show a list of DataRow object in DataGridView?
Just add them to a table having the same schema of the origibal table. For examle:
private void Form1_Load(object sender, EventArgs e)
{
var originalTable = new DataTable();
originalTable.Columns.Add("C1");
originalTable.Columns.Add("C2");
//Original data
originalTable.Rows.Add("A", "B");
originalTable.Rows.Add("X", "Y");
originalTable.Rows.Add("A", "B");
originalTable.Rows.Add("X", "Y");
//An ArrayList containing duplicate DataRow objects
var duplicates = new System.Collections.ArrayList();
duplicates.Add(originalTable.Rows[2]);
duplicates.Add(originalTable.Rows[3]);
//Create a Table having the same schema of the original table
var duplicatesTable = originalTable.Clone();
//Add copy of duplicates to the duplicate table
foreach (DataRow item in duplicates)
{
duplicatesTable.Rows.Add(item.ItemArray);
}
this.dataGridView1.DataSource = duplicates;
}
Note
You don't need to use ArrayList when working with DataTable. You have different options like creating an empty DataTable with the same schema using Clone method of DataTable, creating a copy of data and schema using Copy method, perform some searches using AsDataView and AsEnumerable and AsEnumerable().CopyToDataTable() and so on.
As far as I can see you want to bind these numbers to the datagridview. What you are doing is binding an array list with some objects that has this array you want as a member, and that's what the datagridview displays.
Try to not use an array with 5 items but a class with 5 members and then use a list of these objects as your data source.
Extended tutorial can be found here: http://www.c-sharpcorner.com/article/binding-an-arraylist-with-datagrid-control/
Please excuse my bad English, I'm writing from my mobile phone.
This problem goes away if you change the FindDuplicates() method to instead return a strongly-typed List<T> with a real type in there. Then the DataGridView has actual properties it can look into to know what columns to create.
ArrayLists are dead. You should pretty much never user them in new code. Even if you legitimately can't find a real type to use with a List<T>, List<Object> is still a better choice over ArrayList, as it communicates to the maintainer that the base Object type is really what you want, and it sets you up for easier change if you're ever able to get a stronger type (like an interface) in the future.

Shortest way to turn a regular IEnumerable collection into a DataView

I'm not used to working with DataTables and all its built in functionalities, rather I usually use it for its debug visual display feature.
I'm new to WPF, and although I am experienced with linq/IEnumerable extension methods, I learned that a DataGrid, in order to support editing, the object you assigned to the ItemsSource property must implement a given interface (which I am not sure what it is, for the time being let's say it is either IEditableCollectionView or IBindingList).
Since I am used to collection manipulations via IEnumerable extension methods, here is how I easily manipulate a datatable, filter it and project only the columns I want:
grdSettings.ItemsSource =
_settings
.AsEnumerable()
.Where(row => row["table"].ToString().Equals(e.AddedItems[0].ToString()))
.Select(s => new
{
Setting = s["field"],
Description = s["description"],
CharValue = s["charValue"],
NumMinValue = s["minValue"],
NumMaxValue = s["maxValue"]
});
The problem with this is that the grid will not be editable. If I assign _settings.AsDataView(), the grid is editable. My problem then is I do not want all the columns, and I cannot make (I do not know how) an IEnumerable into a DataView. I read some posts about filtering columns from a datatable/dataview, but the solutions look a bit awkward, not as smooth as I would like...
Isn't there only a small piece I am missing I can "plug" into my solution, instead of having to give up the IEnumerable extension methods filtering... ?
Update: Mike Eason below suggested replacing my anonymous type with a predefined one, stating anonymous types are read only and this is the reason why the grid's collection source does not support editing.
here's the update with a strongly typed object, but it still does not work. Actually it does not make sense to think this alone would make it work, based on the premise stated above, that in order for a grid to allow editing its collection source object must implement a given interface for that.
grdSettings.ItemsSource =
_settings
.AsEnumerable()
.Where(row => row["table"].ToString().Equals(e.AddedItems[0].ToString()))
.Select(s => new GridRecord
{
Setting = s["field"].ToString(),
Description = s["description"].ToString(),
CharValue = s["charValue"].ToString(),
NumMinValue = s["minValue"].ToString(),
NumMaxValue = s["maxValue"].ToString()
});
In the absence of better solutions, I made use of the DataTableProxy nuget package, which will make a DataTable out of a regular IEnumerable<T>, dropped the datatable from my code, and used List<List<string>> instead (because I do not want to create a 2nd instance of a datatable just for the sake of editing the grid) - making a DataTable out of it using the package.
I have my editable grid, but I am still optimistic about simpler solutions :-)

binding a set of datarows to datagridview

I have tried the following code, but nothing is displayed in datagridview.
Any Suggestions?
string strFilterOption = "dtcolnPurchaseProductExpProductNo=270";
dgvProductExp.DataSource = dtPurchaseProductExp.Select(strFilterOption);
From MSDN
The DataGridView class supports the standard Windows Forms
data-binding model. This means the data source can be of any type that
implements one of the following interfaces:
The IList interface, including one-dimensional arrays.
The IListSource interface, such as
the DataTable and DataSet classes.
The IBindingList interface, such as
the BindingList class.
The IBindingListView interface, such as the
BindingSource class.
Due to the default behavior for databinding with array, you can't set, for the DataSource property, the array of Datarows that the Datatable.Select method will return. From this post:
The primary one is that the object implements IList or is a
1-Dimensional Array. The thing is, an array has a default behavior
for databinding - Reflection. The object type within the array is
reflected to discover its public properties which are not indexed and
with value types that can be represented in the grid. These
properties are then used as the Fields when the databinding occurs.
Since the data type in your array is DataRow, there are six public
properties: HasErrors, Item, ItemArray, RowError, RowState, and
Table. But Item is an indexed property and ItemArray is of type
Object(), which can't be displayed in a gridview, so these two
properties are ignored and the other four are shown in the grid.
So another way would consist to create a new DataTable, using Clone method in order to get the schema from the DataTable source, and populate the new DataTable with the array of DataRows.
string strFilterOption = "dtcolnPurchaseProductExpProductNo=270";
DataTable cloneTable;
cloneTable = dtPurchaseProductExp.Clone();
foreach (DataRow row in dtPurchaseProductExp.Select(strFilterOption))
{
cloneTable.ImportRow(row);
}
dgvProductExp.DataSource = cloneTable;
Or, you can also do your binding through a BindingSource object and use its Filter property.
Have you test dgvProductExp.DataSource.Tables(0).Rows.Count ? .. if it show 0 that's why ..
Or are you sure it's not
string strFilterOption = "dtcolnPurchaseProductExpProductNo='270'"; ?
In case if someone experiencing a similar issue, the easiest way will be to convert the data row array back to datatable and then setting the grid view data source.
dgvProductExp.DataSource = dtPurchaseProductExp.Select(strFilterOption).CopyToDataTable();

Add attributes to DataColumn

I am creating a reporting component that takes an IEnumerable input and performs some transformations and aggregations and returns a new IEnumerable with a dynamic number of columns. I am using ADO.NET for this because it is easy to create a DataTable with the appropriate columns.
The transformed IEnumerable is sent to a reporting visualization component that uses information stored in Attributes to format the results nicely. It's not necessary to be able to remove or change the attributes once they have been applied.
My question is this:
Is it possible to associate an Attribute with a DataColumn so that the PropertyDescriptor emitted by the ADO.NET DataView includes these Attributes?
Follow-up question:
If this is not possible with ADO.NET are there other libraries that I can use to accomplish this?
Edit: Update for clarity
I would like to be able to do something like this:
DataTable dt = new DataTable();
DataColumn example = dt.Columns.Add("Test",typeof(double));
//This is the functionality I am looking for ideally
example.AddAttribute(new CustomAttribute("Hello"));
public class CustomAttribute : Attribute
{
}
See the ExtendedProperties property on DataColumns. This allows you to add custom data to a datacolumn. You would have to write the code that uses this custom data yourself, however - the formatting (or what ever you intend the data to be used for) isn't automatic.
To answer my own question: It's not possible to inject attributes into a DataColumn and have them appear in the PropertyDescriptor that DataView's implementation of ITypedList emits

Categories

Resources