Constraint on DataColumn in C# DataTable? - c#

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

Related

Datagrid filter on column that between two age range

I have a datagridview with a column of type string that display values for age ranges such as:
0-18
19-100
0-100
I also have a filter textbox that would need to filter on the age range
(dgv1.DataSource as DataTable).DefaultView.RowFilter =
string.Format("AgeRange LIKE '%{0}' OR AgeRange LIKE '{0}%'", textBoxFilter.Text);
The problem is that if the user enter a number like 18, the grid does not return row for 0-100
How can I get the datagrid to return both 0-18 and 0-100?
I do not think you will be able to do this using the “LIKE” comparator since the values you are looking for are “numeric”. To get the filter you are looking for, you will need a filter with “>=” and “<=” to see if the target age is in the range. It is unclear how the data is originally received, if the “age range” in each row is a string as shown, then I suggest a couple of different hacky ways. In addition, it is unclear what other columns would be in the grid.
One “hacky” approach, would be to make a method that returns a new DataTable with only the rows that fall into the given target range. To help in this endeavor, a second method that takes an int (target value we are looking for), and a DataRowView (The AgeRange we are comparing the “target” value to). This “AgeRange” will be in the rows first column. Here we simply take that string range (“0-18”) and the target value (“18”) to see if this target value IS in the range, then return true or false depending on the result. This can be done using the string.split method to split the “AgeRange” string and int.TryParse to convert the strings into numbers. Below is an example of this.
private bool TargetIsInRange(int target, DataRowView row) {
if (row.Row.ItemArray[0] != null) {
string cellValue = row.Row.ItemArray[0].ToString();
string[] splitArray = cellValue.Split('-');
int startValue;
int endValue;
if (!int.TryParse(splitArray[0], out startValue)) {
return false;
}
if (!int.TryParse(splitArray[1], out endValue)) {
return false;
}
if (target >= startValue && target <= endValue)
return true;
}
return false;
}
The method above should come in handy when looping through the grids rows to figure out which rows go into the new filter DataTable. Next, a method that does this looping through the grid and returns a filtered DataTable. For each row in the grid, we could call the above method and add the rows that return true.
private DataTable GetFilterTable() {
DataTable filterTable = ((DataTable)dgv1.DataSource).Clone();
dgv1.DataSource = gridTable;
int targetValue = -1;
if (int.TryParse(textBox1.Text, out targetValue)) {
foreach (DataGridViewRow row in dgv1.Rows) {
DataRowView dataRow = (DataRowView)row.DataBoundItem;
if (dataRow != null) {
if (TargetIsInRange(targetValue, dataRow)) {
filterTable.Rows.Add(dataRow.Row.ItemArray[0]);
}
}
}
}
return filterTable;
}
It is unclear where you are calling this filter, if you are filtering “strings” then as the user types the filter string in the text box the grid will filter with each character pressed by the user. This is nice with strings, however, in this case using “numbers”, I am guessing a button would be more appropriate. I guess this is something that you will have to decide. Putting all this together using a Button click event to signal when to filter the grid may look something like below
private DataTable gridTable;
public Form1() {
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e) {
gridTable = GetTable();
FillTable(gridTable);
dgv1.DataSource = gridTable;
textBox1.Text = "18";
}
private void FillTable(DataTable dt) {
dt.Rows.Add("0-18");
dt.Rows.Add("19-100");
dt.Rows.Add("0-100");
dt.Rows.Add("17-80");
dt.Rows.Add("18-80");
}
private DataTable GetTable() {
DataTable dt = new DataTable();
dt.Columns.Add("AgeRange", typeof(string));
return dt;
}
private void button1_Click(object sender, EventArgs e) {
if (textBox1.Text == "") {
dgv1.DataSource = gridTable;
return;
}
dgv1.DataSource = GetFilterTable();
}
Hacky Approach 2
The first approach works; however, I am guessing if there is a LOT of data and a LOT of filtering, this may become a performance issue. Therefore, in this approach, extra steps are taken in the beginning to take advantage of the DataTables RowFilter feature, as the posted code is doing. Obviously as stated previously, we will not use the “LIKE” comparator, instead the “<=” and “>=” operators are used.
In order to accomplish this, we MUST somehow turn the given string range “XX-XX” into two (2) ints. Then “add” these integers to the DataTable. Then it will be easy to filter the table using the RowFilter property and the less than and greater than operators. One problem is that it will require “extra” work on our part to set up the grid’s columns properly or these extra two columns of data will also display.
This can be done in the “designer” or manually in code. Without going into too much detail, it is useful to bear in mind that IF you assign a DataTable as a data source to the grid AND you set the grids AutoGenerateColumns property to false… THEN only the grid columns with DataPropertyName names that “match” one of the DataTable column names… will display. In this case, we only want the AgeRange column with the “XX-XX” strings to display, the other two new columns can remain hidden from the user. Setting up the grid column manually may look something like below, however you can do this in the designer. NOTE: the designer does not display an AutoGenerateColumns property, you have to do this in your code.
private void AddGridColumn() {
DataGridViewTextBoxColumn col = new DataGridViewTextBoxColumn();
col.Name = "AgeRange";
col.DataPropertyName = "AgeRange";
col.HeaderText = "Age Range";
dgv1.Columns.Add(col);
}
The important point is that the DataPropertyName MUST match the target column name in the DataTable, otherwise the column will not display.
Next is the construction of the new DataTable. This method is given the original DataTable. A new DataTable is created with three (3) columns, AgeRange-string (displayed), StartRange-int and EndRange-int. The start and end columns will not be displayed. Once this new table is constructed, a foreach loop is started through all the rows in the original table. The string digits from the original tables row are “parsed” into actual numbers and added to the new DataTable along with the original “range” string. This method could look something like below. A helper method is further below to help split the age range string and return a number.
private DataTable GetSplitTable(DataTable sourceTable) {
DataTable dt = new DataTable();
dt.Columns.Add("AgeRange", typeof(string));
dt.Columns.Add("StartRange", typeof(int));
dt.Columns.Add("EndRange", typeof(int));
foreach (DataRow row in sourceTable.Rows) {
int startValue = GetIntValue(row.ItemArray[0].ToString(), 0);
int endValue = GetIntValue(row.ItemArray[0].ToString(), 1);
dt.Rows.Add(row.ItemArray[0], startValue, endValue);
}
return dt;
}
private int GetIntValue(string rangeString, int index) {
string[] splitArray = rangeString.Split('-');
int value = 0;
int.TryParse(splitArray[index], out value);
return value;
}
Putting all this together may look like below. Note, the button click event checks to see if the text box is empty, and if it is, will remove the current filter if one is applied.
private DataTable gridTable;
private DataTable splitTable;
public Form1() {
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e) {
gridTable = GetTable();
FillTable(gridTable);
splitTable = GetSplitTable(gridTable);
AddGridColumn();
dgv1.AutoGenerateColumns = false;
dgv1.DataSource = splitTable;
textBox1.Text = "18";
}
private void FillTable(DataTable dt) {
dt.Rows.Add("0-18");
dt.Rows.Add("19-100");
dt.Rows.Add("0-100");
dt.Rows.Add("17-80");
dt.Rows.Add("15-75");
}
private DataTable GetTable() {
DataTable dt = new DataTable();
dt.Columns.Add("AgeRange", typeof(string));
return dt;
}
private void AddGridColumn() {
DataGridViewTextBoxColumn col = new DataGridViewTextBoxColumn();
col.Name = "AgeRange";
col.DataPropertyName = "AgeRange";
col.HeaderText = "Age Range";
dgv1.Columns.Add(col);
}
private void button1_Click(object sender, EventArgs e) {
string filterString = "";
DataView dv;
if (textBox1.Text != "") {
filterString = string.Format("StartRange <= {0} AND EndRange >= {0}", textBox1.Text);
}
dv = new DataView(splitTable);
dv.RowFilter = filterString;
dgv1.DataSource = dv;
}
This Code:
("AgeRange LIKE '%{0}' OR AgeRange LIKE '{0}%'", textBoxFilter.Text)
is a redundant with two AgeRange LIKE
If you want to search like textBoxFilter.Text you can try
("AgeRange LIKE '%{0}%'", textBoxFilter.Text)
Or
StringBuilder rowFilter = new StringBuilder();
rowFilter.Append("AgeRange Like '%" + textBoxFilter.Text + "%'");
(dgv1.DataSource as DataTable).DefaultView.RowFilter = rowFilter.ToString();

