I have a small Winforms program that I'm testing with. What I'm trying to do is get the row text before changes were made in the datagridview row. Right now, in the actual program (not this test version), when the user presses 'Save' I am updating records in a database. However, the users would like a log of the values before they were changed. That's what I'm trying to figure out... how to get the values before they were changed.
The UI looks like this:
The code looks like this:
using System;
using System.Data;
using System.Windows.Forms;
namespace DataGridViewDemo
{
public partial class Form1 : Form
{
public DataTable DataTable { get; set; }
public Form1()
{
this.InitializeComponent();
// Initialize table and populate with data.
this.DataTable = new DataTable();
this.DataTable.Columns.Add("Name", typeof(string));
this.DataTable.Columns.Add("Age", typeof(int));
this.DataTable.Rows.Add("Tom", 45);
this.DataTable.Rows.Add("Erin", 36);
this.DataTable.Rows.Add("Tom, Jr.", 9);
this.DataTable.AcceptChanges();
// Bind the datasource to the data table.
this.bindingSource1.DataSource = this.DataTable;
this.dgv.DataSource = this.bindingSource1;
}
private void btnSave_Click(object sender, EventArgs e)
{
// Get all changes made to the rows.
var changes = ((DataTable)this.bindingSource1.DataSource).GetChanges(DataRowState.Modified);
if (changes == null)
{
return;
}
// Now I've got every row change to the data table...
var changedRows = changes.Rows;
//.. but how can I get the same rows /before/ they were changed?
}
}
}
I've tried using the CurrentCellDirtyStateChanged event, but it fires on every keystroke in a cell. I'm wondering if the binding source contains a before image of the datatable before I AcceptChanges on it (which I'm not doing yet, in the program above).
When changes are made to data rows, the dataset retains both the original (Original) and new (Current) versions of the row. For example, before calling the AcceptChanges method, your application can access the different versions of a record (as defined in the DataRowVersion enumeration) and process the changes accordingly.
http://msdn.microsoft.com/en-us/library/7kd9zhee.aspx
Related
I am creating a winform application and using data binding to display information about a collection of objects. I've successfully retrieved Strings and Lists from the object and displayed them on my form. I'm running into trouble displaying the object's DataTable.
The object has a DataTable property:
public DataTable Table { get; set; }
The table will be populated by reading a file, but for debugging right now I'm manually creating and populating the table like so:
private void CreateDataTable()
{
Table = new DataTable();
Table.Columns.Add("ID", typeof(int));
Table.Columns.Add("FuelKg", typeof(int));
Table.Columns.Add("Model", typeof(string));
Table.Rows.Add(1, 800, "Boeing 747");
Table.Rows.Add(2, 1023, "Airbus A380");
Table.Rows.Add(3, 62, "Cessna 162");
}
I use data binding to bind the DataTable to my DataGridView:
BindingSource bsAirplanes = new BindingSource();
BindingSource bsTable = new BindingSource();
//...additional code...
//databinding for datatable
bsTable.DataSource = bsAirplanes;
bsTable.DataMember = "Table";
dataGridView2.DataSource = bsTable;
dataGridView2.AutoGenerateColumns = true;
When I load the form on the first record everything looks great.
However, when I move to any other record, the DataGridView that's bound to my DataTable does not populate. For each column in my datatable, I get an error stating:
System.ArgumentException: Column 'ID' does not belong to the table .
at System.Data.DataRow.CheckColumn(DataColumn column)
at System.Data.DataRow.get_Item(DataColumn column, DataRowVersion version)
at System.Data.DataRowView.GetColumnValue(DataColumn column)
at System.Data.DataColumnPropertyDescriptor.GetValue(Object component)
at System.Windows.Forms.DataGridView.DataGridViewDataConnection.GetValue(Int32 boundColumnIndex, Int32 columnIndex, Int32 rowIndex)
The DataGridView is now empty except for the column names.
I know that the columns do indeed belong to my table because I've manually stepped through the code and can see them in my DataTable object. It seems like there is some issue refreshing the data when I move to the next record. Perhaps the DataGridView is only bound to the DataTable in my FIRST Airplane object?
After many rounds of trial and error, I devised this solution. I am not sure it's the best solution, but it works. If there is a better way I'd love to know!
My suspicion that the bsTable BindingSource was linked only to the first object's DataTable seems to be correct. I need to manually update the bsTable's DataSource when I change the main object.
I added an event listener to my main DataGridView (bound to bsAirplanes). When the user selects a different row in the grid, the secondary DataGridView (bound to bsTable) is re-bound to the current Airplane's DataTable and the correct data populates.
In the Form1.Designer file:
this.dataGridView1.SelectionChanged += new System.EventHandler(this.dataGridView1_Changed);
In the Form.cs file:
private void dataGridView1_Changed(object sender, EventArgs e)
{
bsTable.DataSource = bsAirplanes.Current; //notice I am using bsAirplanes.Current, not bsAirplanes
bsTable.DataMember = "Table";
dataGridView2.DataSource = bsTable;
dataGridView2.AutoGenerateColumns = true;
}
I solved an error similar to this error by clearing and reseting the BindingSource before re-assigning its DataSource property like the following:
bsTable.DataSource = null;
bsTable.ResetBindings(true);
// ...
// recreate or reload myDataTable
// if I did not clear and reset the BindingSource,
// the following line throws the DataGridView.DataError
bsTable.DataSource = myDataTable.DefaultView;
I have a DataGridView binded with data from a database. I need to create a Form, which has input fields for data from a single grid row.
The Form has 30+ input controls - TextBoxes, Checkboxes and NumericUpDowns.
For now I went with this approach:
Retrieve current row from the DataGridView and load values from the cells to class instance
Pass the instance to the form and manually fill the input controls
Update the database from the form, update the DataGridView
I want to improve some things:
Is there any way to quickly fill all input controls from a class instance?
Is there any way to determine which input controls have changed their values besides manually subscribing every control on an event handler?
Is there any way to improve this whole thing, e.g. do something more efficiently?
If you are already passing in a DataRow, then you could instead pass in the DataTable and something that identifies the row in that table. And maybe optionally an adapter, if you want to commit the changes immediately on form exit. Then you can create a DataView of that table. And bind each edit control to a field in that view. Something like this:
public partial class EditForm : Form
{
DataRow row = null;
DataView view;
SqlDataAdapter adapter;
public EditForm(SqlDataAdapter adapter, DataTable table, int rowId)
{
InitializeComponent();
this.adapter = adapter;
view = table.DefaultView;
view.RowFilter = $"ID = {rowId}";
if (view.Count == 0) throw new Exception("no such row");
DataRowView dvr = view[0];
row = dvr.Row;
datebox.DataBindings.Add(new Binding("Value", view, "DATE"));
stringbox.DataBindings.Add(new Binding("Text", view, "O_STRING"));
this.FormClosing += EditForm_FormClosing;
}
private void EditForm_FormClosing(object sender, FormClosingEventArgs e)
{
if (row.RowState == DataRowState.Modified) adapter.Update(new DataRow[] { row });
}
}
The above assuming that your table has key column named ID and fields DATE and O_STRING.
This will save you the trouble of creating an intermediate custom class instance, based on that row, moving values to and from various objects and automatically sets the RowStatae in the original table.
Re: value changed indicators. Not sure if there is a really elegant way to do that. Firstly, if I had to, I would change the background (or foreground) color rather than font boldness. Setting font to bold would change the width of the content and that is usually quite annoying. Then, I would add handlers to the TextChanged events (or ValueChange events, for controls that are not text-based). You dont need to write custom handlers to each and every edit control - in the event handler, you get the object sender parameter that points to the control object. Then you can get the field name binded to that control with something like this:
private void stringbox_TextChanged(object sender, EventArgs e)
{
Control ctrl = (Control)sender;
string fieldName = ctrl.DataBindings[0].BindingMemberInfo.BindingMember;
if ((string)view[0].Row[fieldName] != ctrl.Text) ctrl.BackColor = Color.Pink;
}
That way you will only need to add a TextChanged handler once (per edit contol class), not one per every edit box you have.
I am creating a WPF Application that has 2 windows.
In Window_1 i have a button that imports an excel document and stores corresponding fields in a DataTable. After this excel document has been successfully imported i want to prompt user to view the document. If the user says 'Yes' then Window_2 is loaded.
In Window_2 there is a DataGrid and i want it to be populated using the DataTable in Window_1?
How do i populate the DataGrid in Window_2 using data stored in the DataTable in Window_1
i think i have found a solution to my problem. (I have derived this solution from ASh's response.)
In Window_1
DataTable maindata = new DataTable(); //datatable with imported excel data
//button click event to prompt user to view Window_2
private void yes_Click(object sender, RoutedEventArgs e){
var w2 = new Window { dt = maindata };
w2.Show();
}
In Window_2
public DataTable dt { get; set; }
private void Window_Loaded(object sender, RoutedEventArgs e)
{
dtGrid.ItemsSource = dt.DefaultView; //dtGrid is the DataGrid
}
So here is what i think.
Upon click 'Yes' (yes_Click) the data in maindata DataTable is equated to the data in dt DataTable (present in Window_2). The latter DataTable gets the data of maindata DataTable and loads it into DataGrid dt.
I thought i should put out my findings in case another person runs into the same problem.
Thank you all.
You could give the data from window 1 to window 2 like this:
var data = //your data;
Window_2 window = new Window_2(data);
window.Show();
//then in window 2
public Window_2(var data){
InitializeComponent();
this._fieldWithData = data;
}
So make a constructor overload where you pass the data. Then you can pass through the data from the different windows. StackOverflow is here to help to figure out on how to actually fill the datagrids, plenty of posts about that! Hope that helps!
This is the version of the code that works for me in a similar scenario.
In the first Window, in .xaml.cs I would place this:
DataTable myDataTable=new DataTable();
//fill in myDataTable with anything you want. Your case is Excel data
private void btnExtractComplementaryBOM_Click(object sender, RoutedEventArgs e)
{
ShowDataGrid window2 = new ShowDataGrid(myTable) ; //open up an instance of an existing Window called ShowDataGrid. Pass to it as an argument the dataTable object which embeds data from your Excel
ShowDataGrid.Show();
}
In the second Window (which can be customized as you wish by its .xaml).
You should have the following lines of code in ShowDataGrid.xaml.cs
public partial class ShowDataGrid : Window //2nd Window is called "ShowDataGrid" here
{
public DataTable dataTable2; //declare a public datatable variable
public ShowDataGrid(DataTable dataTable2)
{
InitializeComponent();
datagridName.ItemsSource = dataTable2.DefaultView;
}
}
From one window you can get the other window by name, then get the datagrid by name (as long as you have named everything).
Window w = Application.Current.Windows.OfType<Window>().SingleOrDefault(x => x.Name == "windowname");
DataGrid dg = (DataGrid)w.FindName("datagridname");
Then 'dg' is a reference to the datagrid that you want.
As stated here the DataBindingComplete event for a DataGridView is fired whenever the contents of the data source change, or a property such as DataSource changes. This results in the method being called multiple times.
I am currently using the DataBindingComplete event to do some visual formatting to my form. For example, I make the text in the first column (column 0) appear as Row Headers and then hide that column (see code below).
private void grdComponents_DataBindingComplete(object sender, DataGridViewBindingCompleteEventArgs e)
{
foreach (DataGridViewRow row in grdComponents.Rows)
{
row.HeaderCell.Value = row.Cells[0].Value.ToString();
}
grdComponents.Columns[0].Visible = false;
// do more stuff...
}
It is unnecessary to execute this code more than once, and I am looking to put it into a place where that can happen. Unfortunately it didn't work when I added the snippet to the end of my form's Load method (after I set the DataSource of my DataGridView), nor did it work in the DataSourceChanged event.
Yes, you can use DataSourceChanged event, but be aware, that it occurs only when data source is changed. Additionally, DataBindingComplete offers you information why it has happend - through e.ListChangedType:
Reset = 0,// Much of the list has changed. Any listening controls should refresh all their data from the list.
ItemAdded = 1,// An item added to the list
ItemDeleted = 2,// An item deleted from the list.
ItemMoved = 3,// An item moved within the list.
ItemChanged = 4,// An item changed in the list.
PropertyDescriptorAdded = 5,// A System.ComponentModel.PropertyDescriptor was added, which changed the schema.
PropertyDescriptorDeleted = 6,// A System.ComponentModel.PropertyDescriptor was deleted, which changed the schema.
PropertyDescriptorChanged = 7// A System.ComponentModel.PropertyDescriptor was changed, which changed the schema.
According to this answer:
https://social.msdn.microsoft.com/forums/windows/en-us/50c4f46d-c3b8-4da7-b08f-a751dca12afd/databindingcomplete-event-is-been-called-twice
the whole thing happens because you don't have DataMember property set in your dataGridView. And you can set it only if you want to set particular table from database which is set as your DataSource of dataGridView. Other way - throws an exception.
The simplest way will be just to execute this code once:
Add a flag like Boolean isDataGridFormatted in your form.
And check it like
private void grdComponents_DataBindingComplete(object sender, DataGridViewBindingCompleteEventArgs e)
{
if (this.isDataGridFormatted )
return;
foreach (DataGridViewRow row in grdComponents.Rows)
{
row.HeaderCell.Value = row.Cells[0].Value.ToString();
}
grdComponents.Columns[0].Visible = false;
// do more stuff...
this.isDataGridFormatted = false;
}
A bit better will be to prepare your DataGridView during the form construction. As I understand your columns won't change during the course of your program but you don't want to initialize everything manually. You could load some dummy one-item(one-row) data during the initialization:
private void Initialize_DataGridView()
{
// Add dummy data to generate the columns
this.dataGridView_Items.DataContext = new Item[]{ new Item {Id = 5, Value = 6}};
// Make your formatting
foreach (DataGridViewRow row in grdComponents.Rows)
{
row.HeaderCell.Value = row.Cells[0].Value.ToString();
}
grdComponents.Columns[0].Visible = false;
// Reset the dummy data
this.dataGridView_Items.DataContext = null; // Or new Item[]{};
}
...
public MyForm()
{
Initialize();
this.Initialize_DataGridView();
}
I am not sure that exactly such code will work with dataGridView but it is close enough.
Of course an event would have been a nearly ideal solution but there's hardly any that deals with successful autogeneration of columns http://msdn.microsoft.com/en-us/library/system.windows.forms.datagridview_events(v=vs.110).aspx except the AutoGenerateColumnChanged but that is not what we need.
While it is possible to use the ColumnAdded - it will probably execute only once foreach of the autogenerated column, the actual implementation could become an overkill and will be even less direct than already mentioned approaches.
If you will have some time and desire you could create your own DataGridView derived class, take Boolean isDataGridFormatted from your form and implement all the initialization(or event hooking) inside the custom DataGridView.
Please could anyone point me in the right direction for the reentrant exceptions I'm getting when I try and change the datasource of a DataGridView control. Below is a cut down example of the problem that I am currently unable to find a suitable solution for. I come from a Visual Foxpro background where this sort of thing is easily possible.
The only way I've partially solved this is by running a thread to update the datasource but because this is asynchronous there is a small chance that the update will not happen. Another way that works is to have a separate button control to reorder the data but this involves the user pressing it - a much better way would be to automate it after the cell update.
I understand why the grid gets upset on changing a datasource when editing data of another source. I also know that the DataGridView can be set to sort on columns but I want to do more than just sort in my full version.
To run the code below add a DataGridView (naming it dgv1) to a form. Then to make the exception happen change the cell that has the 9 in to a 3 and then move off the cell by either clicking another cell or using the arrow keys. If enter is pressed before highlighting another cell then the exception doesn't occur nor does the grid get reordered.
namespace dgv_test
{
public partial class Form1 : Form
{
private List<dv> grid;
public Form1()
{
InitializeComponent();
// this would be from a database
List<dv> data = new List<dv>
{
new dv{ desc="t1", order=1},
new dv{ desc="t2", order=2},
new dv{ desc="t3", order=9},
new dv{ desc="t4", order=4},
new dv{ desc="t5", order=5},
};
// in memory list
grid =
(from lu in data
orderby lu.order
select new dv
{
desc = lu.desc,
order = lu.order
}).ToList();
// grid list
dgv1.DataSource =
(from g in grid
orderby g.order
select new dv
{
desc = g.desc,
order = g.order
}).ToList();
// make description column readonly
dgv1.Columns[0].ReadOnly = true;
}
private void dgv1_CellLeave(object sender, DataGridViewCellEventArgs e)
{
// only update memory copy if order column changed
if (dgv1.CurrentCellAddress.X == 1 && dgv1.IsCurrentCellDirty)
{
// grid is in memory copy of grid data
grid.ElementAt(dgv1.CurrentCellAddress.Y).order = int.Parse(dgv1.CurrentCell.EditedFormattedValue.ToString());
rgrid();
}
}
// this is the function to update datagridview control
private void rgrid()
{
// query the in memory list from the database
var gx =
(from g in grid
orderby g.order
select new dv
{
desc = g.desc,
order = g.order
}).ToList();
// set the datagridview control with the newly ordered set
dgv1.DataSource = gx.ToList();
// do the same for the in memory list so that both are in alinement
grid = gx.ToList();
}
}
class dv
{
public string desc { get; set; }
public int order { get; set; }
}
}
Try moving your code to the CellValueChanged event of the DataGridView and it should work fine.