So I have this datagridview that is linked to a Binding source that is binding to an underlying data table. The problem is I need to manual add rows to the datagridview.
This cannot be done while it is bound, so I have to work with the databinding.
If I add the rows to the underlying datatable, when the datatable is saved, the rows are duplicated, probably because the binding source somehow got a hold of a copy and inserted it also.
Adding it to the binding source is what I've been trying to do but it's not quite working.
Let me explain exactly what my setup is:
I have a database with two tables:
CashReceiptTable and CashReceiptItemsTable
CashReceiptItemsTable contains a FK to CashReceiptTable.
The form allows the users to add, and modify the two tables.
When the user enters a new cashreceipt, the cash receipt's id is -1, and the FK in cashReceiptitemstable is -1. When the database is saved, cashReceipt's id is updated, and I have to manually update cashreceiptitem's FK.
Here are the problems:
When I try to update the CashReceiptID (the FK) in more than one row in cashreceiteitems binding source, the first row is updated, and disappears (because it's filtered), and the other rows are removed, and I can no longer access them.
I have no idea why this is, I haven't updated the filter yet so they should still be there, but trying to access them throws RowNotInTableException.
I've managed a work around that copies the rows in the the binding source to an in memory array, deletes the first row in the binding source (all the other rows just vanish), update the row's FK and reinsert them into the binding source and save the table.
This works okay, but why do the rows disappear?
I also have one more slight problem. When the CashReceiptsTable is empty and I am adding a new row to it, if I add more than one row to the CashReceiptsItemTable it causes problems. When manually adding the items to the binding source, adding a new row pops to previous row off and pushes it onto the datatable. This hides it from my FK updating routine and it is lost, it also removes it from the DataGridView.
It only does that when I'm adding the first row to CashReceiptsTable. Why does it do this, and how can I fix it?
I'm posting my code that autopopulates it here:
private void autopopulate(decimal totalPayment) {
//remove old rows
for (int i = 0; i < tblCashReceiptsApplyToBindingSource.List.Count; i++) {
DataRowView viewRow = tblCashReceiptsApplyToBindingSource.List[i] as DataRowView;
RentalEaseDataSet.tblCashReceiptsApplyToRow row = viewRow.Row as RentalEaseDataSet.tblCashReceiptsApplyToRow;
if (row.CashReceiptsID == this.ReceiptID) {
tblCashReceiptsApplyToBindingSource.List.Remove(viewRow);
i--;
}
}
decimal payment = totalPayment;
//look for an exact amount
foreach (DataGridViewRow dueRow in dataViewDueRO.Rows) {
decimal due = -1 * (Decimal)dueRow.Cells[Due.Index].Value;
if (due == payment) {
String charge = (String)dueRow.Cells[Description.Index].Value;
int chargeID = ManageCheckbooks.findTransactionID(charge);
tblCashReceiptsApplyToBindingSource.AddNew();
RentalEaseDataSet.tblCashReceiptsApplyToRow row = ((DataRowView)tblCashReceiptsApplyToBindingSource.Current).Row as RentalEaseDataSet.tblCashReceiptsApplyToRow;
row.CashReceiptsID = this.ReceiptID;
row.ApplyTo = chargeID;
row.Paid = payment; //convert to positive
payment = 0;
break;
}
}
//if the exact amount was found, payment will = 0, and this will do nothing, otherwise,
//divy out everything left over (which will be everything)
foreach (DataGridViewRow dueRow in dataViewDueRO.Rows) {
String charge = (String)dueRow.Cells[Description.Index].Value;
decimal due = (Decimal)dueRow.Cells[Due.Index].Value;
if (due > 0 || payment <= 0) {
continue;
}
int chargeID = ManageCheckbooks.findTransactionID(charge);
payment += due; //due is negative, so this will subtract how much the user owes
tblCashReceiptsApplyToBindingSource.AddNew();
RentalEaseDataSet.tblCashReceiptsApplyToRow row = ((DataRowView)tblCashReceiptsApplyToBindingSource.Current).Row as RentalEaseDataSet.tblCashReceiptsApplyToRow;
row.CashReceiptsID = this.ReceiptID;
row.ApplyTo = chargeID;
if (payment >= 0) {
//payment is enough to cover this
row.Paid = due * -1; //convert to positive
} else {
//doesn't have enough money to conver this, can only cover partial, or none
row.Paid = (due - payment) * -1; //math:
//money remaining $50, current charge = $60
//payment = 50 + -60 = -10
//row["Paid"] = (-60 - -10) * -1
//row["Paid"] = (-60 + 10) * -1
//row["Paid"] = -50 * -1
//row["Paid"] = 50
}
if (payment <= 0) {
break; //don't conintue, no more money to distribute
}
}
isVirginRow = true;
}
And this is the function that saves it to the database:
protected override void saveToDatabase() {
tblCashReceiptsBindingSource.EndEdit();
isVirginRow = false;
RentalEaseDataSet.tblCashReceiptsRow[] rows = rentalEaseDataSet.tblCashReceipts.Select("ID < 0") as RentalEaseDataSet.tblCashReceiptsRow[];
int newID = -1;
if (rows.Count() > 0) {
tblCashReceiptsTableAdapter.Update(rows[0]);
newID = rows[0].ID;
}
tblCashReceiptsTableAdapter.Update(rentalEaseDataSet.tblCashReceipts);
//update table
/*foreach (RentalEaseDataSet.tblCashReceiptsApplyToRow row in rentalEaseDataSet.tblCashReceiptsApplyTo.Select("CashReceiptsID = -1")) {
row.CashReceiptsID = newID;
}*/
//update binding source
DataRowView[] applicationsOld = new DataRowView[tblCashReceiptsApplyToBindingSource.List.Count];
RentalEaseDataSet.tblCashReceiptsApplyToRow[] applicationsNew = new RentalEaseDataSet.tblCashReceiptsApplyToRow[tblCashReceiptsApplyToBindingSource.List.Count];
tblCashReceiptsApplyToBindingSource.List.CopyTo(applicationsOld, 0);
for (int i = 0; i < applicationsOld.Count(); i++) {
RentalEaseDataSet.tblCashReceiptsApplyToRow row = applicationsOld[i].Row as RentalEaseDataSet.tblCashReceiptsApplyToRow;
if (row.CashReceiptsID < 0) {
applicationsNew[i] = rentalEaseDataSet.tblCashReceiptsApplyTo.NewRow() as RentalEaseDataSet.tblCashReceiptsApplyToRow;
applicationsNew[i]["ID"] = row.ID;
applicationsNew[i]["CashReceiptsID"] = this.ReceiptID;
applicationsNew[i][2] = row[2];
applicationsNew[i][3] = row[3];
applicationsNew[i][4] = row[4];
//row.Delete();
}
}
for (int i = 0; i < applicationsOld.Count(); i++) {
try {
if ((int)applicationsOld[i].Row["ID"] < 0) {
applicationsOld[i].Row.Delete();
}
} catch (RowNotInTableException) {
break;
}
}
this.tblCashReceiptsApplyToBindingSource.Filter = "CashReceiptsID = " + this.ReceiptID;
foreach (DataRow newRow in applicationsNew) {
if (newRow == null) {
break;
}
tblCashReceiptsApplyToBindingSource.AddNew();
((DataRowView)tblCashReceiptsApplyToBindingSource.Current).Row[0] = newRow[0];
((DataRowView)tblCashReceiptsApplyToBindingSource.Current).Row[1] = newRow[1];
((DataRowView)tblCashReceiptsApplyToBindingSource.Current).Row[2] = newRow[2];
((DataRowView)tblCashReceiptsApplyToBindingSource.Current).Row[3] = newRow[3];
((DataRowView)tblCashReceiptsApplyToBindingSource.Current).Row[4] = newRow[4];
}
tblCashReceiptsApplyToBindingSource.EndEdit();
checkForBadRows();
tblCashReceiptsApplyToTableAdapter.Update(rentalEaseDataSet.tblCashReceiptsApplyTo);
tblCashReceiptsApplyToTableAdapter.Fill(rentalEaseDataSet.tblCashReceiptsApplyTo);
}
You might want to try adding rows to the DataGridView. Since you are binding to it, the DataGridView becomes your 'access point'.
I've got several applications that bind to DataGridView and for most circumstances when I add a row I do so through the DataGridView. It already has properties/methods/events that let you add with relative ease.
If you need some more information I can update.
Related
I have Visual Studio 2019. The project is a .Net Windows Form on C# on .Net Framework 4.8.
I have a Datagridview which shows some tables data from different databases (MS SQL and Postgresql).
I merge that databases, and the result is too long, so we can't fit it on a screen; but we have to see all the data available on that screen, which are more than 40 columns. Reduce the font size is not plausible.
So, the solution proposed was to merge some values on the same column in this way (See this example):
The actual data view:
The way we need to view it:
If you have any ideas or you know an alternative to Datagridview which allows that, please share them.
Thanks in advance.
You can customize your datagridview's row and column to get the Multi-line columns on a datagridview.
I assume that the datatable is the table from database.
using System;
using System.Data;
using System.Linq;
using System.Windows.Forms;
private void Form1_Load(object sender, EventArgs e)
{
DataTable table = new DataTable();
table.Columns.Add("Name");
table.Columns.Add("Field1");
table.Columns.Add("Field2");
table.Columns.Add("Field3");
table.Columns.Add("Field4");
table.Columns.Add("Field5");
table.Columns.Add("Field6");
table.Columns.Add("Field7");
table.Rows.Add("test1", 1, 2, 3, 4, 5, 6, 7);
table.Rows.Add("test2", 1, 2, 3, 4, 5, 6, 7);
table.Rows.Add("test3", 1, 2, 3, 4, 5, 6, 7);
dataGridView1.ColumnHeadersVisible = false;
for (int i = 0; i < table.Columns.Count/2; i++)
{
dataGridView1.Columns.Add("","");
}
string[] columnNames = table.Columns.Cast<DataColumn>()
.Select(x => x.ColumnName)
.ToArray();
int count = table.Columns.Count/2;
var col1 = columnNames.Take(count).ToArray();
var col2= columnNames.Skip(count).Take(count).ToArray();
dataGridView1.Rows.Add(col1);
dataGridView1.Rows.Add(col2);
object[] arr;
for (int i = 0; i < table.Rows.Count; i++)
{
arr = table.Rows[i].ItemArray;
var row1=arr.Take(count).ToArray();
var row2 = arr.Skip(count).Take(count).ToArray();
dataGridView1.Rows.Add(row1);
dataGridView1.Rows.Add(row2);
}
}
Result:
After reviewing your question, I have to say that IMHO your solution is not giving much thought to the end user or if the code has to grab one of the values. Stacking fields into a single column “creates” two issues IMHO…One, as mentioned, is that the user is going to have to do extra work and check the order of the headers to distinguish which field value is which… a subtle yet (annoying) non intuitive extra step. Two, if the user is allowed to change fields or the code needs to grad a field, then, there is going to be extra work needed to differentiated which field goes with which value. Extra work for the user and extra work for the coder doesn’t sound like a good start.
Sorry about my rant. Fortunately, if you wanted to take a table as shown in the question, and turn it into a table as you describe, then the code below should do this. It basically creates “two” (2) field columns. Such that each column contains two fields. The code is hacky yet it is not too complicated I hope. I made numerous comments in the code. Some notes would be that, since we are adding two fields for each column and (as far as I know) a DataGridView won’t allow double column headers, the code does NOT use the column headers row and instead uses the first two rows of the grid for the two column headers. This will allow you to format the two rows to look like headers and/or color code if needed.
Lastly, a better solution IMHO. As previously mentioned a pivot will work, however, there are a couple of issues given how the data is stored in the original table. In a basic pivot where we switch rows and columns, the posted example would have three (3) columns… “Jim”, “Hugh” and “Terrance”. Then the number of rows would be (one (1) + however many fields/field columns). The extra “one” is the field “LastName.” Given this, it may look something like…
Jim Hugh Terrance
LastName Carey Jackman Hill
Field1 1 a N/A
Field2 2 b N/A
……..
It would appear obvious that the “LastName” should go with the column header. Therefore, the transpose/pivot may look like…
Jim Carey Hugh Jackman Terrance Hill
Field1 1 a N/A
Field2 2 b N/A
……..
IMHO, this will be more intuitive for the user to identify fields and there should not be any extra coding if we need to reference a specific value. The picture below shows a complete example from the code below. Drop three (3) DataGridViews onto a form and paste my code. The top-left grid is the original data. The bottom-left grid is the transpose as per your requirements and finally, the grid on the right is the what I feel would work best considering your dilemma.
A note on the last grid on the right… Initially, like the column header rows in your solution, I had the fields as a column in the grid. It will not be difficult to change the code if you want this. However, the code currently adds the field names as “row headers” in the grid. Since the DataTable does not really have row headers, this addition had to be made “after” the data source was set and can be seen in the forms Load event. Again, it will not be difficult to move the field to an added column in the DataTable.
To make this example complete, below is code to create some test data. The incoming parameter totalCols will make totalCols columns in the DataTable for the "Field" values.
private DataTable GetDataFromDB(int totalCols) {
DataTable dt = new DataTable();
dt.Columns.Add("Name", typeof(string));
dt.Columns.Add("LastName", typeof(string));
for (int i = 1; i <= totalCols; i++) {
dt.Columns.Add("Field" + i, typeof(string));
}
DataRow curRow;
string name;
string lName;
for (int i = 1; i < 4; i++) {
switch (i) {
case 1:
name = "Jim";
lName = "Carrey";
break;
case 2:
name = "Hugh";
lName = "Jackman";
break;
default:
name = "Terence";
lName = "Hill";
break;
}
curRow = dt.NewRow();
curRow["Name"] = name;
curRow["LastName"] = lName;
if (i < 3) {
for (int j = 2; j < dt.Columns.Count; j++) {
curRow[j] = "N" + i + "F" + (j - 1);
}
}
dt.Rows.Add(curRow);
}
return dt;
}
We will use three (3) global DataTables, one for each grid.
DataTable originalDT;
DataTable pivotDT1;
DataTable pivotDT2;
The load event that sets each grid to the proper DataTable and some specific formatting for each grid.
private void Form1_Load(object sender, EventArgs e) {
originalDT = GetDataFromDB(45);
dataGridView1.DataSource = originalDT;
// pivot 1 - bottom left grid
pivotDT1 = PivotTable(originalDT);
dataGridView2.DataSource = pivotDT1;
dataGridView2.DefaultCellStyle.Alignment = DataGridViewContentAlignment.MiddleCenter;
dataGridView2.Rows[0].DefaultCellStyle.BackColor = Color.Blue;
dataGridView2.Rows[1].DefaultCellStyle.BackColor = Color.Blue;
dataGridView2.Rows[0].DefaultCellStyle.ForeColor = Color.White;
dataGridView2.Rows[1].DefaultCellStyle.ForeColor = Color.White;
dataGridView2.Columns[0].Frozen = true;
// pivot 2 - right grid
pivotDT2 = PivotTable2(originalDT);
dataGridView3.DataSource = pivotDT2;
int dgvRow = 0;
// add column headers as row headers in the grid
for (int i = 2; i < originalDT.Columns.Count; i++) {
dataGridView3.Rows[dgvRow++].HeaderCell.Value = originalDT.Columns[i].ColumnName;
}
dataGridView3.RowHeadersWidthSizeMode = DataGridViewRowHeadersWidthSizeMode.AutoSizeToAllHeaders;
}
Finally, the two pivot/transform methods…
Using your solution and shown in the bottom-left grid…
private DataTable PivotTable(DataTable originalDT) {
DataTable pivotDT = new DataTable();
// the number of columns will be half the original number of columns
int halfCols = Math.DivRem(originalDT.Columns.Count, 2, out int rem);
// if there is a remainder then there is an odd number of columns and we need to add 1 col
if (rem > 0) {
halfCols++;
}
// add the columns to the pivot table
for (int i = 0; i < halfCols; i++) {
pivotDT.Columns.Add();
}
// the number of rows will be the number of original rows times 2
// PLUS 2 additional rows for the headers
for (int i = 0; i < (originalDT.Rows.Count * 2) + 2; i++) {
pivotDT.Rows.Add();
}
// Add the two header rows from the column names
int originalCol = 0;
for (int i = 0; i < halfCols; i++) {
pivotDT.Rows[0][i] = originalDT.Columns[originalCol++].ColumnName;
// if the original table had an odd number of columns
// then the last column only had one field
// - there would never be a column without at least one field
if (originalCol < originalDT.Columns.Count) {
pivotDT.Rows[1][i] = originalDT.Columns[originalCol++].ColumnName;
}
}
// finally add the rows from the original table.
int pivotRow = 2;
int pivotCol = 0;
int curPivotRow;
int curPivotCol;
string value;
for (int originalRow = 0; originalRow < originalDT.Rows.Count; originalRow++) {
curPivotRow = pivotRow;
curPivotCol = pivotCol;
for (originalCol = 0; originalCol < originalDT.Columns.Count; originalCol++) {
value = originalDT.Rows[originalRow][originalCol].ToString();
if (string.IsNullOrEmpty(value)) {
value = "N/A";
}
pivotDT.Rows[curPivotRow][curPivotCol] = value;
// if this is the first item then simply bump the pivot row
if (curPivotRow < pivotRow + 1) {
curPivotRow++;
}
else { // this is the second item -
// we want the curpivot row to start back at the starting pivotRow
// then move over a column for the next two columns in the original table
curPivotRow = pivotRow;
curPivotCol++;
}
}
// new row in the original data start back at column 0 in the pivot table
// and bump the row index by two since we added two rows
pivotRow += 2;
pivotCol = 0;
}
return pivotDT;
}
And my solution shown in the grid on the right.
private DataTable PivotTable2(DataTable originalDT) {
DataTable pivotDT = new DataTable();
for (int i = 0; i < originalDT.Rows.Count; i++) {
pivotDT.Columns.Add();
}
for (int i = 0; i < originalDT.Columns.Count - 2; i++) {
pivotDT.Rows.Add();
}
int pivotCol = 0;
foreach (DataRow row in originalDT.Rows) {
pivotDT.Columns[pivotCol++].ColumnName = row[0].ToString() + " " + row[1].ToString();
}
int pivotRow = 0;
pivotCol = 0;
string value;
for (int i = 0; i < originalDT.Rows.Count; i++) {
for (int j = 2; j < originalDT.Columns.Count; j++) {
value = originalDT.Rows[i][j].ToString();
if (string.IsNullOrEmpty(value)) {
value = "N/A";
}
pivotDT.Rows[pivotRow++][pivotCol] = value;
}
pivotCol++;
pivotRow = 0;
}
return pivotDT;
}
Finally, I am not that proficient using SQL, however, I am betting it is possible to create an SQL procedure that will produce my solution directly from the data base.
How do I get the data of a specific row stored in a list box by clicking on the particular row ? So if i click on the row i can then access that particular row by index then store it to be used later on
int myMaxResultValue = (int)nud_MaxResults.Value;
int myMaxSuggestValue = (int)nud_MaxSuggestions.Value;
findResults = objBvSoapClient( txt_Search.Text, txt_LastId.Text, cb_SearchFor.Text, text_Country.Text, text_LanguagePreference.Text, myMaxResultValue, myMaxSuggestValue);
if (txt_Search.Text.Length <= 2)// if less than two letters are entered nothing is displayed on the list.
{
ls_Output.Items.Clear();// Clear LstBox
ls_Output.Items.Add(String.Format(allDetails, "ID", "Text", "Highlight", "Cursor", "Description", "Next"));
MessageBox.Show("Please enter more than 2 Chars!!");
}
else if (txt_Search.Text.Length >= 3)// if greater than or equal to 3 letters in the search box continue search.
{
// Get Results and store in given array.
foreach (var items in findResults)
{
//Loop through our collection of found results and change resulting value.
ls_Output.Items.Add(String.Format(allDetails, items.Id, items.Text.ToString(), items.Highlight, items.Cursor, items.Description, items.Next));
}
}
Then to retrieve the whole string i have placed this function within the indexChanged event,:
if (ls_Output.SelectedIndex != -1)
{
int itemAtPostion = ls_Output.SelectedIndex;
string nextStep = "Retrieve";
if (ls_Output.Items[itemAtPostion].ToString().Contains(nextStep))
{
string selItem = ls_Output.SelectedItem.ToString();
MessageBox.Show("You have selected the following address: " + selItem);
lst_Retreive.Text = ls_Output.SelectedItem.ToString();
}
}
You can either get the index of the item or the item itself.
To get the item you can use
string item = listBox.SelectedItem.ToString();
To get the index of the item you can use
int idx = listBox.SelectedIndex;
If your listbox supports multiselect you can use
var items = listBox.SelectedItems();
and
var idx = listBox.SelectedIndices;
I was looking at this in a completly different way, and i should have been thinking about DataTables. I wanted to only be clicking on individual cells and hence the reason I was getting the whole string back rather than individual feilds. Heres how i Solved it
DataTable ss = new DataTable();
ss.Columns.Add("ID");
ss.Columns.Add("Text");
ss.Columns.Add("Highlight");
ss.Columns.Add("Cursor");
ss.Columns.Add("Description");
ss.Columns.Add("Next");
DataRow row = ss.NewRow();
row["ID"] = findResults[0].Id;
row["Text"] = findResults[0].Text;
row["Highlight"] = findResults[0].Highlight;
row["Cursor"] = findResults[0].Cursor;
row["Description"] = findResults[0].Description;
row["Next"] = findResults[0].Next;
ss.Rows.Add(row);
foreach (DataRow Drow in ss.Rows)
{
int num = dataGridView1.Rows.Add();
dataGridView1.Rows[num].Cells[0].Value = Drow["id"].ToString();
dataGridView1.Rows[num].Cells[1].Value = Drow["Text"].ToString();
dataGridView1.Rows[num].Cells[2].Value = Drow["Highlight"].ToString();
dataGridView1.Rows[num].Cells[3].Value = Drow["Cursor"].ToString();
dataGridView1.Rows[num].Cells[4].Value = Drow["Description"].ToString();
dataGridView1.Rows[num].Cells[5].Value = Drow["Next"].ToString();
}
if (txt_Search.Text.Length <= 2)// if less than two letters are entered nothing is displayed on the list.
{
MessageBox.Show("Please enter more than 2 Chars!!");
}
}
catch (Exception ex)
{
MessageBox.Show(ex.ToString());
findResults.Clear();
I know this question have been asked multiple times . But I could not find much help from anyone of those.
I don't want to convert the excel into data table but I want it to be converted to a list of objects and sent to server side for processing.
If it has more than 2K rows it should throw an error. Currently what I am doing is something like :
using (var excel = new ExcelPackage(hpf.InputStream))
{
var ws = excel.Workbook.Worksheets["Sheet1"];
for (int rw = 4; rw <= ws.Dimension.End.Row; rw++)
{
if (ws.Cells[rw, 1].Value != null)
{
int headerRow = 2;
GroupMembershipUploadInput gm = new GroupMembershipUploadInput();
for (int col = ws.Dimension.Start.Column; col <= ws.Dimension.End.Column; col++)
{
var s = ws.Cells[rw, col].Value;
if (ws.Cells[headerRow, col].Value.ToString().Equals("Existing Constituent Master Id"))
{
gm.cnst_mstr_id = (ws.Cells[rw, col].Value ?? (Object)"").ToString();
}
else if (ws.Cells[headerRow, col].Value.ToString().Equals("Prefix of the constituent(Mr, Mrs etc)"))
{
gm.cnst_prefix_nm = (ws.Cells[rw, col].Value ?? (Object)"").ToString();
}
else if (ws.Cells[headerRow, col].Value.ToString().Equals("First Name of the constituent(Mike)"))
{
gm.cnst_first_nm = (ws.Cells[rw, col].Value ?? (Object)"").ToString();
}
.....................
.....................
}
}
iUploadedCnt = iUploadedCnt + 1; //Increase the count by 1
}
if (lgl.GroupMembershipUploadInputList.Count < 2003) //Check for the uploaded list count
{
//throw the error
}
But this approach seems slow.
Conversion of the excel to list seems slow to me. For example , when I upload more than 2k records , the list gets converted first to list and then the count is checked if more than 2003 . This process is definitely slower.
How can it be achieved in a faster /better way ?
You do a lot of repeated string processing which is unnecessary. For each row you check the column headers again if they fit some predefined value. (for instance if (ws.Cells[headerRow, col].Value.ToString().Equals("Existing Constituent Master Id")).
You could do this once before you start parsing all rows and create for instance a Dictionary<int, SomeEnum> which maps the column number to a specific enum value. When parsing the rows you then can make a quick lookup in the dictionary, which column maps to which property.
Furthermore, you define a var s = ws.Cells[rw, col].Value; but never use it. Instead, you read this cell value again, when you assign it to a property of your object. You could just make the necessary conversions and checks here, and then use only s;
// define this enum somewhere
enum ColumPropEnum {
cnst_mstr_id, cnst_prefix_nm, ...
}
//define this prop somewhere
Dictionary<int, ColumnPropEnum> colprops = new Dictionary<int, ColumnPropEnum>();
//do this once before processing all rows
for (int col = ws.Dimension.Start.Column; col <= ws.Dimension.End.Column; col++) {
if (ws.Cells[headerRow, col].Value.ToString().Equals("Existing Constituent Master Id"))
colprops.Add(col, ColumnPropEnum.cnst_mstr_id);
else if (ws.Cells[headerRow, col].Value.ToString().Equals(" ..."))
colprops.Add(col, ColumnPropEnum.cnst_prefix_nm);
...
}
//now use this dictionary in each row
for (int rw = 4; rw <= ws.Dimension.End.Row; rw++)
{
....
for (int col = ws.Dimension.Start.Column; col <= ws.Dimension.End.Column; col++) {
//the single ? checks, whether the Value is null, if yes it returns null, otherwise it returns ToString(). Then the double ?? checks whether the result if the operation is null, if yes, it assigns "" to s, otherwise the result of ToString();
var s = ws.Cells[rw, col].Value?.ToString() ?? "";
ColumnPropEnum cp;
if (colpros.TryGetValue(col, out cp)) {
switch (cp) {
case cnst_mstr_id: gm.cnst_mstr_id = s; break;
case cnst_prefix_nm: gm.cnst_prefix_nm = s; break;
...
}
}
}
}
I'm not sure at which position you add this object to a list or upload it to the server, as this is not part of the code. But it could be faster, to first check only the first column of each row if you have the necessary count of non-null values and throw an error if not and do all the other processing only if you didn't throw the error.
int rowcount = 0;
//If you need at minimum 2000 rows, you can stop after you count 2000 valid rows
for (int rw = 4; rw <= ws.Dimension.End.Row && rowcount < 2000; rw++)
{
if (ws.Cells[rw, 1].Value != null) rowcount++
}
if (rowcount < 2000) {
//throw error and return
}
//else do the list building and uploading
I have a task to develop Windows applications where paging is involved. If I perform any event like splitting date and time, it's applied only to the current page. I would like to apply that event to all pages in the Datagridview.
If I take a datatable/dataset and work on it, the UI is taking time to read the file as it again reads the whole file to data table. So, please suggest any other alternative to apply the events to all pages in the DataGridView.
I will post the code, or upload my code in any site or here, if required.
Please let me know if my question is unclear.
VARIABLES DECLARATION:
List<String> cmbList = new List<string>();
public String Replace;
public String Find;
public String Col;
public String NewColumn;
public String NewColumnValue;
public string MyFOrmat { get; set; }
int PageCount;
int maxRec;
int pageSize = 30;
int currentPage = 1;
int recNo = 0;
string FileName;
String[] datfile;
button1 = BROWSE BUTTON (Where i read the file):
private void button1_Click(object sender, EventArgs e)
{
OpenFileDialog openFileDialog1 = new OpenFileDialog();
openFileDialog1.InitialDirectory = "Desktop";
openFileDialog1.Filter = "dat files (*.DAT)|*.DAT|All files (*.*)|*.*";
openFileDialog1.FilterIndex = 2;
openFileDialog1.RestoreDirectory = true;
if (openFileDialog1.ShowDialog() == DialogResult.OK)
{
try
{
FileName = openFileDialog1.FileName;
string text = System.IO.File.ReadAllText(FileName);
datfile = text.Split(new string[] { "\r\n", "\n" }, StringSplitOptions.None);
//Added on 2015-12-02
maxRec = datfile.Length - 1;
PageCount = maxRec / pageSize;
LoadPage(MyFOrmat);
}
catch (Exception ex)
{
MessageBox.Show("Error: Could not read file from disk. Original error: " + ex.Message);
}
}
}
LOADPAGE Code:
public void LoadPage(string Format, bool isFindAndReplace = false)
{
int startRec;
int endRec;
if (currentPage == PageCount)
{
endRec = maxRec;
}
else
{
endRec = pageSize * currentPage;
}
dataGridView1.Rows.Clear();
if (recNo == 0)
{
dataGridView1.Columns.Clear();
}
int rowindex = 0;
startRec = recNo;
for (int RowCount = startRec; RowCount <= endRec; RowCount++)
{
if (datfile[RowCount].ToString() != "" )
{
if (RowCount == 0)
{
string[] column = datfile[RowCount].Split('þ');
for (int i = 0; i < column.Length - 1; i++)
{
if (column[i].ToString() != "" && column[i].ToString() != "\u0014")
{
DataGridViewTextBoxColumn dgvtxtcountry = new DataGridViewTextBoxColumn();
dgvtxtcountry.HeaderText = column[i].ToString();
dgvtxtcountry.Name = column[i].ToString();
dataGridView1.Columns.Add(dgvtxtcountry);
cmbList.Add(column[i]);
i += 1;
}
}
}
if (RowCount != 0)
{
dataGridView1.Rows.Add();
string[] column = datfile[RowCount].Split('þ');
int index = 0;
for (int i = 1; i < column.Length - 1; i++)
{
if (column[i].ToString() != "\u0014")
{
if (i == 3)
{
dataGridView1.Rows[rowindex].Cells[index].Value = Convert.ToDateTime(column[i]).ToString(Format);
}
else
{ dataGridView1.Rows[rowindex].Cells[index].Value = column[i].Trim('þ'); }
index += 1;
i += 1;
}
}
rowindex += 1;
}
}
recNo += 1;
}
}
FIND and REPLACE Event:
private void btnFindandReplace_Click(object sender, EventArgs e)
{
Form2 f = new Form2();
f.cmbColumnCombo.DataSource = cmbList;
f.ShowDialog();
for (int i = 0; i <= dataGridView1.Rows.Count - 1; i++)
{
//dataGridView1.Rows[rowindex].Cells[index].Value = Convert.ToDateTime(column[i]).ToString(Format);
if (dataGridView1.Rows[i].Cells[f.cmbColumnCombo.Text].Value.ToString().ToLower().Contains(f.txtfind.Text.ToLower()))
{
//dataGridView1.Rows[i].Cells[f.cmbColumnCombo.Text].Value = dataGridView1.Rows[i].Cells[f.cmbColumnCombo.Text].Value.ToString().ToLower().Replace(f.txtfind.Text.ToLower(), f.txtreplace.Text);
//bulidDataRow(i);
if (!string.IsNullOrEmpty(f.txtfind.Text))
{
dataGridView1.Rows[i].Cells[f.cmbColumnCombo.Text].Value = dataGridView1.Rows[i].Cells[f.cmbColumnCombo.Text].Value.ToString().Replace(f.txtfind.Text, f.txtreplace.Text);
#region Commented
//dataGridView1.Rows[i].Cells[f.cmbColumnCombo.Text].Value = dataGridView1.Rows[i].Cells[f.cmbColumnCombo.Text].Value.ToString().Replace(f.txtfind.Text, f.txtreplace.Text);
//bulidDataRow(i);
#endregion
}
}
}
}
private void btnNext_Click(object sender, EventArgs e)
{
currentPage += 1;
if (currentPage > PageCount)
{
currentPage = PageCount;
//Check if you are already at the last page.
if (recNo == maxRec)
{
MessageBox.Show("You are at the Last Page!");
return;
}
}
LoadPage(MyFOrmat);
}
Please let me know if anything needs to be added.
To summarize your requirements:
You want to read largish data files of 10k - 500k records
You want to display them in chunks/pages in a DataGridView
You want allow the user to modify the data:
The user can merge columns
The user can use change&replace on the data
Date&time columns may be split
Possibly modified data shall be saved
The way I see it you have two approaches:
Either cache the data
Or cache the actions
Caching the actions is doable but clearly a lot more fuss, both in coding the caching and in keeping the data synchronized.
So caching the data would be my first choice.
Here is a sketch of how to break up the functionality:
A function to read in the whole data and load them into a DataTable
Functions for the initial display and for displaying a certain page
Functions for doing each of the changes on the list of rows.
After calling a changing function the current page display must be refreshed.
Keeping the total quantity of data in memory shouldn't really be a problem today; I notice that you are reading in all data as strings already in the datFile array. Reading it into a table will spare you to split it over and over..
A DataTable.DataRow also offers nice properies like HasErrors or RowState. And its Items can have a dedicated type to help with formatting..
Note however that DataRow doesn't have a (real) constructor; instead it must be created from a DataTable, so you will first have to create one from your columns!
The display code would use a pageSize and a currentFirstLine variable; it can clear and add the rows into the DGV or you could go for a binding solution with the DataTable you need anyway holding the DataRows and a filter on the table or rather on an BindingSource.
Of course you can also use a structure of your own, maybe a simple as a string[] or a List<string>to hold the row data..
If you are interested in the idea of caching the actions, you could create a ChangeAction class that holds:
the type
the parameters needed, ie, the column(s), the change&replace strings etc..
Then in a List<ChangeAction> you would store them as they happen and then apply them to each unchanged row. But here comes the first catch: You will need to know which row have been changed and maybe if a ChangeAction can be applied twice without screwing up the data.. More problems may or may not come later, depending on the details of you data and actions..
Here is an example of how to set up the binding using class level variables:
DataTable DT = new DataTable();
BindingSource BS = new BindingSource();
int pageSize = 0;
int firstLineVisible = 0;
After filling the table you can bind it and set the initial filer:
BS.DataSource = DT;
dataGridView1.DataSource = BS;
pageSize = (dataGridView1.ClientSize.Height - dataGridView1.ColumnHeadersHeight)
/ dataGridView1.Rows[0].Height;
int l1 = firstLineVisible; int l2 = firstLineVisible + pageSize;
BS.Filter = "Nr >= " + l1 + " and Nr < " + l2;
When scrolling you simply change the firstLineVisible and rest the Filter and the DataSource..
Now all your data modifications should work on the data in the DataTable using the SetField method!
Also note that you need one column in your data that holds a running number. If your data don't have one it is easy to include it by adding it to the data lines:
The column gets autogenerated in the DataGridView. For the DataTable we want to have it in the first data line; I use a separator string sep:
var lines = File.ReadAllLines(fileName).ToList();
..
string[] sep = { ";" };
var p0 = ("Nr" + sep[0] + lines[0]).Split(sep, StringSplitOptions.None );
DT.Columns.Clear();
for (int i = 0; i < p0.Length; i++) DT.Columns.Add(p0[i], typeof(string));
Adding it to the data is just as simple:
for (int l = 1; l < lines.Count; l++)
{
var p = (l + sep[0] + lines[l]).Split(sep, StringSplitOptions.None);
DT.Rows.Add(p);
}
You can hide the number column if you want to..:
dataGridView1.Columns["Nr"].Visible = false;
You should add that line right after setting the Filter.
I have a method that stores each line in a gridview into the database, then if the save is successful, removes the row; but if it isn't successful (cannot be stored in the db) it does not remove the row. Unfortunately, I can't get the row-removal to work properly.
This is my current code:
public static void SavePAC(PlantAreaCode_CreateView CView)
{
List<int> removeRows = new List<int>();
// For each cell in the DataGrid, stores the information in a string.
for (rows = 0; rows < CView.dataGridView1.Rows.Count; rows++)
{
correctSave = false;
if (CView.dataGridView1.Rows[rows].Cells[col].Value != null)
{
// Creates a model, then populates each field from the cells in the table.
PModel = new PlantAreaCode_Model();
PModel.AreaCode = Convert.ToString(CView.dataGridView1.Rows[rows].Cells[0].Value);
PModel.AreaName = Convert.ToString(CView.dataGridView1.Rows[rows].Cells[1].Value);
PModel.Comments = Convert.ToString(CView.dataGridView1.Rows[rows].Cells[2].Value);
// Passes the model into the Database.
Database_Facade.Operation_Switch(OPWRITE);
}
if (correctSave == true) // correctSave is set in the database insert method.
{
removeRows.Add(rows);
}
}
foreach (int i in removeRows)
{
CView.dataGridView1.Rows.RemoveAt(0); // Deletes all bar the last row, including any rows that cause errors
}
}
I have also tried:
foreach (int i in removeRows)
{
CView.dataGridView1.Rows.RemoveAt(i);
}
But that crashes at halfway, because the Rows index keeps changing each time a row is removed.
How can I achieve this? How can I remove a row if the save is successful, but keep it if there is an error?
May this help:
1] Make sure correctSave is being modified correctly.
2] Revert the loop flow, Looping backward allow to remove the row processed by the loop without affecting the index of the next row to process.
for (rows = CView.dgvCreate.Rows.Count - 1; rows >= 0 ; rows--)
3] Use CView.dataGridView1.Rows.RemoveAt(rows);
Try to populate collection of rows for removing with DataGridViewRow not with index. This works for me.
public void SavePAC(PlantAreaCode_CreateView CView)
{
List<DataGridViewRow> removeRows = new List<DataGridViewRow>();
foreach (DataGridViewRow row in CView.dataGridView1.Rows)
{
correctSave = false;
if (row.Cells[col].Value != null)
{
// Creates a model, then populates each field from the cells in the table.
PModel = new PlantAreaCode_Model();
PModel.AreaCode = Convert.ToString(row.Cells[0].Value);
PModel.AreaName = Convert.ToString(row.Cells[1].Value);
PModel.Comments = Convert.ToString(row.Cells[2].Value);
// Passes the model into the Database.
Database_Facade.Operation_Switch(OPWRITE);
}
if (correctSave == true) // correctSave is set in the database insert method.
{
removeRows.Add(row);
}
}
foreach (DataGridViewRow rowToRemove in removeRows)
{
CView.dataGridView1.Rows.Remove(rowToRemove);
}
}
You have to sort removeRows in descending order.
List<int> removeRowsDesc = removeRows.OrderByDescending(i => i);
Then use the foreach loop
foreach (int i in removeRowsDesc)
{
CView.dataGridView1.Rows.RemoveAt(i);
}
This way the reindexing wont affect the deletion.