Compare two DataRows excluding some columns

I have two DataTables, e.g. OriginalEntity and Entity.
Interface application modifies Entity dataTable's rows. While saving I want to check DataRows that is modified or different from OrigianlEntity.
But, also I need to exclude few fields while comparing e.g. modified date and other audit fields.
Currently I am looping through each rows of datatable, like this:
List<string> auditFields = new List<string>(){"createdon","modifiedon"};
string IdentityKeyName = "id";
object ent,orgEnt;
foreach(string columnName in columnList) // ColumnList is List of columns available in datatable
{
foreach(DataRow dr in Entity.Rows)
{
ent = dr[columnName];
orgEnt = OriginalEntity.Select(IdentityKeyName + " = " + dr[IdentityKeyName].ToString())[0][columnName];
if(!ent.Equals(orgEnt) && !auditFields.Contains(columnName))
{
isModified = true;
break;
}
}
}
I just want an efficient way to achieve above. Please suggest.
Thanks everyone for your suggestion, and this is my (as I don't have primary key defined)
Solution:
public bool isModified(DataTable dt1, DataTable dt2, string IdentityKeyName)
{
bool isModified = false;
List<string> auditFields = new List<string>() { "createdon", "modifiedon" };
isModified = isModified || (dt1.Rows.Count != dt2.Rows.Count);
if(!isModified)
{
//Approach takes 150 ms to compare two datatable of 10000 rows and 24 columns each
DataTable copyOriginalEntity = dt1.Copy();
DataTable copyEntity = dt2.Copy();
//Exclude field you don't want in your comparison -- It was my main task
foreach(string column in auditFields)
{
copyOriginalEntity.Columns.Remove(column);
copyEntity.Columns.Remove(column);
}
for(int i=0;i<copyOriginalEntity.Rows.Count;i++)
{
var origItems = copyOriginalEntity.Rows[i].ItemArray;
var entityItem = copyEntity.Select(IdentityKeyName + " = " + copyOriginalEntity.Rows[i][dentityKeyName].ToString())[0].ItemArray;
if(string.Concat(origItems) != string.Concat(entityItem)){ isModified = true; break; }
}
}
return isModified;
}
You are going to have to loop though the columns to compare. This compare ent.Equals(orgEnt) in your code is comparing if the object references are the same. This doesn't seem like what you want and you want to compare values.
public bool IsChanged(DataTable original, DataTable source, string idKeyName, params string[] ignoreColumns)
{
// make sure "key" column exist in both
if (!original.Columns.Contains(idKeyName) || !source.Columns.Contains(idKeyName))
{
throw new MissingPrimaryKeyException("Primary key column not found.");
}
// if source rows are not the same as original then something was deleted or added
if (source.Rows.Count != original.Rows.Count)
{
return false;
}
// Get a list of columns ignoring passed in and key (key will have to be equal to find)
var originalColumns =
original.Columns.Cast<DataColumn>()
.Select(c => c.ColumnName)
.Where(n => !ignoreColumns.Contains(n) && n != idKeyName)
.ToArray();
// check to make sure same column count otherwise just fail no need to check
var sourceColumnsCount =
source.Columns.Cast<DataColumn>()
.Select(c => c.ColumnName).Count(originalColumns.Contains);
if (originalColumns.Length != sourceColumnsCount)
{
return false;
}
//Switch to linq
var sourceRows = source.AsEnumerable();
return sourceRows.All(sourceRow =>
{
// use select since not real key
var originalCheck = original.Select(idKeyName + " = " + sourceRow[idKeyName]);
if (originalCheck.Length != 1)
{
// Couldn't find key or multiple matches
return false;
}
var originalRow = originalCheck.First();
//Since using same array we can use linq's SequenceEqual to compare for us
return
originalColumns.Select(oc => sourceRow[oc])
.SequenceEqual(originalColumns.Select(oc => originalRow[oc]));
});
}
There might be some micro optimizations but I think no matter what you will have to check each column.

filtered Query for objectContext Using Linq to SQL

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;
}
}
}

