I'm not new to WinForms but I have always "rolled my own" when it comes to validation - and I think it's time to take advantage of the built-in stuff. I Googled around for some basics but I'm not finding what I need...
I've got a DataGridView. I have a (custom) object that has four String properties. I am getting a List<> of them from an XML file.
So when I do this:
dgv.DataSource = genericListOfStationObjects;
the rows do show up correctly in the DataGridView. So the databinding is working just fine - at least in the "incoming" direction.That's good.But what I need to do is:
track IsDirty on each row (without manually adding a flag?)
visually indicate (within the DataGridView) if any of the values in the DataGridView's cells are invalid. (I have validation methods on my custom object (of which the List<> is comprised.) I cannot get those "error glyphs" to show up. I've tried all the SO posts I could find on that...
Thank you very much,Eliezer
To answer your first question there are two native properties that you will want to utilize:
IsCurrentCellDirty
IsCurrentRowDirty
These only work for the currently selected cell/row. So you may have to resort to making your own if you need to track "IsDirty" on each row. But there is an event that comes with the native dirty settings, CurrentCellDirtyStateChanged you can utilize to log all of the changes. You could also use CellValueChanged to log any changes you might need. I personally use a form-level property to keep track if I have any data edits/changes by using either of these events and if I do, I save the edits before closing the form.
Data validation is fairly straightforward on the WinForms DataGridView. To get the red error glyphs to show simply set the ErrorText of a cell or row. I also utilize the DataGridView's native RowValidating event as well.
void dg_RowValidating(object sender, DataGridViewCellCancelEventArgs e)
{
DataGridViewRow dgRow = dg.Rows[e.RowIndex];
if ((dgRow.Cells["yourColumnName"].Value == null) ||
(dgRow.Cells["yourColumnName"].Value.ToString().Length == 0))
{
// Set both the row and cell error text at the same time.
dgRow.ErrorText = dgRow.Cells["dgTxtColTest List"].ErrorText =
"You must enter a value in the " + yourColumnName + " column."
e.Cancel = true;
}
}
When the row is validated you must clear any error messages you might have created above:
void dg_RowValidated(object sender, DataGridViewCellEventArgs e)
{
// Clear errors from row header and cells in the row
DataGridViewRow row = dg.Rows[e.RowIndex];
row.ErrorText = ""; // Clear the row header error text
// Clear all error texts from the row
foreach (DataGridViewCell cell in row.Cells)
{
cell.ErrorText = ""; // Clear each cell in the row as now row is valid
}
}
Related
I want the current row in a DataGridView. Not by mouse click but by pressing enter...
I know of this:
datagridview.CurrentCell.RowIndex
and
datagridview.CurrentRow.Index
and
datagridview.SelectedRows[0].Index
...
My problem is that generally this works ok except when I get to the last row. Because it always gets the index of the second last row.
Any idea how this could happen?
Catching the current row in a DataGridView is really quite simple and you have posted two ways which work just fine:
int currentRow = datagridview.CurrentCell.RowIndex;
or:
int currentRow = datagridview.CurrentRow.Index
The third one is actually rather problematatic as, depending on the SelectionMode of the DataGridView the current row may not be selected.
But your problems come from trying to grab the index in response to the user hitting the Enter-key.
This by default will move the current cell one row down, if there is one. So the behaviour will vary between the last and the other rows..
If there isn't a 'next' row, the current cell will either stay where it is or, if AllowUserToAddRows is true, the DGV will create a new, empty row and move there.
So if you always want to get the current index without moving the current cell you need to prevent the processing of the Enter-key.
Here is one way to do that:
private void dataGridView1_KeyDown(object sender, KeyEventArgs e)
{
if (e.KeyCode == Keys.Enter)
{
// don't pass the enter key on to the DGV:
e.Handled = true;
// now store or proecess the index:
Console.WriteLine(dataGridView1.CurrentRow + "");
}
}
The user will still be able to move around with the cursor keys.
If DataGridView is configured to allow row adds, current cell selection is a little confusing.
Let's say there is a DataGridView control with 5 valid rows of data, and user clicks on row 5. Then user clicks on row 6 and the new row is added to the display and the cell on row 6 is highlighted.
But CurrentCell.RowIndex and CurrentRow.Index remain set to row 5 (actual value=4), even though the UI no longer shows the focus there.
This has nothing to do with mouse or keyboard.
I detect this case with code like this:
bool lastRowSelected = false;
if (grid.SelectedCells != null)
{
foreach (DataGridViewCell cell in grid.SelectedCells)
{
if (cell.RowIndex >= grid.NewRowIndex)
{
lastRowSelected = true;
break;
}
}
}
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.
I have two datagridview columns; startdate and enddate using the custom MaskedTextBox column type described in this article: http://www.codeproject.com/Articles/26005/DataGridViewColumn-Hosting-MaskedTextBox
Now, I want to automatically move editing focus to the next cell on the same row when the user has entered a valid date in the column.
The way this MaskedTextBox column works is that in the OnTextChanged() event
I try to convert the .Text value to a DateTime, and if that happens without exceptions I assign that DateTime value to the DataGridView.CurrentCell.Value.
Then I try to move to the next cell using the DataGridView.CellValueChanged event like this:
private void myDataGridView_CellValueChanged(object sender, DataGridViewCellEventArgs e)
{
if (e.ColumnIndex == -1 || e.RowIndex == -1)
return;
//Move to next cell on the same row
if (myDataGridView.Columns[e.ColumnIndex].ValueType.Equals(typeof(DateTime)))
{
try
{
myDataGridView.EndEdit(DataGridViewDataErrorContexts.Commit);
int NewColIndex = myDataGridView.CurrentCell.ColumnIndex + 1;
if (NewColIndex > -1 && NewColIndex < myDataGridView.Columns.Count)
myDataGridView.CurrentCell = myDataGridView[NewColIndex, myDataGridView.CurrentCell.RowIndex];
}
catch
{
}
}
}
The editing focus DO get changed to the next column/cell, BUT I get a NullReference exception when I start typing in the second column/cell, originating from the PositionEditingControl() method in the DataGridViewMaskedTextCell class.
Can anyone please tell me why that happens?
Is there a "better" way to move to the next cell than setting .CurrentCell?
EDIT:
It seems this problem is related to the fact that the second column also uses this custom editcontrol. Tried the same thing with a regular TextBoxColumn as the second column and that works great. There is something with the way that the second column is initializing its editing control I guess?
From an ergonomy point of view, I recommend you to wait user press the 'Enter' Key prior validating the content of the cell. You should use the CellEndEdit event handler to catch it, instead of the CellValueChanged
Then, if your validation is successful (format is a DateTime), you could use the DataGridView::SelectNextControl() method.
As stated in this post: DataGridView SelectNextControl
don't forget to call DetatchEditingControl method of the CurrentCell
I want an OpenFileDialog to come up when a user clicks on a cell, then display the result in the cell.
It all works, except that the DataGridView displays an extra row, for adding values to the list it's bound to. The row shows up if dataGridView.AllowUserToAddNewRows == true, which is what I want. What I don't want is for the application to crash when that row is edited programatically; instead, it should do exactly what it would do if the user had edited that row manually (add the new row to the underlying list, push another empty row onto the grid for adding values).
I read about SendKeys.Send(), which should make the DataGridView behave exactly as though the user had typed the value in; however, it does not work either. Here is what I am trying:
if (openFileDialog1.ShowDialog() == DialogResult.OK)
{
dataGridView1.CurrentCell = cell;
//simply doing a cell.Value = etc. will cause the program to crash
cell.ReadOnly = false;
dataGridView1.Columns[cell.ColumnIndex].ReadOnly = false;
dataGridView1.EditMode = DataGridViewEditMode.EditOnEnter;
dataGridView1.BeginEdit(true);
SendKeys.Send(openFileDialog1.FileName + "{Enter}");
dataGridView1.EndEdit();
cell.ReadOnly = true;
dataGridView1.Columns[cell.ColumnIndex].ReadOnly = true;
}
//I would expect the FileName would be in the cell now, and a new empty
//row tacked onto the end of the DataGridView, but it's not; the DataGridView
//is not changed at all.
I found a workaround on this page, though I don't know why it works
public MyForm()
{
InitializeComponent();
//Create a BindingSource, set its DataSource to my list,
//set the DataGrid's DataSource to the BindindingSource...
_bindingSource.AddingNew += OnAddingNewToBindingSource;
}
private void OnAddingNewToBindingSource(object sender, AddingNewEventArgs e)
{
if(dataGridView1.Rows.Count == _bindingSource.Count)
{
_bindingSource.RemoveAt(_bindingSource.Count - 1);
}
}
I'm getting very sick of spending so much time dealing with Visual Studio bugs...
I was having the same problem when trying to programattically edit cells with a binding source.
""Operation is not valid due to the current state of the object"
Which operation? What State? So helpful.
My code seem to work fine except when editing the last row in the grid.
Turns out the key is DataGridView.NotifiyCurrentCelldirty(true)
The correct sequence for programatically editing a cell, so it works the same as if the user did it.
(A new empty row appears when changing a cell in the last row) is something like this:
1) Make the cell to edit the current cell (do what ever you need to the current currentcell, first
like calling endEdit if it is in edit mode.)
2) Call DataGridview.BeginEdit(false)
3) Call DataGridView.NotifyCurrentCellDirty(true)
4) Modify the value.
5) Call DataGridView.EndEdit()
And you'll want to do something for the RowValidating and RowValidated events.
One of my routines for updating a cell value looks like this:
This is from a method in my class derived from DataGridView.
You could do the same thing from the containing form, calling
through a DataGridView instance, because the methods are public.
Here the calls are using an impliciit 'this.'
private void EnterTime()
{
if (CurrentRow == null) return;
SaveCurrentCell(); // Calls EndEdit() if CurrentCell.IsInEditMode
DataGridViewCell previous = CurrentCell;
CurrentCell = CurrentRow.Cells[CatchForm.TimeColumn];
BeginEdit(false);
NotifyCurrentCellDirty(true);
CurrentCell.Value = DateTime.Now;
EndEdit();
CurrentCell = previous;
}
I’m not sure why a separate call is needed.
Why doesn’t BeginEdit, or actually modifying the cell value, cause the right
things to happen?
And if you move the NotifyCurrentCellDirty call to after you actually modify the cell,
it doesn’t behave correctly either. All very annoying.
This is old, but I am running VS2010 and just come across this issue. I have a DataGridView bound to a List<T> using a BindingList<T>. I have a drag n' drop event on my DataGridView and it would throw this exception after deleting all rows from the DGV (except for the last blank one which one cannot delete) and then adding new rows to the DGV in the DragDrop handler via the BindingList<T>. This exception was not thrown if I simply added rows manually editing individual cells.
One solution I read said to handle the BindingList<T>.AddNew event, but I found that this event did not fire when calling BindingList<T>.Add() within the DragDrop event handler (I'm not sure why). I solved the issue by adding
if(bindingList.Count == 0)
bindingList.RemoveAt(0)
inside of the DragDrop event handler before adding new objects to bindingList. It seemed that adding an object to the bindingList failed when the only "object" in the bindingList was the one associated to the final blank row. The point of a BindingList<T> is to allow the developer to work with it instead of the DGV directly, but it seems doing so can cause problems in border cases.
The relationship between DGV rows and BindingList<T> rows seems to be a bit of a gray area. I have not spent much time investigating this, but it is not clear to me what is the state of the "object" in the BindingList<T> associated to the final (empty) row of the DGV. However, it does seem like the "object" at the end is only instantiated "correctly" when you interact with the final row directly (not via a DataSource).
Try this:
if (openFileDialog1.ShowDialog() == DialogResult.OK)
{
int row = e.RowIndex;
int clmn = e.ColumnIndex;
if(e.RowIndex == dataGridView1.Rows.Count- 1)
dataGridView1.Rows.Add();
dataGridView1.Rows[row].Cells[clmn].Value = openFileDialog1.FileName;
}
EDIT
I didn't notice that you are binding your datagridview :(
Ok, to solve it: use binding source, set its DataSource property to your list, then set the data source of the data grid view to this binding source. Now, the code should look like so:
public partial class frmTestDataGridView : Form
{
BindingSource bindingSource1 = new BindingSource();
List<string> datasource = new List<string>();
public frmTestDataGridView()
{
InitializeComponent();
datasource.Add("item1");
datasource.Add("item2");
datasource.Add("item3");
bindingSource1.DataSource = datasource;
dataGridView1.DataSource = bindingSource1;
}
private void dataGridView1_CellClick(object sender, DataGridViewCellEventArgs e)
{
if (openFileDialog1.ShowDialog() == DialogResult.OK)
{
int row = e.RowIndex;
int clmn = e.ColumnIndex;
if (e.RowIndex == dataGridView1.Rows.Count - 1)
{
bindingSource1.Add("");
}
dataGridView1.Rows[row].Cells[clmn].Value = openFileDialog1.FileName;
}
}
}
Remember to use Row.BeginEdit() and Row.EndEdit() if you get this error while editing a value in a row, using DataGrid or GridEX from Janus (in my case). The sample code that Darrel Lee posted here (https://stackoverflow.com/a/9143590/1278771) remind me to use these instructions that I forgot to use and this solved the problem for me.
I have a DataGridView that is bound to a list of object. It has some columns that the user can edit. There are certain inputs that are not allowed for a row as a whole. How can I roll back if the user enters invalid inputs in some cell.
I tried using the RowValidating event handler but it was not called after cell value has been changed. Even when I implemet CellValueChanged, I still cannot roll back the changes.
...
Any idea how to accomplish this
When databinding exists, for me it works with:
myBindingSource.CancelEdit();
myDataGridView.RefreshEdit();
Once editing has been completed and you validate the changes, you can do this:
DataTable dt = this.dataGridView.DataSource as DataTable;
dt.RejectChanges();
From MSDN:
When the DataTable.RejectChanges
method is called, any rows still in
edit-mode cancel their edits. New rows
are removed. Modified and deleted rows
return back to their original state
(DataRowState.Unchanged).
You can use the CellValidating event to check the contents of the cell just before it is committed. If you don't like it (whatever your validation rules are), you have a few options.
1) You can Cancel the event. The user gets an error icon on the row, and cannot leave the cell. They are locked into the cell edit behavior until they commit the cell (Enter, Tab) with valid data.
2) You can roll back the value to another value (the previous value, some default value).
private void dataGridView1_CellValidating(object sender, DataGridViewCellValidatingEventArgs e)
{
DataGridView grid = sender as DataGridView;
if (grid.Columns[e.ColumnIndex].HeaderText == "Text ID")
{
// Suppose you want to prevent them from leaving a cell when the text
// in a specific column contains spaces:
// value will hold the new data
string value = (string)e.FormattedValue;
if (value.Contains(" "))
{
grid.Rows[e.RowIndex].ErrorText = "String IDs cannot contain spaces.";
// Setting e.Cancel will prevent them from leaving the cell.
e.Cancel = true;
}
}
else if (grid.Columns[e.ColumnIndex].HeaderText == "Platform")
{
// Or, suppose you have another column that can only contain certain values.
// You could have used a ComboBoxColumn, but it didn't play with paste, or something.
if (grid.EditingControl != null && (string)e.FormattedValue != "All")
{
// Going straight to the EditingControl will allow you to overwrite what
// the user thought they were going to do.
// Note: You don't want to e.Cancel here, because it will lock them
// into the cell. This is just a brute force fix by you.
string oldvalue = (string)grid.Rows[e.RowIndex].Cells[e.ColumnIndex].Value;
grid.EditingControl.Text = "All"; // or set it to the previous value, if you like.
}
}
}