I have the code below. To explain there will always be values for the 'tl' variable.
At the moment its hard coded to always assume 4 columns in the row, but I want to make it work based on the count of the columns and make it build the levels based on how many columns there are, but there also needs to be a value in the column.
So at the moment if there is a value in column 2, it will build the 'ltwo' variable, and then if there is a value in column 3 it does the 'lthree'.
I want to make it build as many levels as it needs to so im not repeating code and having the same code over and over.
public static List<AdditionalPropertyType> SQLAddPropsStructured(DataTable dataTable, List<AdditionalPropertyType> currentadditionalproperties)
{
foreach (DataRow row in dataTable.Rows)
{
var tl = new AdditionalPropertyType
{
Name = row[0].ToString(),
Value = row[1].ToString()
};
if (!String.IsNullOrEmpty(row[2].ToString()))
{
var ltwo = new AdditionalPropertyType
{
Name = row[2].ToString()
};
var ltwolist = new List<AdditionalPropertyType>();
ltwolist.Add(tl);
ltwo.AdditionalProperties = ltwolist;
if (!String.IsNullOrEmpty(row[3].ToString()))
{
var lthree = new AdditionalPropertyType
{
Name = row[3].ToString()
};
var lthreelist = new List<AdditionalPropertyType>();
lthreelist.Add(ltwo);
lthree.AdditionalProperties = lthreelist;
currentadditionalproperties.Insert(0, lthree);
}
else
currentadditionalproperties.Insert(0, ltwo);
}
else
currentadditionalproperties.Insert(0, tl);
}
return currentadditionalproperties;
}
You can get the columns using the Columns property of the DataTable:
foreach (DataRow row in dataTable.Rows)
{
foreach(DataColumn column in dataTable.Columns)
{
Trace.WriteLine(column.ColumnName + " = " + row[column]);
}
}
You probably want to do something like this: (written on the websites, some minor typos can be present)
You need to iterate the additional columns and check if there is a value present. When there is a value, create a backup reference and renew your property.
public static List<AdditionalPropertyType> SQLAddPropsStructured(DataTable dataTable, List<AdditionalPropertyType> currentadditionalproperties)
{
// check if there are atleast 2 columns defined
if(dataTable.Columns.Count < 2)
throw new Exception("At least two columns are required");
// The result
var currentadditionalproperties = new List<AdditionalPropertyType>();
// iterate the rows
foreach (DataRow row in dataTable.Rows)
{
// create the base property
var tl = new AdditionalPropertyType
{
Name = row[0].ToString(),
Value = row[1].ToString()
};
// check the rest of the columns for additional names
foreach(int index=2;index<dataTable.Columns.Count;index++)
{
var columnValue = row[index].ToString();
// if the column is empty, discontinue the iteration
if(String.IsNullOrEmpty(columnValue))
break;
// create a backup reference.
var previous = tl;
// create a new AdditionalPropertyType
var tl = new AdditionalPropertyType { Name = columnValue };
// Create the list
tl.AdditionalProperties = new List<AdditionalPropertyType>();
// add the previous (backup reference)
tl.AdditionalProperties.Add(previous);
}
// insert the 'chain' of additional properties on the list at possition 0
currentadditionalproperties.Insert(0, tl);
}
// return the list
return currentadditionalproperties;
}
The first step is to reverse your condition and make use of the keyword continue
public static List<AdditionalPropertyType> SQLAddPropsStructured(DataTable dataTable, List<AdditionalPropertyType> currentadditionalproperties)
{
foreach (DataRow row in dataTable.Rows)
{
var tl = new AdditionalPropertyType
{
Name = row[0].ToString(),
Value = row[1].ToString()
};
if (String.IsNullOrEmpty(row[2].ToString())){
currentadditionalproperties.Insert(0, tl);
continue;
}
var ltwo = new AdditionalPropertyType
{
Name = row[2].ToString()
};
var ltwolist = new List<AdditionalPropertyType>();
ltwolist.Add(tl);
ltwo.AdditionalProperties = ltwolist;
if (String.IsNullOrEmpty(row[3].ToString())) {
currentadditionalproperties.Insert(0, ltwo);
continue;
}
var lthree = new AdditionalPropertyType
{
Name = row[3].ToString()
};
var lthreelist = new List<AdditionalPropertyType>();
lthreelist.Add(ltwo);
lthree.AdditionalProperties = lthreelist;
currentadditionalproperties.Insert(0, lthree);
}
return currentadditionalproperties;
}
Now, the code is clearer. The next step is to collect the repeating cases. Note the second case onward is repeating. Thus, do further simplification:
public static List<AdditionalPropertyType> SQLAddPropsStructured(DataTable dataTable, List<AdditionalPropertyType> currentadditionalproperties)
{
foreach (DataRow row in dataTable.Rows)
{
var tlprev = new AdditionalPropertyType
{
Name = row[0].ToString(),
Value = row[1].ToString()
};
bool isTlUpdated = true;
for (int i = 2; i <= 3; ++i) { //change this according to your need
if (String.IsNullOrEmpty(row[i].ToString()) && isTlUpdated){
currentadditionalproperties.Insert(0, tlprev);
isTlUpdated = false;
break; //note that this will now change to break to break from the current for-loop
}
var lnext = new AdditionalPropertyType
{
Name = row[i].ToString()
};
var lnextlist = new List<AdditionalPropertyType>();
lnextlist.Add(tlprev);
lnext.AdditionalProperties = lnextlist;
tlprev = lnext; //need to record this for the next loop or end of the case
isTlUpdated = true;
}
if (isTlUpdated) //correction by Jeroen
currentadditionalproperties.Insert(0, tlprev);
}
return currentadditionalproperties;
}
The key is to simplify the code step-by-step
You haven't posted all your code, so I had to guess in a couple of places (such as what the "currentAdditionalProperties" does).
I think that the below code illustrates what you want to do by making the logic extendable depending on how many columns the data table has.
The trick is to just store the "last thing" in a variable, so it can be used for the "current thing". At the end, whatever was the "last thing" is what you want to store in your "currentAdditionalProperties" object. I have commented so you can see the logic.
private List<AdditionalPropertyType> SQLAddPropsStructured(DataTable dataTable)
{
AdditionalPropertyType lastNewType; // to remember the previous new instance
// for all rows...
foreach (DataRow row in dataTable.Rows)
{
// the first type takes name and value from the first two fields
AdditionalPropertyType newType = new AdditionalPropertyType();
newType.Name = row[0].ToString();
newType.Value = row[1].ToString();
// remember this type: it is used as the AdditionalProperties for the NEXT type
lastNewType = newType;
// additional types start from field 2
int field = 2;
// iterate until we find a NULL field.
// If you want to check for the end of the fields rather than a NULL value, then instead use:
// while(field < dataTable.Columns.Count)
while(!String.IsNullOrEmpty(row[field].ToString()))
{
// create new type
var newSubType = new AdditionalPropertyType();
// get name
Name = row[field].ToString();
// new type takes the PREVIOUS type as its additional parameters
List<AdditionalPropertyType> propertyData = new List<AdditionalPropertyType>();
propertyData.Add(lastNewType);
newSubType.AdditionalProperties = propertyData;
// remember THIS type for the NEXT type
lastNewType = newSubType;
// process next field (if valid)
field++;
}
// put the last set of properties found into the current properties
currentAdditionalProperties.Insert(0, lastNewType);
return currentAdditionalProperties;
}
}
I got roped into some old code, that uses loose (untyped) datasets all over the place.
I'm trying to write a helper method to find the DataTable.Name using the names of some columns.....(because the original code has checks for "sometimes we have 2 datatables in a dataset, sometimes 3, sometimes 4)..and its hard to know the order. Basically, the TSQL Select statements conditionally run. (Gaaaaaaaaaaaaaahhh).
Anyway. I wrote the below, and if I give it 2 column names, its matching on "any" columnname, not "all column names".
Its probably my linq skillz (again), and probably a simple fix.
But I've tried to get the syntax sugar down..below is one of the things I wrote, that compiles.
private static void DataTableFindStuff()
{
DataSet ds = new DataSet();
DataTable dt1 = new DataTable("TableOne");
dt1.Columns.Add("Table1Column11");
dt1.Columns.Add("Name");
dt1.Columns.Add("Age");
dt1.Columns.Add("Height");
DataRow row1a = dt1.NewRow();
row1a["Table1Column11"] = "Table1Column11_ValueA";
row1a["Name"] = "Table1_Name_NameA";
row1a["Age"] = "AgeA";
row1a["Height"] = "HeightA";
dt1.Rows.Add(row1a);
DataRow row1b = dt1.NewRow();
row1b["Table1Column11"] = "Table1Column11_ValueB";
row1b["Name"] = "Table1_Name_NameB";
row1b["Age"] = "AgeB";
row1b["Height"] = "HeightB";
dt1.Rows.Add(row1b);
ds.Tables.Add(dt1);
DataTable dt2 = new DataTable("TableTwo");
dt2.Columns.Add("Table2Column21");
dt2.Columns.Add("Name");
dt2.Columns.Add("BirthCity");
dt2.Columns.Add("BirthState");
DataRow row2a = dt2.NewRow();
row2a["Table2Column21"] = "Table2Column1_ValueG";
row2a["Name"] = "Table2_Name_NameG";
row2a["BirthCity"] = "BirthCityA";
row2a["BirthState"] = "BirthStateA";
dt2.Rows.Add(row2a);
DataRow row2b = dt2.NewRow();
row2b["Table2Column21"] = "Table2Column1_ValueH";
row2b["Name"] = "Table2_Name_NameH";
row2b["BirthCity"] = "BirthCityB";
row2b["BirthState"] = "BirthStateB";
dt2.Rows.Add(row2b);
ds.Tables.Add(dt2);
DataTable dt3 = new DataTable("TableThree");
dt3.Columns.Add("Table3Column31");
dt3.Columns.Add("Name");
dt3.Columns.Add("Price");
dt3.Columns.Add("QuantityOnHand");
DataRow row3a = dt3.NewRow();
row3a["Table3Column31"] = "Table3Column31_ValueM";
row3a["Name"] = "Table3_Name_Name00M";
row3a["Price"] = "PriceA";
row3a["QuantityOnHand"] = "QuantityOnHandA";
dt3.Rows.Add(row3a);
DataRow row3b = dt3.NewRow();
row3b["Table3Column31"] = "Table3Column31_ValueN";
row3b["Name"] = "Table3_Name_Name00N";
row3b["Price"] = "PriceB";
row3b["QuantityOnHand"] = "QuantityOnHandB";
dt3.Rows.Add(row3b);
ds.Tables.Add(dt3);
string foundDataTable1Name = FindDataTableName(ds, new List<string> { "Table1Column11", "Name" });
/* foundDataTable1Name should be 'TableOne' */
string foundDataTable2Name = FindDataTableName(ds, new List<string> { "Table2Column21", "Name" });
/* foundDataTable1Name should be 'TableTwo' */
string foundDataTable3Name = FindDataTableName(ds, new List<string> { "Table3Column31", "Name" });
/* foundDataTable1Name should be 'TableThree' */
string foundDataTableThrowsExceptionName = FindDataTableName(ds, new List<string> { "Name" });
/* show throw exception as 'Name' is in multiple (distinct) tables */
}
public static string FindDataTableName(DataSet ds, List<string> columnNames)
{
string returnValue = string.Empty;
DataTable foundDataTable = FindDataTable(ds, columnNames);
if (null != foundDataTable)
{
returnValue = foundDataTable.TableName;
}
return returnValue;
}
public static DataTable FindDataTable(DataSet ds, List<string> columnNames)
{
DataTable returnItem = null;
if (null == ds || null == columnNames)
{
return null;
}
List<DataTable> tables =
ds.Tables
.Cast<DataTable>()
.SelectMany
(t => t.Columns.Cast<DataColumn>()
.Where(c => columnNames.Contains(c.ColumnName))
)
.Select(c => c.Table).Distinct().ToList();
if (null != tables)
{
if (tables.Count <= 1)
{
returnItem = tables.FirstOrDefault();
}
else
{
throw new IndexOutOfRangeException(string.Format("FindDataTable found more than one matching Table based on the input column names. ({0})", String.Join(", ", columnNames.ToArray())));
}
}
return returnItem;
}
I tried this too (to no avail) (always has 0 matches)
List<DataTable> tables =
ds.Tables
.Cast<DataTable>()
.Where
(t => t.Columns.Cast<DataColumn>()
.All(c => columnNames.Contains(c.ColumnName))
)
.Distinct().ToList();
To me sounds like you're trying to see if columnNames passed to the method are contained within Column's name collection of Table. If that's the case, this should do the work.
List<DataTable> tables =
ds.Tables
.Cast<DataTable>()
.Where(dt => !columnNames.Except(dt.Columns.Select(c => c.Name)).Any())
.ToList();
(Below is an append by the asker of the question)
Well, I had to tweak it to make it compile, but you got me there..
Thanks.
Final Answer:
List<DataTable> tables =
ds.Tables.Cast<DataTable>()
.Where
(dt => !columnNames.Except(dt.Columns.Cast<DataColumn>()
.Select(c => c.ColumnName))
.Any()
)
.ToList();
Final Answer (which is not case sensitive):
List<DataTable> tables =
ds.Tables.Cast<DataTable>()
.Where
(dt => !columnNames.Except(dt.Columns.Cast<DataColumn>()
.Select(c => c.ColumnName), StringComparer.OrdinalIgnoreCase)
.Any()
)
.ToList();
i have tried to search some examples about my approach but all questions was not close enough to what i was trying to achieve .
for the TLDR sake , Question is : how do i make it work as in plain sql query?
using c# - Winforms with SqlCompact4 and Linq to SQL
my scenario involves a form with all the relevant Db table columns as availble filters to query
and then on text change event of each filtertextbox as a filter, the datasource of the gridview updates accordingly
and because i allow filtered search via many of them columns i was trying to avoid use of some extra
lines of code.
so lets say we only concentrate on 4 columns
custID, name, email, cellPhone
each has its corresponding TextBox.
i am trying to make a query as follows :
first i systematically collect all Textbox into a List
var AllFormsSearchFiltersTBXLst = new List<TextBox>();
code that collects all tbx on current form
var AllFormsSearchFiltersTBXLst = [currentFormHere].Controls.OfType<TextBox>();
so now i have all of textboxes as filters regardless if they have any value
then check who has some value in it
forech textbox in this filters textboxes if text length is greater than zero
it means that current filter is active
then.. a second list AllFormsACTIVESearchfiltersTBXLst will contain only active filters
what i was trying to achieve was in same way i didn't have to specify each of textbox objects
i just looped through each of them all as a collection and didn't have to specify each via it's id
now i want to make a filter on a dbContext using only those active filters
so i will not have to ask if current tbxName is email
like
query = db.Where(db=>db.email.Contains(TbxEmail.Text));
and again and again for each of 10 to 15 columns
what i have got so far is nothing that implements what i was heading to.
using (SqlCeConnection ClientsConn = new SqlCeConnection(ConfigurationManager.ConnectionStrings["Conn_DB_RCL_CRM2014"].ConnectionString))
{
System.Data.Linq.Table<ContactsClients> db = null;
// get all column names from context
var x =(System.Reflection.MemberInfo[]) typeof(ContactsClients).GetProperties();
using (DB_RCL_CRM2014Context Context = new DB_RCL_CRM2014Context(ClientsConn))
{
if (!Filtered)
db = Context.ContactsClients;//.Where(client => client.Name.Contains("fler"));
else
{
db = Context.ContactsClients;
// filters Dictionary contains the name of textBox and its value
// I've named TBX as Columns names specially so i could equalize it to the columns names when needed to automate
foreach (KeyValuePair<string,string> CurFltrKVP in FiltersDict)
{
foreach (var memberInfo in x)
{
// couldn't find out how to build the query
}
}
}
BindingSource BS_Clients = new BindingSource();
BS_Clients.DataSource = db;
GV_ClientInfo_Search.DataSource = BS_Clients;
what i normally do when working with plain sql is
foreach textbox take its value and add it into a string as filter
var q = "where " ;
foreach(tbx CurTBX in ALLFILTERTBX)
{
q +=CurTBX.Name +" LIKE '%" + CurTBX.Text + "%'";
// and some checking of last element in list off cores
}
then pass this string as a filter to the main select query ... that simple
how do i make it work as in plain sql query?
I think that you're trying to get the property of db dynamically, like: db.email according to the looped name of your textbox (here 'email'). However, I recommend you to do it some other way. I'd make a switch for each type of the property, like: email, name etc. Something like this:
// Create a list for the results
var results = new List<YourDBResultTypeHere>();
foreach(tbx CurTBX in ALLFILTERTBX)
{
switch(CurTBX.Name) {
case "email":
results.AddRange(db.Where(db => db.email.Contains(tbx.Text)).ToList());
break;
case "name":
results.AddRange(db.Where(db => db.name.Contains(tbx.Text)).ToList());
break;
}
}
try this
void UpdateGridViewData(bool Filtered=false, Dictionary<string,string> FiltersDict = null)
{
using (SqlCeConnection ClientsConn = new SqlCeConnection(ConfigurationManager.ConnectionStrings["Conn_DB_RCL_CRM2014"].ConnectionString))
{
System.Data.Linq.Table<ContactsClients> db = null;
IEnumerable<ContactsClients> IDB = null;
BindingSource BS_Clients = new BindingSource();
System.Reflection.MemberInfo[] AllDbTblClientsColumns = (System.Reflection.MemberInfo[])typeof(ContactsClients).GetProperties();
using (DB_RCL_CRM2014Context Context = new DB_RCL_CRM2014Context(ClientsConn))
{
if (!Filtered)
{
db = Context.ContactsClients;
BS_Clients.DataSource = db;
}
else
{
string fltr = "";
var and = "";
if (FiltersDict.Count > 1) and = "AND";
for (int i = 0; i < FiltersDict.Count; i++)
{
KeyValuePair<string, string> CurFltrKVP = FiltersDict.ElementAt(i);
if (i >= FiltersDict.Count-1) and = "";
for (int j = 0; j < AllDbTblClientsColumns.Length; j++)
{
if (AllDbTblClientsColumns[j].Name.Equals(CurFltrKVP.Key))
{
fltr += string.Format("{0} Like '%{1}%' {2} ", AllDbTblClientsColumns[j].Name, CurFltrKVP.Value, and);
}
}
}
try
{
IDB = Context.ExecuteQuery<ContactsClients>(
"SELECT * " +
"FROM ContactsCosmeticsClients " +
"WHERE " + fltr
);
BS_Clients.DataSource = IDB;
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
GV_ClientInfo_Search.DataSource = BS_Clients;
}
}
}
is it possible to create a value range constraint on a DataTable in C#?
I'm dynamically adding a column to a DataTable:
this.PrimaryCorrelationMatrix.Columns.Add(sName, typeof(int));
but I'd like all values within this Column to be integers from [0, 10]. Can I implement such a constraint directly on the DataTable?
The next best option I can think of is to create some object with possible values [0, 10], and instead of typeof(int), using typeof(specialObj).
One way to do this is to inspect the e.ProposedValue in the ColumnChanging event of the DataTable.
To have the constraint on a particular column, you can use the ExtendedProperties collection of the DataColumn to act as your flag to check those constraints:
DataTable dt = new DataTable();
DataColumn dc = new DataColumn("Range", typeof(int));
dc.ExtendedProperties.Add("Min", 0);
dc.ExtendedProperties.Add("Max", 10);
dt.Columns.Add(dc);
dt.ColumnChanging += dt_ColumnChanging;
In the ColumnChanging event, you would check if those properties exist, then use them:
void dt_ColumnChanging(object sender, DataColumnChangeEventArgs e) {
if (e.Column.ExtendedProperties.ContainsKey("Min") &&
e.Column.ExtendedProperties.ContainsKey("Max")) {
int min = (int)e.Column.ExtendedProperties["Min"];
int max = (int)e.Column.ExtendedProperties["Max"];
if ((int)e.ProposedValue < min) e.ProposedValue = min;
if ((int)e.ProposedValue > max) e.ProposedValue = max;
}
}
I can recommend you to forget about data tables and use classes. You can use data annotation to validate your model.
Use this attribute to validate a range of values for an specific property/
This code is extracted from the specified article(example of class making the validation of a range):
public class Product
{
[Range(5, 50)]
public int ReorderLevel { get; set; }
[Range(typeof(Decimal),"5", "5000")]
public decimal ListPrice { get; set; }
}
Your are going to find a lot of benefits of using classes.
This is an old post but I am using a solution to sync the Check_Constraints NOT filled by the OleDbDataAdapter.FillSchema on Access Databases worth to mention. Just used a OleDbConnection to retrieve GetOleDbSchemaTable and foreach() the rows extracting the validation text expression and created an anonymous delegate at the appropriate Table & Column attached to the proper Table.ColumnChanging event. The string validation provided by the Access schema will then be evaluated dynamically by the handy Eval() function described here. There is my code:
DataTable schemaTable = connection.GetOleDbSchemaTable(OleDbSchemaGuid.Check_Constraints, null);
// Attach delegate Eval() of each Check_Constraints on proper Table/Column
foreach (DataRow myField in schemaTable.Rows)
{
string constraint_name = "";
string check_clause = "";
foreach (DataColumn myProperty in schemaTable.Columns)
{
if (myProperty.ColumnName == "CONSTRAINT_NAME")
constraint_name = myField[myProperty.ColumnName].ToString();
if (myProperty.ColumnName == "CHECK_CLAUSE")
check_clause = myField[myProperty.ColumnName].ToString();
}
var rule = constraint_name.Replace("[", "").Replace("]", "").Split('.');
if (rule.Length == 3 && dataset.Tables.Contains(rule[0]) && dataset.Tables[rule[0]].Columns.Contains(rule[1]) && String.IsNullOrEmpty(check_clause) == false)
{
dataset.Tables[rule[0]].ColumnChanging += delegate (object sender, DataColumnChangeEventArgs e)
{
if (e.Column.ColumnName == rule[1] && Convert.ToBoolean(ToolBox.Eval(e.ProposedValue + check_clause)) == false)
{
throw new Exception("Tabela: " + rule[0] + ", coluna: " + rule[0] + ", cheque: " + check_clause);
}
};
Debug.WriteLine(rule[0] + "." + rule[1] + ": " + check_clause);
}
}
I have a datagridview on my form and I populate it with this:
dataGridView1.DataSource = students.Select(s => new { ID = s.StudentId, RUDE = s.RUDE, Nombre = s.Name, Apellidos = s.LastNameFather + " " + s.LastNameMother, Nacido = s.DateOfBirth })
.OrderBy(s => s.Apellidos)
.ToList();
Now, I use the s.Apellidos as the default sort, but I'd also like to allow users to sort when clicking on the column header.
This sort will not modify the data in any way, it's just a client side bonus to allow for easier searching for information when scanning the screen with their eyes.
Thanks for the suggestions.
Set all the column's (which can be sortable by users) SortMode property to Automatic
dataGridView1.DataSource = students.Select(s => new { ID = s.StudentId, RUDE = s.RUDE, Nombre = s.Name, Apellidos = s.LastNameFather + " " + s.LastNameMother, Nacido = s.DateOfBirth })
.OrderBy(s => s.Apellidos)
.ToList();
foreach(DataGridViewColumn column in dataGridView1.Columns)
{
column.SortMode = DataGridViewColumnSortMode.Automatic;
}
Edit: As your datagridview is bound with a linq query, it will not be sorted. So please go through this [404 dead link, see next section] which explains how to create a sortable binding list and to then feed it as datasource to datagridview.
Code as recovered from dead link
Link from above is 404-dead. I recovered the code from the Internet Wayback Machine archive of the page.
public Form1()
{
InitializeComponent();
SortableBindingList<person> persons = new SortableBindingList<person>();
persons.Add(new Person(1, "timvw", new DateTime(1980, 04, 30)));
persons.Add(new Person(2, "John Doe", DateTime.Now));
this.dataGridView1.AutoGenerateColumns = false;
this.ColumnId.DataPropertyName = "Id";
this.ColumnName.DataPropertyName = "Name";
this.ColumnBirthday.DataPropertyName = "Birthday";
this.dataGridView1.DataSource = persons;
}
As Niraj suggested, use a SortableBindingList. I've used this very successfully with the DataGridView.
Here's a link to the updated code I used - Presenting the SortableBindingList - Take Two - archive
Just add the two source files to your project, and you'll be in business.
Source is in SortableBindingList.zip - 404 dead link
One more way to do this is using "System.Linq.Dynamic" library. You can get this library from Nuget. No need of any custom implementations or sortable List :)
using System.Linq.Dynamic;
private bool sortAscending = false;
private void dataGridView_ColumnHeaderMouseClick ( object sender, DataGridViewCellMouseEventArgs e )
{
if ( sortAscending )
dataGridView.DataSource = list.OrderBy ( dataGridView.Columns [ e.ColumnIndex ].DataPropertyName ).ToList ( );
else
dataGridView.DataSource = list.OrderBy ( dataGridView.Columns [ e.ColumnIndex ].DataPropertyName ).Reverse ( ).ToList ( );
sortAscending = !sortAscending;
}
You don't need to create a binding datasource. If you want to apply sorting for all of your columns, here is a more generic solution of mine;
private int _previousIndex;
private bool _sortDirection;
private void gridView_ColumnHeaderMouseClick(object sender, DataGridViewCellMouseEventArgs e)
{
if (e.ColumnIndex == _previousIndex)
_sortDirection ^= true; // toggle direction
gridView.DataSource = SortData(
(List<MainGridViewModel>)gridReview.DataSource, gridReview.Columns[e.ColumnIndex].Name, _sortDirection);
_previousIndex = e.ColumnIndex;
}
public List<MainGridViewModel> SortData(List<MainGridViewModel> list, string column, bool ascending)
{
return ascending ?
list.OrderBy(_ => _.GetType().GetProperty(column).GetValue(_)).ToList() :
list.OrderByDescending(_ => _.GetType().GetProperty(column).GetValue(_)).ToList();
}
Make sure you subscribe your data grid to the event ColumnHeaderMouseClick. When the user clicks on the column it will sort by descending. If the same column header is clicked again, sorting will be applied by ascending.
your data grid needs to be bound to a sortable list in the first place.
Create this event handler:
void MakeColumnsSortable_DataBindingComplete(object sender, DataGridViewBindingCompleteEventArgs e)
{
//Add this as an event on DataBindingComplete
DataGridView dataGridView = sender as DataGridView;
if (dataGridView == null)
{
var ex = new InvalidOperationException("This event is for a DataGridView type senders only.");
ex.Data.Add("Sender type", sender.GetType().Name);
throw ex;
}
foreach (DataGridViewColumn column in dataGridView.Columns)
column.SortMode = DataGridViewColumnSortMode.Automatic;
}
And initialize the event of each of your datragrids like this:
dataGridView1.DataBindingComplete += MakeColumnsSortable_DataBindingComplete;
You can use DataGridViewColoumnHeaderMouseClick event like this :
Private string order = String.Empty;
private void dgvDepartment_ColumnHeaderMouseClick(object sender, DataGridViewCellMouseEventArgs e)
{
if (order == "d")
{
order = "a";
dataGridView1.DataSource = students.Select(s => new { ID = s.StudentId, RUDE = s.RUDE, Nombre = s.Name, Apellidos = s.LastNameFather + " " + s.LastNameMother, Nacido = s.DateOfBirth }) .OrderBy(s => s.Apellidos).ToList();
}
else
{
order = "d";
dataGridView1.DataSource = students.Select(s => new { ID = s.StudentId, RUDE = s.RUDE, Nombre = s.Name, Apellidos = s.LastNameFather + " " + s.LastNameMother, Nacido = s.DateOfBirth }.OrderByDescending(s => s.Apellidos) .ToList()
}
}
there is quite simply solution when using Entity Framework (version 6 in this case). I'm not sure but it seems to ObservableCollectionExtensions.ToBindingList<T> method returns implementation of sortable binding list. I haven't found source code to confirm this supposition but object returning from this method works with DataGridView very well especially when sorting columns by clicking on its headers.
The code is very simply and relies only on .net and entity framework classes:
using System.Data.Entity;
IEnumerable<Item> items = MethodCreatingItems();
var observableItems = new System.Collections.ObjectModel.ObservableCollection<Item>(items);
System.ComponentModel.BindingList<Item> source = observableItems.ToBindingList();
MyDataGridView.DataSource = source;
put this line in your windows form (on load or better in a public method like "binddata" ):
//
// bind the data and make the grid sortable
//
this.datagridview1.MakeSortable( myenumerablecollection );
Put this code in a file called DataGridViewExtensions.cs (or similar)
// MakeSortable extension.
// this will make any enumerable collection sortable on a datagrid view.
//
// BEGIN MAKESORTABLE - Mark A. Lloyd
//
// Enables sort on all cols of a DatagridView
//
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
public static class DataGridViewExtensions
{
public static void MakeSortable<T>(
this DataGridView dataGridView,
IEnumerable<T> dataSource,
SortOrder defaultSort = SortOrder.Ascending,
SortOrder initialSort = SortOrder.None)
{
var sortProviderDictionary = new Dictionary<int, Func<SortOrder, IEnumerable<T>>>();
var previousSortOrderDictionary = new Dictionary<int, SortOrder>();
var itemType = typeof(T);
dataGridView.DataSource = dataSource;
foreach (DataGridViewColumn c in dataGridView.Columns)
{
object Provider(T info) => itemType.GetProperty(c.Name)?.GetValue(info);
sortProviderDictionary[c.Index] = so => so != defaultSort ?
dataSource.OrderByDescending<T, object>(Provider) :
dataSource.OrderBy<T,object>(Provider);
previousSortOrderDictionary[c.Index] = initialSort;
}
async Task DoSort(int index)
{
switch (previousSortOrderDictionary[index])
{
case SortOrder.Ascending:
previousSortOrderDictionary[index] = SortOrder.Descending;
break;
case SortOrder.None:
case SortOrder.Descending:
previousSortOrderDictionary[index] = SortOrder.Ascending;
break;
default:
throw new ArgumentOutOfRangeException();
}
IEnumerable<T> sorted = null;
dataGridView.Cursor = Cursors.WaitCursor;
dataGridView.Enabled = false;
await Task.Run(() => sorted = sortProviderDictionary[index](previousSortOrderDictionary[index]).ToList());
dataGridView.DataSource = sorted;
dataGridView.Enabled = true;
dataGridView.Cursor = Cursors.Default;
}
dataGridView.ColumnHeaderMouseClick+= (object sender, DataGridViewCellMouseEventArgs e) => DoSort(index: e.ColumnIndex);
}
}
KISS : Keep it simple, stupid
Way A:
Implement an own SortableBindingList class when like to use DataBinding and sorting.
Way B:
Use a List<string> sorting works also but does not work with DataBinding.
I suggest using a DataTable.DefaultView as a DataSource. Then the line below.
foreach (DataGridViewColumn column in gridview.Columns)
{
column.SortMode = DataGridViewColumnSortMode.Automatic;
}
After that the gridview itself will manage sorting(Ascending or Descending is supported.)
If you get an error message like
An unhandled exception of type 'System.NullReferenceException'
occurred in System.Windows.Forms.dll
if you work with SortableBindingList, your code probably uses some loops over DataGridView rows and also try to access the empty last row! (BindingSource = null)
If you don't need to allow the user to add new rows directly in the DataGridView this line of code easily solve the issue:
InitializeComponent();
m_dataGridView.AllowUserToAddRows = false; // after components initialized
...
Create a class which contains all properties you need, and populate them in the constructor
class Student
{
int _StudentId;
public int StudentId {get;}
string _Name;
public string Name {get;}
...
public Student(int studentId, string name ...)
{ _StudentId = studentId; _Name = name; ... }
}
Create an IComparer < Student > class, to be able to sort
class StudentSorter : IComparer<Student>
{
public enum SField {StudentId, Name ... }
SField _sField; SortOrder _sortOrder;
public StudentSorder(SField field, SortOrder order)
{ _sField = field; _sortOrder = order;}
public int Compare(Student x, Student y)
{
if (_SortOrder == SortOrder.Descending)
{
Student tmp = x;
x = y;
y = tmp;
}
if (x == null || y == null)
return 0;
int result = 0;
switch (_sField)
{
case SField.StudentId:
result = x.StudentId.CompareTo(y.StudentId);
break;
case SField.Name:
result = x.Name.CompareTo(y.Name);
break;
...
}
return result;
}
}
Within the form containing the datagrid add
ListDictionary sortOrderLD = new ListDictionary(); //if less than 10 columns
private SortOrder SetOrderDirection(string column)
{
if (sortOrderLD.Contains(column))
{
sortOrderLD[column] = (SortOrder)sortOrderLD[column] == SortOrder.Ascending ? SortOrder.Descending : SortOrder.Ascending;
}
else
{
sortOrderLD.Add(column, SortOrder.Ascending);
}
return (SortOrder)sortOrderLD[column];
}
Within datagridview_ColumnHeaderMouseClick event handler do something like this
private void dgv_ColumnHeaderMouseClick(object sender, DataGridViewCellMouseEventArgs e)
{
StudentSorter sorter = null;
string column = dGV.Columns[e.ColumnIndex].DataPropertyName; //Use column name if you set it
if (column == "StudentId")
{
sorter = new StudentSorter(StudentSorter.SField.StudentId, SetOrderDirection(column));
}
else if (column == "Name")
{
sorter = new StudentSorter(StudentSorter.SField.Name, SetOrderDirection(column));
}
...
List<Student> lstFD = datagridview.DataSource as List<Student>;
lstFD.Sort(sorter);
datagridview.DataSource = lstFD;
datagridview.Refresh();
}
Hope this helps
Just in case somebody still looks for it, I did it on VS 2008 C#.
On the Event ColumnHeaderMouseClick, add a databinding for the gridview, and send the order by field like a parameter. You can get the clicked field as follows:
dgView.Columns[e.ColumnIndex].Name
In my case the header's names are similar to view field names.
I have a BindingList<> object bind as a data source to dataGridView.
BindingList x1;
x1 = new BindingList<sourceObject>();
BindingSource bsx1 = new BindingSource();
bsx1.DataSource = x1;
dataGridView1.DataSource = bsx1;
When I clicked the column header, no sorting takes place.
I used the SortableBindingList answer provided by Tom Bushell.
Having included two source files into my project
SortableBindingList.cs
PropertyComparer.cs
Then this change is made to my code:
Be.Timvw.Framework.ComponentModel.SortableBindingList x1; // 1
x1 = new Be.Timvw.Framework.ComponentModel.SortableBindingList<sourceObject>(); // 2
BindingSource bsx1 = new BindingSource();
bsx1.DataSource = x1;
dataGridView1.DataSource = bsx1;
After these changes I performed a build on my program. I am now able to sort by clicking the column headers. Only two lines need changing, they are highlighted in the code snippet above by trailing comments.
In my case, the problem was that I had set my DataSource as an object, which is why it didn't get sorted. After changing from object to a DataTable it workd well without any code complement.
If using a DataTable: dgv.DataSource = (DataTable)table
You can automatically enable Sorting for objects that contain the IComparable Interface. After creating the DataTable, when adding the columns be sure to set the type also to at least object:
table.Columns.Add("ColumnName", typeof(object))
Otherwise, if you do Not specifically give it a type, it converts the object to a string.
I spent a fair amount of time creating a dgv_ColumnHeaderMouseClick() event because it was Not sorting the DataGridView correctly, then to find that all you need to do is specify the type for the column name, and it sorts properly. And the reason it was not sorting correctly previously was because without specifying the type for DataTable columns, it will convert objects to strings.
Just instead of passing a list to the datagrid, you store the search result as a datatable.
dataGridView1.DataSource = students
.Select(s => new {
ID = s.StudentId,
RUDE = s.RUDE,
Nombre = s.Name,
Apellidos = s.LastNameFather + " " + s.LastNameMother,
Nacido = s.DateOfBirth })
.OrderBy(s => s.Apellidos)
.ToDataTable();