How to convert datatable to dictionary in ASP.NET/C#

The DataTable below:
ClassID ClassName StudentID StudentName
1 A 1000 student666
2 B 1100 student111
5 C 1500 student777
1 A 1200 student222
2 B 1080 student999
The dictionary key is composed of "ClassID ,ClassName " and value is composed of "StudentID,StudentName" .
Dictionary<string, string> d = new Dictionary<string, string>();
foreach (DataRow dr in table.Rows)
{
string key=dr["ClassID"].ToString() + dr["ClassName"].ToString();
if (!d.ContainsKey(key))
{
//Do something();......
}
else
{
//Do something();......
}
}
foreach (var s in d.Keys)
{
Response.Write(s+"|+"+d[s]+"<br>");
}
Is there a faster way?
assume that key is '1,A' ,Value should be ' 1000,student666' and '1200,student222'
Here goes then. Using Linq, you can group them then perform string concatenation if you want.
// Start by grouping
var groups = table.AsEnumerable()
.Select(r => new {
ClassID = r.Field<int>("ClassID"),
ClassName = r.Field<string>("ClassName"),
StudentID = r.Field<int>("StudentID"),
StudentName = r.Field<string>("StudentName")
}).GroupBy(e => new { e.ClassID, e.ClassName });
// Then create the strings. The groups will be an IGrouping<TGroup, T> of anonymous objects but
// intellisense will help you with that.
foreach(var line in groups.Select(g => String.Format("{0},{1}|+{2}<br/>",
g.Key.ClassID,
g.Key.ClassName,
String.Join(" and ", g.Select(e => String.Format("{0},{1}", e.StudentID, e.StudentName))))))
{
Response.Write(line);
}
Try this:
Dictionary<string, string> d = new Dictionary<string, string>();
foreach (DataRow dr in table.Rows)
{
string key=dr["ClassID"].ToString() + "-" + dr["ClassName"].ToString();
string value=dr["StudentID"].ToString() + "-" + dr["StudentName"].ToString();
if (!d.ContainsKey(key))
{
d.Add(key, value);
}
}
Reference
Dictionary.Add Method
OR ELSE Try Onkelborg's Answer
How to use compound key for dictionary?
The tricky thing here is the composite key (ClassID, ClassName). Once you identify that, it's easy to search this site for the solution.
I'd recommend using tuples as pointed out here: Composite Key Dictionary
The easiest way is to use a string value of ClassID|ClassName as key. For example, use string value "1|A" for key for the first row, and string value "2|B" for key for the second row, etc.
Here is something that can give you an idea:
using System;
using System.Data;
using System.Collections.Generic;
namespace SO17416111
{
class Class
{
public int Id { get; set; }
public string Name { get; set; }
}
// Note that definition of Class and Student only differ by name
// I'm assuming that Student can/will be expanded latter.
// Otherwise it's possible to use a single class definition
class Student
{
public int Id { get; set; }
public string Name { get; set; }
}
class Program
{
static void Main()
{
DataTable table = GetData();
Dictionary<Class, List<Student>> d = new Dictionary<Class, List<Student>>();
foreach (DataRow dr in table.Rows)
{
// If it's possible to get null data from the DB the appropriate null checks
// should also be performed here
// Also depending on actual data types in your DB the code should be adjusted as appropriate
Class key = new Class {Id = (int) dr["ClassID"], Name = (string) dr["ClassName"]};
Student value = new Student { Id = (int)dr["StudentID"], Name = (string)dr["StudentName"] };
if (!d.ContainsKey(key))
{
d.Add(key, new List<Student>());
}
d[key].Add(value);
}
foreach (var s in d.Keys)
{
foreach (var l in d[s])
{
Console.Write(s.Id + "-" + s.Name + "-" + l.Id + "-" + l.Name + "\n");
}
}
}
// You don't need this just use your datatable whereever you obtain it from
private static DataTable GetData()
{
DataTable table = new DataTable();
table.Columns.Add("ClassID", typeof (int));
table.Columns.Add("ClassName", typeof (string));
table.Columns.Add("StudentID", typeof (int));
table.Columns.Add("StudentName", typeof (string));
table.Rows.Add(1, "A", 1000, "student666");
table.Rows.Add(2, "B", 1100, "student111");
table.Rows.Add(5, "C", 1500, "student777");
table.Rows.Add(1, "A", 1200, "student222");
table.Rows.Add(2, "B", 1080, "student999");
return table;
}
}
}
Note, that this can be compiled and tested as a console application - I substituted your Response.Write with Console.Write. I'm also generating a test DataTable, you should be able to use one that is already present in your application. As far as Class/Student classes go, you have several options here: you can have two separate classes as I show, you can use the same class or you can even use a Tuple class. I suggest you use two separate classes, as it improves readability and maintainability.
Note if you just need to output them, you don't need a dictionary or anything to that effect:
// Add null checks and type conversions as appropriate
foreach (DataRow dr in table.Rows)
{
Response.Write(dr["ClassID"] + "-" + dr["ClassName"] + "-" + dr["StudentID"] + "-" + dr["StudentName"] + "<br>");
}

How to enable DataGridView sorting when user clicks on the column header?

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

Categories

Resources